弄浪的鱼

开闭原则、依赖倒置原则、单一职责原则、接口隔离原则、迪米特原则、里氏替换原则、合成复用原则。

开闭原则

  • what:一个软件实体如类、模块和函数应该对扩展开发对修改关闭
  • why:提高软件系统的可复用性及可维护性

  • how: 版本更新尽量不更改源代码,但是可以添加新功能;

  • example: 弹性工作时间,每天工作8小时不变,早点来早点走,晚点来晚点走

面向抽象编程,继承、多态机制

开闭原则

左图表示我们有一个订单接口,现在要在其基础上扩展一个功能,能够实现打折的功能。如何才能实现打折功能呢?

  1. 在 IOrder 接口中添加一个 getDiscountPrice(),并在其实现类中实现这个方法。这样可行么?如果这样做就需要在所有实现这个方法的类中,添加 getDiscountPrice()。
  2. 修改 getPrice() 的实现,这样原价去哪里获取呢?也不是很好
  3. 我们可以写一个类 ShoeDiscoutOrder 继承 ShoeOrder 然后 @Override getPrice() 来实现打折功能,而原价则使用 getOriginPrice(),并在其中使用 super.getPrice() 调用父类方法。

这是开闭原则的简单应用:扩展是开启的,但是对接口和基类的修改是关闭的。

依赖倒置原则

  • what: 高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • why: 减少雷剑的耦合性,提高系统稳定性,提高代码可读性和可维护性,降低修改程序所造成的风险
  • how:抽象不应该依赖细节;细节应该依赖抽象。针对接口编程,不要针对实现编程。

  • example:对比扩展类中的方法编程,以及面向接口编程的区别。

现在有一个 course 类,类中有上各种课程的方法,我在应用层调用各种上课的方法,就需要在 course 中新增相应的方法。此时就造成了一种局面:高层模块依赖于低层模块。代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 应用层(高层)调用低层的方法
public class Test {
public static void main(String[] args) {
Geely geely = new Geely();
geely.studyJavaCourse();
geely.studyFECourse();
}
}

// 低层想要扩展就要加方法
public class Geely {
public void studyJava(){
// 业务代码
}

public void studyPython(){
// 业务代码
}

// 更多课程都要添加新的方法
}

我们希望高层模块不依赖低层模块,并且让它们依赖其抽象。具体来说是什么意思呢?就是说我有一个接口实现了 stydyCouse() 这一个方法,具体什么课,怎么学都由该接口的实现类负责。我从高层传入对象,就可以调用相应的类。

依赖倒置原则实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 高层调用低层,只要注入具体的实现类
public class Test {
public static void main(String[] args) {
Geely geely = new Geely();
geely.setiCourse(new JavaCourse());
geely.studyImoocCourse();

geely.setiCourse(new FECourse());
geely.studyImoocCourse();

}
}

// 低层调用方法无需变动
public class Geely {
private ICourse iCourse;

public void setiCourse(ICourse iCourse) {
this.iCourse = iCourse;
}

public void studyImoocCourse(){
iCourse.studyCourse();
}
}

依赖倒置的核心是面向接口编程

单一职责原则

  • what: 不要存在多余一个导致类变更的原则
  • why: 一个类有多个职责会导致它变更,修改某一个职责可能导致其他职责出错
  • how: 一个类/接口/方法只负责一个职责

  • example:类、接口和方法级别的单一职责实现。

类级别

现在有一个 bird 类,有的鸟靠飞,有的鸟靠走。我们可能会用一个逻辑判断来实现。如果说遵循单一职责原则,就应该将其拆成两个类 FlyBirdWalkBird ,然后由应用层来判断是哪种鸟。

1
2
3
4
5
6
7
8
9
public class Bird {
public void mainMoveMode(String birdName){
if("鸵鸟".equals(birdName)){
System.out.println(birdName+"用脚走");
}else{
System.out.println(birdName+"用翅膀飞");
}
}
}

接口级别

比如说有个 ICourse 接口,接口中有若干方法,一些方法负责获取课程信息,一些方法负责管理可能。比如说,有个方法是退订这门课,那就不能获取课程信息了对不对。总之,这样一个接口现在负责了两种职责,我们就应该将其拆成两组接口。

1
2
3
4
5
6
7
8
9
public interface ICourse {
// 职责一:负责获取课程信息
String getCourseName();
byte[] getCourseVideo();

// 职责二:负责管理课程
void studyCourse();
void refundCourse();
}

单一职责原则的实现

方法级别

方法中做逻辑判断,负责多个任务,实际上是可以拆分开的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void updateUsername(String userName){
userName = "geely";
}
private void updateUserAddress(String address){
address = "beijing";
}

// 多个职责,条件判断,拆!
private void updateUserInfo(String userName,
String address,boolean bool){
if(bool){
//todo something1
}else{
//todo something2
}


userName = "geely";
address = "beijing";
}

接口隔离原则

  • what: 用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
  • why: 符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性
  • how:

    • 一类对一个类的依赖应该建立在最小的接口上
    • 建立单一接口,不要建立庞大臃肿的接口
    • 尽量细化接口,接口中的方法尽量少
    • 注意适度原则,一定要适度
  • example: 一个关于动物的接口承载太多的方法,将其拆分的例子

同样是动物但是这个动物能做的事儿,其他动物可能不能做。如果动物都实现同样的接口,那么有些方法的实现就要空着了。所以要拆分。

1
2
3
4
5
public interface IAnimalAction {
void eat();
void fly();
void swim();
}

拆分前,拆分后如下所示:

接口隔离原则演示

接口隔离原则看着简单,但是把握好接口隔离的粒度还是需要仔细考量的。

迪米特原则

  • what: 一个对象应该对其他对象保持最少的了解,又叫最少知道原则
  • why: 降低类之间的耦合
  • how: 尽量降低类和类之间的耦合
  • example:

Boss 类只需要和 TeamLeader 打交道,而不需要和 course 打交道。

耦合

修改之后如下所示:

修改之后

迪米特原则的关键就是梳理出这个类应该和哪些类打交道,不应该和哪些类打交道,做到尽可能合理。

里氏替换原则

合成复用原则