第二周课程学习总结

用户头像
Geek_a327d3
关注
发布于: 2020 年 06 月 17 日



编程语言的种类?

面向机器

底层的汇编语言,需要按照机器的语言逻辑进行开发,push pop mov add sub 等指令。



面向过程

按照人的思维逻辑进行开发,只要了解所用语言的语法即可。代码量庞大时,项目难以维护,难扩展。



面向对象

万物皆对象

程序是对象的集合,它们通过发送消息来告知彼此所要做的

每个对象都有自己的由其他对象所构成的存储

每个对象都拥有自己的类型

某一特定类型的所有对象都可以接收同样的消息



面向对象设计的目的

强内聚,低耦合,从而使系统,易扩展,更强壮,可移植,更简单。



什么是对象?面向对象的特征?

对象具有状态、行为和标识

状态:表明每个对象可以有自己的数据。



什么是无状态对象?

无状态对象是指没有属性的对象,只有一些方法。



有状态对象 VS 无状态对象

有状态对象当多线程访问对象状态时,可能会涉及到线程安全问题。

无状态对象当多线程访问时,不需要考虑线程安全问题。



行为:表明每个对象可以产生行为。



什么是对象行为?

对象行为是指每个对象都有自己的方法。



标识:表明每个对象区别于其它的对象。



什么是对象标识?

例如我们在开发中,开发User管理模块,每个User都有ID属性,每个ID属性的值也肯定是不一样的。

每个对象实例的内存地址肯定也是不一样的,也可以被认为是对象的唯一标识。



封装性

隐藏实现,不需要知道对象内部是如何实现的,只需要拿来用就可以了。对象只暴露可提供的方法。



继承性

子类继承父类,就拥有了父类权限大于等于protected的方法以及属性。

继承需要符合 is-a 的关系

例如

正例:麻雀继承鸟类,麻雀 is-a 鸟;

反例:栈(Stack) 继承 集合(Vector), 栈是先进后出的数据结构,而Vector是一个线性数据结构,所以 Stack is not a Vector。



多态性

子类实现父类或者接口的抽象方法,程序使用抽象父类或接口编程,运行期注入不同的子类,程序就会表现 出不同的形态,是为多态。



框架 VS 工具

框架(框架调用应用程序代码):

系统的骨架已经搭建好,开发者需要按照框架的要求配置,或者实现某些接口,只有按照框架的开发规范才能运行项目。

例如:SpringMVC 我们需要按照Spring MVC规范定义Controller,才可以接收到用户的请求。

工具(应用程序代码调用工具):

只是拿来用的工具,例如StringUtils类,用来判断字符串是否为空,实际上我不用这工具也可以开发项目。

框架是必须按照框架的规范开发项目,项目才能跑起来。



工具只是工具,用不用都不会影响到项目运行。

好的设计 VS 坏的设计

好的设计:强内聚,低耦合,从而使程序易扩展,更强壮,可移植,更简单。

坏的设计:僵硬,脆弱,不可移植,导致误用的陷阱,晦涩,过度设计。



需求:

我之前所做的项目其中包含这样一个功能,校验零部件是否符合校验规则。

一个零部件由多个零部件或者多个材料组成;

一个材料可以由多个材料或者物质组成;

CAMDS校验:

  1. 一个材料的组成部分要么全部是材料,要么全部是物质,如果是物质,这个材料下含有的所有物质的总含量必须是100%;

BGO校验:

  1. 每个材料的组成部分,如果是物质,有害物质的含量不能超过一定的比例。

(PS:我记得当时还有很多校验规则,比如IMDS,具体的规则想不起来了,就以两个为例吧)



反例:

public class Checker {
public List<CheckResult> check(Commponent rootCommponent){
if(ObjectUtils.allNotNull(rootCommponent)){
// 进行递归
// 如果是材料 并且材料的子节点是物质,将所有物质的含量加在一起,
// 如果总含量少于100% 将校验错误添加到List当中
// 如果是物质,找出有害物质列表,如果该物质是有害物质,判断有害物质含量是否超标
// 如果超标 将校验错误添加至List中
}
// 返回错误列表
}
}

