SOLID 设计原则(第二周 + 作业)

发布于: 2020 年 06 月 18 日

感悟

本周课程主要是讲解面向对象的SOLID设计原则。本是OOD的基础,是多年来面试求职者的必问题目,虽然极少得到比较满意的回答,但是此次却也从老师的讲解中也有了新的感悟。

What

使用面向对象语言,不等于是在做面向对象编程。封装,继承,多态是面向对象的三大特征,其中多态是核心。如果不使用多态,那连使用面向对象语言都谈不上。

面向对象编程语言很多,Java,C++,甚至python,javascript都支持面向对象编程范式,尤其Java是最为著名的主流纯面向对象(不纠结8种primitive types)编程语言。但是常话说得好“你只是在Java面向对象语法下写C语言代码”,掌握一门面向对象语言不难,可是做到正真的OOAD和OOP就不是看上去那么容易了。DDD是正确OOAD的方法,而SOLID正是指导正确OOP的方法。

SRP(Single Responsibility Principle)单一职责原则

一个类只有一个引起它变化的原因。所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。

简单说,一个类应该只指代问题域中的一种事物,代表是个纯净的概念(抽象),尽可能简洁单纯。这个原则其实是最难实现的,如何平衡复杂度,抽象得刚刚好,既不过于细节琐碎而难以编程,又不能太大而化之,使得类杂合过多职责而难以复用。

最佳的设计方式是从行为开始做接口抽象,通过接口交互和数据分析得到接口,实现类和entity。

OCP(Open Close Principle)开闭原则

一个类对扩展开发,对修改封闭。简单的说就是当有新的需求变化是,我们不想修改已有的类,而是想通过扩展来加强类。decorator模式就是一个很好的例子。通过装饰器模式,可以在不改变原来代码的情况下加强原来的类。servlet的filter和Spring MVC的interceptor都是OCP的示例。

这是一个被广泛应用的思想。在ops的世界里,不也是只想增量改变,而不想要breaking change么?不引入breaking change的办法,最简单不就是不change,使change是一个increment,增加新功能同时不影响已经运行的功能。

LSP(Liskov Substitution Principle)里氏代换原则

所有使用父类的地方,都可以用子类代替。这个大概是看起来简单,但最难正确理解的基本原则了。Java天然支持多态,子类实例可以赋值给父类变量,所以LSP天然有语言层面的支持。但是就这么简单,就不是LSP了,Liskov就不会因为这个获得图灵奖了。文章和书本上讲解LSP时,大多会用到正方形,长方形的例子,很多年没参透这个例子的内涵,最后多数是死记LSP的规则:

  1. 子类可以增加类变量和方法。(增加的类变量导致override equals方法也不行,子类override equals方法使得对称性和传递性不可兼得,进而破坏LSP)

  2. 可以overload,但不要override父类方法

  3. 非要override父类方法,要接受父类方法的全部前置条件,保持全部后置条件

  4. 方法参数应是父类方法参数的子类

  5. 方法的返回参数应是父类方法的父类

  6. 方法异常应是父类方法的子类

这次非常感谢老师的一句话,

LSP要在场景中讨论。LSP要求继承关系是场景中的真IS-A关系,不是人心中的继承关系。

为什么正方形,长方形是好例子?因为我们都知道在数学上,正方形是特殊的长方形,正方形应该是长方形的子类,但是当面向对象编程时,落实到具体的场景中,正方形就未必是长方形的子类了!

class Rectangle {
private int width;
private int height;
public void setWidth(int w) {
this.width = w;
}
public int getWidth() {
return this.width
}
public void setHeight(int h) {
this.height = h
}
public int getHeight() {
return this.height
}
}
class Square extends Rectangle {
public void setWidth(int w) {
this.setWidth(w)
this.setHeight(w)
}
public void setHeight(int h) {
this.setHeight(h)
this.setWidth(h)
}
}

我们来构造一个Calculaor类计算面积的场景。

class Calculator {
public int area(Rectangle r) {
return r.getWidth() * r.getHeight()
}
}

此时正方形,长方形的继承关系仍然符合我们的常识。但是当我们想向Calculator加一个halfArea()方法时,这个关系就出问题了,看代码

public int halfArea(Rectangle r) {
int halfWidth = r.getWidth() / 2;
return halfWidth * r.getHeight()
}

这段代码基于父类Rectangle建立了一个合约:面积是长X宽,一半面积可以减少一半长来计算。但是当传入Rectangle的子类Square时,这个合约被破环了,Square的长减少一半,面积会减少到四分之一。所以,这个场景下,Rectangle和Triangle是一类——宽减少一半,面积也减少一半,是IS-A关系;而Square和Circle才是一类——边长/半径减少一半,面积减少到四分之一。

可见,引入简单的面积计算场景,我们习以为常的,从形状上应该是继承关系的长方形,正方形就不在像直觉上一样成立了。

ISP(Interface Segregation Principle)接口隔离原则

来不及写了,以后补上

DIP(Dependency Injection Principle)依赖注入原则

DIP与IOC(Inversion Of Control,控制反转)因为实现类似且常配合使用,经常混为一谈。两者时常混用,比如老师也是在解释和举例DIP时,用到的是IOC。

DIP

1.高层模块不应该依赖底层模块,两者都应该依赖其抽象

2.抽象不应该依赖细节

3.细节应该依赖抽象

IOC

反转OOP中类之间的控制,以达到松耦合的目的。控制是指类的额外职责,也就是非主要职责。SRP要求一个类只有一个职责,但是现实中很难做到,当不能完全做到SRP时,应该运用IOC,把次要职责分离并交出(反转)控制权。比如一个类可能既要初始化数据库连接,又要访问数据库,当访问数据库是主要目的时,初始化连接就是前置条件,是次要职责。此时,应该把初始化连接的职责分离到另一个类,然后调用类方法获取连接。这就是IOC,把对连接初始化的控制交给了另一个类,本类只是依赖它,自己不去控制初始化连接的职责。通常不会直接依赖一个具体类,而是增加一个接口。类依赖连接工厂接口,然后实现多种数据源连接工厂,并使用DIP在运行时注入一个具体实现到数据访问类。数据访问类专注于数据访问,不再控制连接的初始化,交由工厂控制。这就是IOC和DIP的配合使用。

老师使用了更极端的例子,就是前后端的协作。前后端订立接口时,谁定接口,谁使用接口,并委派接口的实现(某事的控制权)给提供者,这个过程就是控制反转(依赖反转)。使用者把某事的控制权让渡给了接口实现者,是实现者控制了某事(指令编码),而使用者不直接控制,只是向实现者请求结果。

作业

作业一

详见DIP和IOC部分。

好莱坞原则是我有事会去找你,你不要来找我。这个原则象形的描述了依赖倒置(控制反转)原则。请求方依赖服务提供方,向服务提供方发起请求,服务提供方自主完成,提供结果。而不是请求方亲自完成任务,得到结果。

作业二

SpringMVC

SpringMVC定义了MVC三个组件的关系,Dispatcher接受请求,查询路由,分发请求给Controller,Controller把请求参数通过方法注入给一个实现,然后在实现中要调用Service并返回ModelView,ViewResolver根据ModelView渲染HTTP Response。结合Spring,我们实现Controller,并将实现通过Spring注入到MVC框架中。这个过程,既在构造对象时用到了IOC,MVC框架又通过DIP使用到具体的Controller实现。

作业三

发布于: 2020 年 06 月 18 日 阅读数: 20
用户头像

林毋梦

关注

还未添加个人签名 2018.08.25 加入

还未添加个人简介

评论

发布
暂无评论
SOLID设计原则(第二周+作业)