设计原则
- 研究 23 种设计模式是困难的,甚至是没必要的
- 六大设计原则
- 零、单一职责原则
- 开闭原则
- 里氏代换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特法则
- 合成复用原则
研究 23 种设计模式是困难的,甚至是没必要的
设计模式有23种,我认为对普通人来说想要灵活掌握这23种设计模式是十分困难的。
- 设计模式的书中,介绍每一种设计模式,往往都举一个形象有趣的例子,然后读者会被有趣的例子和实现所吸引,而其背后蕴含的道理往往很难通过简单的思考来想清楚。
- 意识到上一点的人,通过简单思考后,觉得23种设计模式就像23种模板。23 这个数字已经足够大,大到背会这23个模板都是一个比较大的工程量。于是,学习者会转向记忆这23中模板上,并且最后以记不住这些模板告终。
首先,记忆模板很重要。学习的本质就是:记忆 + 思维。但是记忆哪些东西才是最关键的。记住那些关键假设,推理出进一步的结论。如果再次迭代,记住下一步结论,就能推理出下一步的结论。学习就是这样循环往复。
因此,只记忆模板,不求甚解是学习的大忌。
设计模式难学的第二个难点是思维容易发散,没有统一的思维切入点。光靠 23 种模板,很难让人明白,这种设计比那种设计好在哪里?应该怎样向好的方向去设计软件?23 种设计模式,反而使大脑更发散了。
设计模式底层的底层原理其实是六大设计原则。我认为仔细揣摩这六大原则,使用六大原则来分析23种设计模式的利弊,才是学习设计模式的正途。
软件设计是一个哲学问题。更注重设计的利弊思辨,而不是一个固定的答案。而大多数人包括我缺乏这样的思辨能力。六大设计模式,可以辅助我们分析设计的好坏。
六大设计原则,是针对面向对象的设计模式的原则,也就是针对 封装、继承和多态 的使用原则。
- 零、单一职责原则:这是所有模块设计的原则,确保功能模块化,与面向对象无关。
- 一、开闭原则:对拓展(继承)开放,对修改关闭
- 二、里氏替换原则:子类不改变父类原有职责
- 三、依赖倒置原则 :依赖抽象接口,不依赖具体实现
- 四、接口隔离原则:拆分大接口
- 五、迪米特法则 :对别的模块知道的越少越好,最好仅知道少量的接口。
- 六、合成复用原则:多用组合,少用继承
这六大原则也不是必须遵守的。因为这些原则都是定性的描述,多用xx,少用 xx等等。我们遵循这些模式,需要遵循到什么程度是很难判断的?毕竟现实中都是实在的量,接口有几个参数,我继承了几个类,等等。衡
因此我们要做得是分析利弊:
- 如果违背了某一原则,我们要付出什么样的代价
- 如果遵循了某一原则,我们得到了什么要的好处
软件设计本身就是一场不同设计目标之间的权衡。抽象程度高,则代码更灵活,但是可读性会变差。高性能,则需要更多缓存。所以我要说的是,如果违背原则的代价可以承受,那就可以违背原则。
事实上,由于软件设计是一个哲学问题,有另外一本书叫 《软件设计的哲学》写的也很不错,其中系统阐述了什么是软件的复杂性。里面的一些观点,貌似还要和这里相反。所以说,哲学是讲辩证法的。
六大设计原则
零、单一职责原则
一个类只能有一个引起它变化的原因。这个原因就是这个类的职责。
但说到底,职责的划分也是设计,把两件事当做一个职责也不是不可能。
如果违背了这一原则,一个类存在两个以上的职责,有以下两点代价
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当用户仅需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
所以,如果几个职责的实现不会有太大变化,或者这些职责只在一个地方用的时候,违背这个原则也无碍
开闭原则
对扩展开放,对修改关闭
拓展就是继承。通过继承抽象接口来实现新的功能,而不是修改核心功能代码。
里氏代换原则
任何基类出现的地方,子类一定可以出现。子类可以拓展,但不能改变父类原有的功能。
- 子类不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 确保兼容父类的用法:
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
依赖倒置原则
依赖抽象,不要依赖实体。加入 A 模块依赖 B 模块,可以选择不要直接依赖 B 模块,而让 A 依赖一个接口IB,然后使用 B 实现一个 IB。从图上看,对 B 的依赖发生了反转
接口隔离原则
将庞大的接口拆分成小接口,放在不同的模块中去实现。
迪米特法则
最少知道原则。每个模块对于其他单元只能拥有有限的知识。尤其是要依赖接口,而不是依赖具体实现。
合成复用原则
多用合成聚合,少用继承。继承是一个很强的依赖。体现在
- 对基类的修改会影响所有子类
- 子类必须实现基类所有的抽象接口
- 子类被强制拥有了基类的所有内容。因此:
- 实现子类还需要理解所有基类的其他已实现的接口。否则,你的继承就不成立,因为你不全面了解你的父亲,你就解释不了为什么要继承。