首先它肯定是僵硬的、脆弱的、难移植的;因为判断逻辑很复杂,假如需求只有CAMDS校验和BGO校验,过了3天又提出了IMDS校验,XXX校验,你首先要看递归逻辑,然后再写入你的逻辑,几个校验混在一起,很容易改错代码(改错代码可能有些夸张,有些校验的逻辑特别复杂,代码特别多),但你做起来会很烦,很多临时变量混在一块。



PS:

为什么说难移植? 这个项目是供车企使用的,当时有很多车企都定制化了很多不同的功能,导致当时项目有很多个版本,不管哪个版本加校验 改校验,那一坨坨的代码,自行体会。

为什么说脆弱的、僵硬的?当时这个类中加入了很多私有方法,也都是公用的,改点逻辑就容易引起问题。非常难扩展。



重构后:

public interface Checker{
CheckResult check(Commponent commponent);
}
public CAMDSChecker implements Checker{
public CheckResult check(Commponent commponent){
// CAMDS校验
}
}
public BGOChecker implements Checker{
public CheckResult check(Commponent commponent){
// BGO校验
}
}
public class Application{
private List<Checker> checkers = new ArrayList<Checker>();
public void addCheck(Checker checker){
if(checker != null){
checkers.add(checker);
}
}
public void check(Commponent commponent){
for(Checker checker : checkers){
// 校验保存异常
}
}
}

大体设计就是如此;

首先它是一眼就能看懂的,CAMDSChecker就是做CAMDS校验的,BGOChecker就是做BGO校验的。

如果我们加入新的校验方法,例如IMDS,加入IMDSChecker实现Checker接口即可,不用看原来那一坨坨的代码,新建个类直接撸就好。

如果想看校验方法,CAMDSChecker就是CAMDS校验,你不用像原来一样从很多代码中找到这个CAMDS,这也算分而治之的一种思想吧。好处就不多说了,自行体会。



从4个方面判断

易扩展,易添加新功能(符合)

更强壮,不容易被粗心的程序员破坏(符合)

可移植,能够在多样的环境下运行(符合)

更简单,容易理解,容易维护(符合)



面向对象设计原则

单一职责原则(SRP)

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。



反例:

我们设计一个OrderService,其中包含保存订单,查询订单,第三方下单。

保存订单需要访问第三方接口(微信)下单,返回对应的信息,客户端(小程序)会使用该信息进行付款。

此时来了一个新需求,要求支持支付宝下单,支付宝支付,这时我们就需要修改OrderService对应的实现类,实际上OrderService应只负责订单校验、调用Dao将订单保存到Service,自己本身不应该负责对接第三方下单接口。在保存订单后直接调用UnifiedOrder接口进行下单,将UnifiedOrder接口返回的结果直接返回给客户端。

UnifiedOrder可以有很多种实现,例如WechatUnifiedOrder(微信支付下单),AliPayUnifiedOrder(支付宝支付下单)等等。

开闭原则(OCP)

软件实体(模块、类、函数等)应该对扩展是开放的,对修改是关闭的。

通俗的讲就是:软件功能是可以扩展的,但是软件实体是不可以被修改的。

当我们在代码中看到else 或者 switch/case 关键字的时候,基本可以判断违反开闭原则了。



我之前做过一个物联网的系统,是关于鱼菜共生的设备数据检测,如果水的温度过高发出报警,如果水的温度过低,也会发出报警;如果水的质量出现问题也会发出报警;(当时会有很多参数影响报警,这里只拿水参数做例子)。设备上报的数据经过解码后是这样的:{"temperature":100,"param1":10,"param2":10} (PS忘记当时具体是什么参数了,就拿param1,param2做例子好了)

反例:

public class Checker{
public List<Alarm> check(WaterParam param){
List<Alram> alarms = new ArrayList<Alarm>();
if(param.getTemperature() < LOW || param.getTemperature() > HIGH){
alarms.add(Alarm());
}
// 其他判断条件 ...
// 以后的需求如果发生变化 不管添加 或修改 都会改动这个类,如果需求过多这个放大代码将惨不忍睹
// 而且所有规则都在这里 代码改一处会不会影响其他地方
}
}

出现关于水报警的需求改动 就需要动Checker的代码。

正例:

代码如下(代码简陋些):

