本文是学习极客时间-设计模式之美的学习笔记。 参考资料:极客时间-设计模式之美
目录
一、SOLID原则
SOLID由五个设计原则组成:SRP(单一设计原则),OCP(开闭原则),LSP(里式替换原则),ISP(接口独立原则),DIP(依赖反转原则)。
1.SRP(单一设计原则)
定义:一个类或者模块只负责完成一个职责。这里的模块可以看做比类更加粗粒度的代码块,一个模块可以包含多个类。
理解:
(1) 设计的类要避免大而全,而应该粒度小、功能单一。
(2) 实现高内聚,松耦合。
(3) 如果一个类里包含了多个业务不相干的功能那么就是职责不单一的,需要拆分。
(4) 并不是拆分的越细越好,要把握尺度。过度的拆分反而会降低内聚性,增加维护难度。
需要考虑拆分的场景:
(1) 类中的代码行数、函数或属性过多。
(2) 类依赖的其他类过多,或者依赖类的其他类过多。
(3) 私有方法过多,需要考虑能否将私有方法独立到新的类中并且设置为公开方法,供其他类使用。
(4) 难以给一个类用一个业务名词概括。
(5) 类中大量的方法只集中在部分几个属性上。
总结:单一职责原则归根结底就是为了提高内聚降低耦合,通过拆分大而全的类为职责单一的类,既可以明确各个类的职责,也可以降低代码修改维护的难度。但是过度的拆分反而会降低内聚性,本来只需要维护一个完整功能的类被拆分成许多小的功能类,增加了新的工作量以及有修改功能时遗忘某些小的类的导致出现bug的风险。
2.OCP(开闭原则)
定义:软件实体(模块、类、方法)对扩展开放、对修改关闭。
理解:
(1) 添加功能时应该是在已有代码基础上扩展(新增模块、类、方法等),而非修改已有代码。
(2) 只要没有破坏原有代码的正常运行和原有的单元测试就是一个满足开闭原则的合格改动。
(3) 虽然应用在不同的代码粒度下有不同的理解(在粗代码粒度认为是“修改”,在细代码粒度下认为是“扩展”),只要满足上述设计初衷就是合格的改动。
(4) 添加功能不可能不在原有的代码上做修改,所以只需要保证修改操作更集中,更上层,让最核心、最复杂的那部分逻辑代码符合开闭原则即可。
(5) 灵活使用开闭原则,避免过度设计。不可能在设计之初就能识别出所有的扩展点,并且未来需求是不确定的,因此只需要考虑短期内扩展或需求改动对代码结构影响较大的情况。
(6)开闭原则提高了可扩展性但是会提高代码的复杂度,降低了代码的可读性,需要根据实际业务需求进行平衡。
指导思想: 保持偏向顶层的指导思想,时刻具备扩展意识、抽象意识、封装意识。预留可扩展点,封装可变部分隔离变化,提供抽象化的接口供上层系统使用。
方法:多态,依赖注入,基于接口而非实现编程,大部分设计模式(如:装饰、策略、模板、职责链、状态等)
总结:开闭原则的设计目的是为了提高代码的可扩展性,减少新增功能带来的工作量,也可以防止修改以前代码导致出现bug。但它并不能完全杜绝修改,而是以最小的修改代价来完成新功能的开发。使用开闭原则时要避免过度设计,否则只会设计多余没有的扩展点增加了开发难度和工作量。开闭原则增加扩展性的代价就是降低了代码的可读性。
3.LSP(里式替换原则)
定义:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
理解:多态不等于里式替换,多态只满足可以替换接口,但是不能保证和原来父类执行结果一致。因为多态的子类可能会修改原来父类的方法的方法逻辑(多态本身的要求),所以不能保证替换在原本父类对象后不破坏原有程序的正确性。
常见反例:
(1) 子类违背父类声明要实现的功能。
(2) 子类违背父类对输入、输出、异常的规定。
(3) 子类违背父类注释中所罗列的任何特殊说明。
总结:里式替换的设计目的是指导继承关系中子类的设计的原则,核心就是按照协议设计,子类可以修改内部逻辑但不能破坏父类定义的协议(如函数声明要实现的功能;对输入、输出、异常的约定、注释中所罗列的任何特殊说明)。里式替换的目的就是为了子类继承父类的功能函数时只进行扩充而不修改原本需要实现的功能,确保不违背原本的设计思路,并且防止出现bug。
4.ISP(接口隔离原则)
定义:接口的调用者或使用者不应该强迫它去依赖不需要的接口。
理解:
(1) 接口可以指一组API接口、单个API接口或函数、OOP中的接口。
(2) 相对于单一原则,接口隔离原则更加侧重于接口的设计,从调用者的角度去考虑接口的职责是否单一。当调用者只使用了部分接口或者接口的部分功能,那接口的设计职责就不够单一。
总结:接口隔离原则是从调用方考虑的类似的单一原则。在设计接口时如果部分接口或者接口的部分功能被调用者使用,那么就可以考虑把这一部分隔离出来,而不强迫调用者用到其他不会使用的接口;针对函数可以拆分为更加细粒度的函数让调用者只用需要的函数;接口的设计要保证单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
5.DIP(依赖反转原则)
补充:
控制反转:是一种设计思想。“控制”指对程序执行流程的控制,“反转”指将流程的控制从程序员转到由框架控制。
依赖注入:是一种编码技巧。不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
因为依赖注入还是需要程序员在上层代码自己创建对象,而如果需要创建的对象过多在依赖注入时会变得很复杂,所以考虑将这块与业务逻辑无关的代码抽象为框架。
依赖注入框架:只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
定义:高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。
理解:高层模块指调用方,底层模块指被调用方。高层模块不依赖低层模块不是说它们之间不能有关系,而是通过中间抽象(如协议)建立关系。高层和底层的实现都依赖协议,而协议不依赖与高层或底层的实现逻辑。
总结:依赖反转类似于控制反转,主要是用来指导框架层面的设计。
二、KISS原则
定义:尽量保持简单
理解:
(1) 并不是代码行数越少越简单,还需要考虑逻辑复杂度、实现难度以及代码的可读性。
(2) 逻辑复杂度大的代码不一定不满足KISS原则。因为本身就复杂的问题本身就需要复杂的解决方法。
方法:
(1) 使用容易理解的技术实现代码,增加代码可读性。
(2) 善于使用已有的工具类,不要重复造轮子。
(3) 不要通过牺牲代码的可读性来过度优化。
总结:KISS原则的设计目的是为了保持代码的可读和可维护性,它的考量标准由逻辑复杂度、实现难度、代码可读性等,而且本身复杂的问题用复杂的方法解决并不未被KISS原则。
三、YAGNI原则
定义:不要过度设计,只保留需要的部分
理解:
(1) 不需要提前实现还不需要的代码,也不需要导入当前不需要的包
(2) 不考虑当前不需要的代码,但还是要预留好扩展点。
(3) 与KISS不同在于,KISS原则讲究的是如何做(保持简单),YAGNI原则说的是要不要做(当前不需要的就不做)。
总结:YAGNI原则的核心就是不要过度的设计,这样只会带来多余的开销。
四、DRY原则
定义:不要写重复的代码
理解:
(1) 重复是指实现逻辑重复、功能语义重复、代码执行重复。
(2) 实现逻辑重复重复但功能语义不重复的不算违背DRY原则,因为直接合并会违反单一原则。重复代码部分可以考虑单独提成细粒度的函数执行。
(3) 实现逻辑不重复的但功能语义重复的违背了DRY原则。因为不同地方调用这两个函数实现的功能是一样的,会对其他开发人员造成不一样的误解,而且修改也需要同时修改多处。
(4) 代码执行重复违反了DRY原则。
总结:DRY原则主要是为了减少代码量,提高代码的可读性、可维护性,复用已经经过测试的老代码,从而降低bug产生的概率。
五、LOD原则
补充:
(1) 高内聚:指相近的功能应该放到同一个类中。用于指导类本身的设计。
(2) 松耦合:类与类之间的依赖关系简单清晰,即使有依赖关系,一个类的代码改动很少导致依赖类的改动。指导类与类之间的依赖关系的设计。
两者之间相辅相成,高内聚有利于松耦合,松耦合有利于高内聚。
定义:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
理解:
(1) 本身没有直接依赖关系的就不要依赖,可以有助于减少代码修改工作量。
(2) 如果有依赖关系尽量只依赖必要的接口,同样可以有助于减少代码修改工作量。
总结:LOD原则的核心思想就是减少类之间的耦合度,实现高内聚,松耦合。让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。