程序设计原则
1. 面向对象的设计模式有七大基本原则
开闭原则(Open Closed Principle,OCP)
单一职责原则(Single Responsibility Principle, SRP)
里氏代换原则(Liskov Substitution Principle,LSP)
依赖倒置原则(Dependency Inversion Principle,DIP)
接口隔离原则(Interface Segregation Principle,ISP)
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
迪米特法则(Law of Demeter,LOD)或者 最少知识原则(Least Knowledge Principle,LKP)
2. 逐一理解
2.1. 开闭原则
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
这里讲的场景是软件出现新的功能需求,而不是原有功能出现问题。有Bug当然只能修改。更确切一点,是从需求角度就是对原有功能的扩展。比如原来的功能是画个圆形,现在要增加一个画三角形的功能。很容易产生一个抽象的概念——画图形,画三角形是对原来画圆形程序的扩展,此时就不应该在原有程序上修改,加一堆if...else...。修改有什么不好呢?首先增加了程序逻辑复杂度,降低了可读性,其次引入了不确定因素,有可能给原有功能带来不必要的问题。那有怎么实现扩展呢?那就必须在设计时采用合适的解耦方法。
2.2. 单一职责原则
指一个类或者模块应该有且只有一个改变的原因。也就是一个类只实现一个功能。
这个思想的一个典型案例就是Linux中的命令。它的设计思想就是一个命令只做一件事,只做好一件事。这样的好处是对于调用者来说非常容易选择和理解。这也是复用最方便最经济的方案。调用者可以很清楚自己在做什么,并且不需要理解一堆无关的功能。
2.3. 里氏代换原则
任何基类可以出现的地方,子类一定可以出现。反之则不成立。
这个原则是对子类行为的约束。子类在覆盖基类方法时,不仅仅是支持相同的入参和出参类型,而且要从行为逻辑上与基类保持一致。比如基类有保存和查询两个方法,逻辑是保存后立即可以查询到,也就是保存行为是实时的,那么子类也必须遵守这个逻辑,子类的保存方法也必须是实时的,否则虽然方法及其参数都一致,但仍然不能被复用。
2.4. 依赖倒置原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
不同层次的模块也应该解耦,这样能避免系统的僵化。现实中很多软件系统都是因为分层不清或者高层模块严重依赖低层模块,从而导致软件架构、技术栈无法与时俱进,最终被淘汰。
2.5. 接口隔离原则
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
本质上是要求对要实现的功能充分解析,拆分出不同的功能,不要一股脑的都放到一个接口中。否则这个接口就失去了抽象意义,变成了只服务于一个具体场景的“死”接口。
2.6. 合成/聚合复用原则
尽量使用合成/聚合的方式,而不是使用继承。
继承的问题在与可以改变基类的行为,当没有这个需求的时候,继承就显得没有必要,而且给后续扩展带来隐患。合成和聚合就避免了这个问题,因为它只是调用。合成和聚合有什么区别呢? 合成是部分与整体关系,两者不可分;聚合是拥有关系,两者可分。
2.7. 迪米特法则(最小知识原则)
一个对象应该对其他对象保持最少的了解。
这个原则是为了明确对象之间的依赖边界,排除无关功能,提高程序的可维护性。比如一个对象有3个方法,但当前对象只用它其中2个方法,那最好让第三个方法对当前对象不可见,解决办法就是增加一个中介,隐藏调不用的方法。
3. 心得总结
这几个原则其实围绕着一个核心目标:解耦。而解耦的目的是让程序容易维护,减少程序维护破坏原有功能的风险,进而让系统具备持续改进的能力。个人认为现实当中很难完全遵守这些原则。一方面是扩展可能性的权衡,解耦是有成本的,如果从业务角度分析,某方面未来需要扩展的可能性极小,那可能没有必要花这个成本。另一方面是能力问题,人的思想认识水平是发展的,可能最初设计时并没有足够的抽象能力,或者并没有认识到某些功能扩展的可能性,当发现这个问题时,可能要做的事情是“重构”。 既然这些原则很难完全遵守,那掌握它们的意义是什么呢?其实这些原则的作用是告诉我们正确的方向,当我们面临这些问题时它们是有力的理论支撑。