软件设计的7大原则
开闭原则、依赖倒置原则、单一职责原则、接口隔离原则、迪米特原则、里氏替换原则、合成复用原则。
开闭原则
- what:一个软件实体如类、模块和函数应该对扩展开发,对修改关闭。
why:提高软件系统的可复用性及可维护性
how: 版本更新尽量不更改源代码,但是可以添加新功能;
example: 弹性工作时间,每天工作8小时不变,早点来早点走,晚点来晚点走
面向抽象编程,继承、多态机制
左图表示我们有一个订单接口,现在要在其基础上扩展一个功能,能够实现打折的功能。如何才能实现打折功能呢?
- 在 IOrder 接口中添加一个 getDiscountPrice(),并在其实现类中实现这个方法。这样可行么?如果这样做就需要在所有实现这个方法的类中,添加 getDiscountPrice()。
- 修改 getPrice() 的实现,这样原价去哪里获取呢?也不是很好
- 我们可以写一个类
ShoeDiscoutOrder
继承ShoeOrder
然后 @Override getPrice() 来实现打折功能,而原价则使用 getOriginPrice(),并在其中使用super.getPrice()
调用父类方法。
这是开闭原则的简单应用:扩展是开启的,但是对接口和基类的修改是关闭的。
依赖倒置原则
- what: 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- why: 减少雷剑的耦合性,提高系统稳定性,提高代码可读性和可维护性,降低修改程序所造成的风险
how:抽象不应该依赖细节;细节应该依赖抽象。针对接口编程,不要针对实现编程。
example:对比扩展类中的方法编程,以及面向接口编程的区别。
现在有一个 course 类,类中有上各种课程的方法,我在应用层调用各种上课的方法,就需要在 course 中新增相应的方法。此时就造成了一种局面:高层模块依赖于低层模块。代码是这样的:
|
|
我们希望高层模块不依赖低层模块,并且让它们依赖其抽象。具体来说是什么意思呢?就是说我有一个接口实现了 stydyCouse()
这一个方法,具体什么课,怎么学都由该接口的实现类负责。我从高层传入对象,就可以调用相应的类。
|
|
依赖倒置的核心是面向接口编程。
单一职责原则
- what: 不要存在多余一个导致类变更的原则
- why: 一个类有多个职责会导致它变更,修改某一个职责可能导致其他职责出错
how: 一个类/接口/方法只负责一个职责
example:类、接口和方法级别的单一职责实现。
类级别
现在有一个 bird 类,有的鸟靠飞,有的鸟靠走。我们可能会用一个逻辑判断来实现。如果说遵循单一职责原则,就应该将其拆成两个类 FlyBird
和 WalkBird
,然后由应用层来判断是哪种鸟。
|
|
接口级别
比如说有个 ICourse 接口,接口中有若干方法,一些方法负责获取课程信息,一些方法负责管理可能。比如说,有个方法是退订这门课,那就不能获取课程信息了对不对。总之,这样一个接口现在负责了两种职责,我们就应该将其拆成两组接口。
|
|
方法级别
方法中做逻辑判断,负责多个任务,实际上是可以拆分开的。
|
|
接口隔离原则
- what: 用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
- why: 符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性
how:
- 一类对一个类的依赖应该建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
- 注意适度原则,一定要适度
example: 一个关于动物的接口承载太多的方法,将其拆分的例子
同样是动物但是这个动物能做的事儿,其他动物可能不能做。如果动物都实现同样的接口,那么有些方法的实现就要空着了。所以要拆分。
|
|
拆分前,拆分后如下所示:
接口隔离原则看着简单,但是把握好接口隔离的粒度还是需要仔细考量的。
迪米特原则
- what: 一个对象应该对其他对象保持最少的了解,又叫最少知道原则
- why: 降低类之间的耦合
- how: 尽量降低类和类之间的耦合
- example:
Boss 类只需要和 TeamLeader 打交道,而不需要和 course 打交道。
修改之后如下所示:
迪米特原则的关键就是梳理出这个类应该和哪些类打交道,不应该和哪些类打交道,做到尽可能合理。