public interface Checker{
Alarm check(WaterParam water);
}
public class TemperatureChecker implements Checker{
private static final int LOW = 10;
private static final int HIGH = 20;
public Alarm check(WaterParam water){
if(water.getTemperature() > HIGH || water.getTemperature() < LOW){
return new Alarm("报警了");
}
}
}
public class Param1Checker implements Checker{
public Alarm check(WaterParam water){
// 一些判断条件 如果触发报警条件 就返回报警
}
}
public class Application{
private List<Checker> checkers = new ArrayList<Checker>();
public void addCheck(Checker checker){
if(checker != null){
checkers.add(checker);
}
}
public void check(WaterParam water){
Alarm alarm = null;
for(Checker checker : checkers){
if((alarm = checker.check(water)) != null){
// 保存报警
}
}
}
}
public class Client{
public static void main(String[] args){
Application application = new Application();
application.addChecker(new TemperatureChecker());
// application.addChecker(new Param1Checker());
application.check(WaterParam);
}
}

新需求:Param1不需要检测了,也不需要报警了,那么 将add(Parm1Checker)代码去掉即可。

这里代码是改变了,因为去掉了某行代码。实际上我没有必要纠结某个代码是"修改"还是"扩展",更没有必要太纠结它是否违反"开闭原则"。只要它没有破坏原有的代码正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。开闭原则的初衷还是要求我们的功能一定要可扩展,扩展不会动到核心的代码或类。

里氏替换原则(LSP)

继承是否合理我们需要用里氏替换原则来判断。是否合理并不是从继承的设计本身看,而是从应用场景的角度看。如果在应用场景中,也就是在程序中,子类可以替换父类,那么继承就是合理的,如果不能替换,那么继承就是不合理的。

反例:

  1. 父类中提供的sortPerson()方法是按照人员的年龄进行排序,而子类重写了sortPerson()方法,按照ID进行排序。那子类就违背了里氏替换原则。

  2. 父类某个函数约定,运行出错时返回null,子类重写了该方法,运行时出错直接抛出了异常。

  3. 在父类中,某个函数约定,输入数据可以是任意类型,但子类实现的时候,输入数据只可以是int类型(Java中Map与Properties)



接口隔离原则(ISP)

不应该强迫客户程序依赖它们不需要的方法,从客户的需要出发,强调不要让客户看到他们不需要的方法。

例如

在UserService接口中,含有save,update,select,delete方法,delete方法是非常危险的一个方法,需要在特定的场景下才可以调用。如果delete方法也定义在UserService中,客户使用UserService接口时就会看到delete方法,如果客户误调用就会导致很多麻烦。因此我们将将delete方法定义在UserDiscardService中,UserServiceImpl实现UserDiscardService、UserService接口。这样UserService接口就不会看到不该看到的方法,在需要使用delete方法时,直接使用UserDiscardService接口即可。



依赖倒置原则(DIP)

依赖倒置原则是指高层模块不应该去依赖低层模块,它们应依赖共同一个高层抽象。

也就是说,接口被高层模块定义,高层模块拥有接口,低层模块实现高层接口。

不是高层模块依赖低层的接口,而是低层模块依赖高层接口,从而实现依赖关系的倒置。



对于Service和DAO这个例子来说,就是Service定义接口,DAO实现接口,这样才符合依赖倒置原则。



所以依赖倒置也被称为好莱坞原则,即 你不要调用我,我会去调用你。



高层模块&低层模块:

例如 我们日常使用MVC三层架构中的Controller与Service,Controller就是高层模块,而Service就是低层模块,因为Controller会去调用Service。

例如 我们日常开发中,前端开发人员与后端开发人员,前端开发人员接到需求后,在做页面的同时,他们知道自己需要什么样的接口,接口需要后端人员开发。所以前端人员属于高层模块,后端开发人员属于低层模块。然而我们日常开发都是由后端开发人员撰写接口文档交给前端开发人员,实际上这并不符合依赖倒置原则。根据依赖倒置原则,前端开发人员(高层模块)应该定义接口(高层抽象),前端开发人员(高层模块)与后端开发人员(低层模块)共同依赖这个接口文档进行工作(高层抽象),后端开发人员实现这个高层抽象,前端开发人员调用高层抽象的实现。



PS:有些总结可能过于粗糙,如有不对,还请助教大佬以及各位同学斧正,谢谢。

用户头像

Geek_a327d3

关注

还未添加个人签名 2020.04.14 加入

还未添加个人简介

评论

发布
暂无评论
第二周课程学习总结