面向对象设计原则 - 简述
一、单一职责原则(SRP)
定义:
就一个类而言,应该仅有一个引起它变化的原因。
说明:
- 职责被刻画为引起变化的原因;若有多个动机来引起类的职责变化,则说明该类承担了过多的职责,需要分离职责,也即分离为负责不同职责的类,依此实现独立的变化。
- 类职责过多的类也意味着耦合性较高,这种耦合性也会导致脆弱的设计。
- 类职责过多,职责间可能存在依赖,也可能是完全独立的。
- 某些类可能有多个职责,但是各职责可能不怎么变化,则可根据程序设计需要考虑是否需要分离,也即仅当实际发生职责变化了职责分离操作才有意义,并不是说一定完全支持实现分离职责,因为分离过多的类也会导致不必要的类膨胀或者称之为复杂度坏味;
- 另外还有一个说法,若类中的多个职责总是同时变化,则可不必分离,这个还是看具体的程序需要(即可能存在依赖关系的职责)。
如何识别并分离职责:
一般类所谓的职责,指的是完成某项工作或认为所提供的功能,其可由一系列功能函数支持实现;分离职责即可按照分离功能实现的策略进行。
小结:
单一职责看似简单,实则比较复杂,其主要体现在识别职责、分离职责,以及确定何时才有必要执行职责分离。
二、开放-封闭原则(OCP)
定义:
软件实体(类、模块、函数等)应是可扩展的,但是不可修改的。
说明:
- 开放封闭原则的解决问题,主要是针对僵化代码的修改,避免因修改某处时引起其他更多的修改;而OCP的原则实现,则可满足修改某处代码时不会引起太多其他地方的修改甚至不会引起修改,增加新的代码即可支持新的功能实现。
- 开放封闭封闭原则,其要求可扩展,意味着可以改变模块或函数的功能,使其支持新的行为;另外对修改封闭,也意味着对模块行为扩展时不会影响原来的代码或设计实现。
- 实现开放封闭原则的核心:抽象和多态。
如何实现开放封闭:
模块应该依赖于抽象接口,而不是具体的类;这样可使得通用部分保持不变,而实现细节部分可以任意修改,也即是抽象接口来实现封闭,而具体实现细节可以保持扩展。
小结:
虽然开放封闭原则虽然也简单,但是从本质上来说几乎很难真正做到完全封闭;另外对于开放封闭,设计人员需要对其模块设计时需要确定哪些变化封闭,也即是需要确定可能发生变化的种类并构造抽象来隔离变化,这些需要设计人员的经验、常识等。
OCP的代价可能是需要花费一定的时间和精力来发现并创建抽象,另外抽象也会增加一定的软件复杂性,故应针对确定可能发生的变化或者发生变化时来执行OCP原则。
OCP作为面向对象设计的核心,带来灵活可扩展、可重用、可维护的好处,但建议应该在真正需要应对频繁变化的地方做出必要的抽象即可,避免复杂度上升。
三、里氏替换原则(LSP)
定义:
子类型必须能够替换它们的基类型。
说明:
- 里氏替换原则要求可用子类来替换基类对象,而不会出现非预期的结果的行为,也即是能够调用基类对象的地方,也可调用子类对象,并且功能、行为上保持一致。
- 除了功能、行为上保持一致外,子类不应该增加其他的新约束出来,如基类不会抛出异常,而子类抛出了异常、子类退化了基类的某些功能等。
如何实现里氏替换原则:
基于开放封闭原则,即抽象与多态,另外还有其他的约束规范,使得子类替换基类时行为上不会发生变化。
小结:
类似于OCP、SRP原则,LSP原则也应该仅对可预测的比较明显违背LSP原则的情况进行处理,而不是对所有的情况都按照LSP原则的要求,以避免不必要的复杂度上升。
对于违反了里氏替换原则的情况下,基本上也违背了OCP原则。
四、依赖倒置原则(DIP)
定义:
高层模块不应该依赖于底层模块,两者均应依赖于抽象;抽象不应该依赖于细节,细节应依赖于抽象。
说明:
- 依赖倒置原则在框架设计中使用最为广泛。
- 依赖倒置不仅仅是依赖关系的倒置,更是接口的所有权的倒置,即依赖的接口在高层模块中,由高层模块声明并被高层模块调用,底层模块需要继承实现该接口。
- 很多库或工具类基本上不会依赖于外面的抽象,即可能作为独立的个体实现,此时因为这些类基本上很稳定保持不变,故直接依赖这些类影响不会太大,另外若是不希望直接依赖这些类,可以增加一个适配器或包装类,以继续满足高层依赖于抽象的原则。
- C++的模板可以定义一个通用的方法,由模板参数来实现不同的实例,此时可不需要继承关系以及多态,也能实现依赖关系倒置;仅仅要求模板参数类提供该通用方法的内部需要的功能函数即可。这个时候通用方法仅依赖于相同接口的约定,而不用关心不同的模板实例类型和对象了。以模板的方式可称为静态多态,另外基于面向对象的便是动态多态的方式。
小结:
一般的说法是,若程序是按照依赖关系倒置的,则它就是面向对象的设计;否则可能就是面向过程的设计。
依赖倒置原则使得抽象与实现细节彼此隔离,且作为框架设计中的技术原则,更容易维护、扩展。
五、接口隔离原则(ISP)
定义:
不应该让用户依赖于他们不需要的方法或接口。
说明:
- 此原则主要针对胖接口而言,即应该将内聚的接口作为一组抽象基类,其他的接口隔离开来形成其他抽象基类。
- 若用户依赖了不需要的接口,则若有其他用户感兴趣的方法改变时则会影响到当前用户的使用,这也是一种耦合;此外胖接口导致实现类型中需要实现不必要的方法。
解决接口隔离的方法(或者称为避免出现胖接口直接依赖的方法):
- 因需要用到不同接口的功能方法,可采取使用适配器的方式(或称为委托)来支持多个接口的功能,或者隔离接口的功能,即调用者不用关心另外接口的提供者,只需要关系自己所需要的接口方法即可。
- 基于多重继承的方法,即调用者同样不用关心另外的接口,仅关心自己需要的接口方法即可,此时多重继承内部实现功能支持或转接等实现。
针对以上两种方式,可以认为均为适配器的设计模式实现,方法1为基于组合对象适配器,方法2为基于类继承的适配器。方法1会有额外的对象调用间接性增加了一定的内存和调用时间开销。方法1和方法2产生的胖类(多继承的或组合的类)不被外部所依赖。
小结:
针对胖接口,可以直接根据客户分组分类,剥离为多个接口;或者针对客户需要,用新的接口来包裹调用胖接口,避免客户直接与胖接口打交道。这样胖接口的改变不会影响到客户使用,同样客户依赖的接口改变也可以与胖接口独立变化不会产生直接影响。
接口隔离原则要求,设计者尽量不要出现胖接口,另外对于已有的胖接口,也不要直接与胖接口产生直接依赖。
六、迪米特法则(最少知道原则)(LoD)
定义:
对象之间应尽可能少的了解对象。
说明:
- 迪米特法则为了尽量减少类之间的依赖,使得模块功能独立或者较少的依赖。
- 门面模式、中介者模式便是迪米特法则的一个应用。
- 迪米特法则说明了,类应该依赖于必要的方法,同样类也应该仅提供必要的方法。
小结:
迪米特法则导致类间的交互通信不够直接,可能导致较多的中介者(第三者)来转发请求以及会有不同类对象间的通信、交互、效率等问题。
迪米特法则可最大限度的减低类之间的耦合,有利于信息隐藏和复用。
七、组合复用原则(CRP)
优先使用组合而不是继承。