单一职责原则:
一个类只负责一个功能领域中的相应职责(对于一个类而说,只有一个引起它变化的原因)。
单一职责原则从职责(改变理由)的侧面上为我们对类(接口)的抽象的颗粒度建立了判断基准:在为系统设计类(接口)的时候应该保证它们的单一职责性。
降低了类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险
实现高内聚,低耦合的指导方针。
高内聚,低耦合
从类角度来看, 高内聚低耦合:减少类内部,对其他类的调用;从功能块来看 高内聚低耦合:减少模块之间的交互复杂度(接口数量,参数数据)。
在实现低耦合的过程中,无意间已经实现了高内聚。
实现高内聚,低耦合的办法:
1.少使用类的继承,多用接口隐藏实现的细节。
2.少使用全局变量。
3.类属性和方法的声明少用public,多用private关键字。
4.块的功能化分尽可能的单一
5.模块只对外暴露最小限度的接口,形成最低的依赖关系。
6.只要对外接口不变,模块内部的修改,就不得影响其他模块
开闭原则:
一个软件实体应当对扩展开放,对修改关闭(就是软件实体应该尽量在不修改原有代码的情况下进行扩展);软件实体可以指一个软件模块,一个由多个类组成的局部结构或一个独立的类。
作用:
- 稳定性。开闭原则要求扩展功能不修改原来的代码,这可以让软件系统在变化中保持稳定。
- 扩展性。开闭原则要求对扩展开放,通过扩展提供新的或改变原有的功能,让软件系统具有灵活的可扩展性。
遵循开闭原则的系统设计,可以让软件系统可复用,并且易于维护。
即:提高系统的可复用性和可维护性。
实现方法:对软件系统中的不变的部分加以抽象成不变的接口。可以通过抽象类、纯虚函数、继承的方法。
好处:提高系统的可复用性和可维护性。
举例:
1.从简单工厂模式到工厂模式,新增一个产品只需派生一个工厂类,一个产品类。无需修改原有的代码。属于开闭原则。
2.一个类关联另外一个基类(其一般作为虚基类),调用基类的虚函数(一般是纯虚函数)。让子类的实现具体个性化的功能。
里氏代换原则:
所有引用基类(父类)的地方必须能透明地使用其子类的对象。在软件中将一个基类对象替换成它的子类对象,程序不产生任何异常和错误,反之不成立。(实现开闭原则的重要方式之一)
里式替换原则的引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能。
具体:
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的方法时(重载/重写或实现抽象方法)的后置条件(即方法的输出/返回值)要比父类更严格或相等
好处:
•约束继承泛滥,是开闭原则的一种体现。
•加强程序的健壮性,同时变更时也可以做到非常好地提高程序的维护性、扩展性。降低需求变更时引入的风险。
延伸:
在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承。
如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这只是一个一般性的指导原则,使用的时候还要具体情况具体分析
依赖倒置原则(Dependency Inversion Principle ,DIP)
A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
B. 抽象不应该依赖于细节,细节应该依赖于抽象
C.针对接口编程,不要针对实现编程。
依赖:在程序设计中,如果一个模块a使用/调用了另一个模块b,我们称模块a依赖模块b。
高层模块与低层模块:往往在一个应用程序中,我们有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外有一些高层次的类,这些类封装了某些复杂的逻辑,并且依赖于低层次的类,这些类我们称之为高层模块。
依赖倒置(Dependency Inversion):
面向对象程序设计相对于面向过程(结构化)程序设计而言,依赖关系被倒置了。因为传统的结构化程序设计中,高层模块总是依赖于低层模块。
解决办法:在高层模块与低层模块之间,引入一个抽象接口层。
High Level Classes(高层模块) --> Abstraction Layer(抽象接口层) --> Low Level Classes(低层模块)
抽象接口是对低层模块的抽象,低层模块继承或实现该抽象接口。
这样,高层模块不直接依赖低层模块,而是依赖抽象接口层。抽象接口也不依赖低层模块的实现细节,而是低层模块依赖(继承或实现)抽象接口。
类与类之间都通过抽象接口层来建立关系。
使用方法:
1. 依赖于抽象
- 任何变量都不应该持有一个指向具体类的指针或引用。
- 任何类都不应该从具体类派生。
2. 设计接口而非设计实现
-
使用继承避免对类的直接绑定
-
抽象类/接口: 倾向于较少的变化;抽象是关键点,它易于修改和扩展;不要强制修改那些抽象接口/类
3. 避免传递依赖
- 避免高层依赖于低层
- 使用继承和抽象类来有效地消除传递依赖
说白了还是使用继承、虚函数多态重写、纯虚函数接口的抽象类的方法。和开闭原则差不多的思想。
举例:
原有的场景:
原本一个高层类含有具体对象类作为函数参数的函数。如果具体对象类变成新的具体对象类,那么高层类也要改造,违背了开闭原则。
改造:
在高层的函数中,把抽象基类作为参数。该函数调用基类的抽象接口。
从抽象基类派生不同的子类,实现各自功能。
在调用的时候,再确定用哪个子类,并把子类对象作为高层函数的参数。
好处:
可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险。更加满足开闭原则。
接口分隔原则(Interface Segregation Principle ,ISP)
不能强迫用户去依赖那些他们不使用的接口。
如果用户被迫依赖他们不使用的接口,当接口发生改变时,他们也不得不跟着改变。换而言之,一个用户依赖了未使用但被其他用户使用的接口,当其他用户修改该接口时,依赖该接口的所有用户都将受到影响。这显然违反了开闭原则,也不是我们所期望的。
1. 一个类对一个类的依赖应该建立在最小的接口上
2. 建立单一接口,不要建立庞大臃肿的接口
3. 尽量细化接口,接口中的方法尽量少
使用办法:
1.通过多重继承实现
各个不同的类接口实现各自的功能,而通过多重继承的类实现不同基类的功能集合。这样可以让不同类的对象实现不同的功能。
2.通过关联实现
好处:
• 接口分隔原则从对接口的使用上为我们对接口抽象的颗粒度建立了判断基准:在为系统设计接口的时候,使用多个专门的接口代替单一的胖接口。
• 符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性。
• 注意适度原则,接口分隔要适度,避免产生大量的细小接口。
单一职责原则和接口分隔原则的区别
单一职责强调的是接口、类、方法的职责是单一的,强调职责,方法可以多,针对程序中实现的细节;
接口分隔原则主要是约束接口,针对抽象、整体框架。
组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)
尽量使用组合/聚合,不要使用类继承。
即在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的。
组合和聚合都是关联的特殊种类。
聚合表示整体和部分的关系,表示“拥有”。组合则是一种更强的“拥有”,部分和整体的生命周期一样。
组合的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个组合关系的成分对象是不能与另一个组合关系共享的。
组合是值的聚合(Aggregation by Value),而一般说的聚合是引用的聚合(Aggregation by Reference)。
在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。
什么时候才应该使用继承
如果语义上存在着明确的"Is-A"关系,并且这种关系是稳定的、不变的,则考虑使用继承;如果没有"Is-A"关系,或者这种关系是可变的,使用组合。另外一个就是只有两个类满足里氏替换原则的时候,才可能是"Is-A" 关系。也就是说,如果两个类是"Has-A"关系,但是设计成了继承,那么肯定违反里氏替换原则。
错误的使用继承而不是组合/聚合的一个常见原因是错误的把"Has-A"当成了"Is-A" 。"Is-A"代表一个类是另外一个类的一种;"Has-A"代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。
通过组合/聚合复用的优缺点
优点:
1.新对象存取子对象的唯一方法是通过子对象的接口。
2.这种复用是黑箱复用,因为子对象的内部细节是新对象所看不见的。
3.这种复用更好地支持封装性。
4.这种复用实现上的相互依赖性比较小。
5.每一个新的类可以将焦点集中在一个任务上。
6.这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象。
7.作为复用手段可以应用到几乎任何环境中去。
缺点: 就是系统中会有较多的对象需要管理
通过继承来进行复用的优缺点
优点:
新的实现较为容易,因为基类的大部分功能可以通过继承的关系自动进入派生类。
修改和扩展继承而来的实现较为容易。
缺点:
继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,又称“白箱”复用。
如果基类发生改变,那么派生类的实现也不得不发生改变。
从基类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性
迪米特原则(最少知道原则)(Law of Demeter ,LoD)
迪米特原则(Law of Demeter)又叫最少知道原则(Least Knowledge Principle),可以简单说成:talk only to your immediate friends,只与你直接的朋友们通信,不要跟“陌生人”说话
好处和缺点
迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
迪米特原则不希望类直接建立直接的接触。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特原则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度。
参考: