一、设计模式原则
1. 开闭原则
- 开闭原则的意思是:对扩展开放,对修改关闭。开闭原则是编程中最基础、最重要的设计原则。
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而 不是通过修改 已有的代码来实现变化。
- C++实现扩展的方式
- 在类中组合基类指针
- 继承虚函数覆盖
2. 单一职责
- 一个类应该仅有一个引起它变化的原因;(一个类或者一个方法只负责一项职责,尽量做到类的只有一个行为原因引起变化)。
- 如类 A 负责两个不同职责:职责 1 ,职责 2 。如果当职责 1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为A1 , A2。
- 单一职责原则的根本在于控制类的粒度大小 。
3. 里氏替换
- 里氏代换原则的根本,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常。(只要父类能出现的地方,其子类就可以出现,而且将父类替换为子类也不会产生任何错误或异常)。
- 子类型必须能够替换掉它的父类型;主要出现在子类覆盖父类实现,原来使用父类型的程序可能出现错误,因为子类覆盖了父类方法却没有实现父类方法的职责(子类覆盖了父类的方法并且改变了父类方法的原有功能逻辑);
- 比如,父类原来传递来两个参数进行加法运算,子类覆盖后,进行减法运算,改变了父类方法的职责,导致子类替换父类时,出现错误。
- 如何满足里氏替换原则
- 需要注意应该尽可能的将父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,这样可以做到满足开闭原则;
- 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法,也就是父类定义,子类实现。
- 子类不应该破坏父类的契约,也就是不能更改原有的方法的逻辑含义。
- 里氏替换是继承复用的基石,只有当子类可以替换父类,且软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在基类的基础上增加新的行为。
- 里氏代换原则是对开闭原则的补充
- 实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
4. 接口隔离
- 不应该强迫客户依赖于它们不用的方法 (通过protected/private权限修饰符,控制其可见性,不允许客户使用其不能或者不希望使用的接口);
- 类之间依赖关系应该建立在最小的接口上;
- 一般用于处理一个类拥有比较多的接口,而这些接口涉及到很多职责;简单理解:复杂的接口,根据业务拆分成多个简单接口;
- 接口的设计粒度越小,系统越灵活,但是灵活的同时结构复杂性提高,开发难度也会变大,维护性降低
5. 依赖倒置
- 高层模块不应该依赖低层模块,两者都应该依赖抽象接口;
- 抽象接口不应该依赖具体实现,具体实现应该依赖于抽象接口;
- 自动驾驶系统公司是高层,汽车生产厂商为低层,它们不应该互相依赖,不然一方变动另一方也会跟着变动;而是应该抽象一个自动驾驶行业标准,高层和低层都依赖它;这样以来就解耦了两方的变动;
- 自动驾驶系统、汽车生产厂商都是具体实现,它们应该都依赖自动驾驶行业标准(抽象);
- 依赖倒转原则的注意事项和细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好 .
- 变量的声明类型尽量是抽象类或接口 , 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
- 继承时遵循里氏替换原则
6. 迪米特原则
- 又称最少知道原则,尽量降低类与类之间的耦合;一个对象应该对其他对象有最少的了解。
- 通俗地讲,对于被依赖类而言,无论实现逻辑多复杂,都尽量地将逻辑封装在内部,对外除了提供公有方法,不对外泄露任何信息。
- 迪米特法则还有另外一种解释:只和自己直接的朋友交流。首先,看一下什么是朋友和直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
class Memcached;
class Demo
{
public function demo()
{
/* Memcached 以局部变量的形式出现在 Demo 类中,
* 所以它不是 Demo 类的直接朋友,所以违背了迪米特原则。
*/
...
Memcached* cache = new Memcached();
cache->write('test', 'test123');
cache->read('test');
cache->delete('test');
...
}
}
Demo demo = new Demo();
demo->demo();
- 遵循迪米特法则可以从以下几个设计思路入手:
- 不要将依赖类以局部变量的形式在类中使用
- 依赖类尽可能少地公布公有方法
- 如果一个方法放在本类中,既不增加类间关系,也不对本类产生负面影响,那就放置在本类中
7. 合成复用原则
- 尽量使用合成/聚合的方式,而不是使用继承(组合优于继承,继承耦合度高,组合耦合度低);
- 复用一个类有两种常用形式,继承和组合。尽量使用对象组合,而不是继承来达到复用的目的,因为继承子类可以覆盖父类的方法,将细节暴露给子类,而且会建立强耦合关系,是一种静态关系,不能在运行时更改等等弊端。
二、设计模式原则总结
- 开闭原则是其他设计原则的精神领袖,其他设计原则是对开闭原则在不同角度的解释和具体实施方式。
- 各项设计原则并不是一个完全独立的个体,各设计原则相互之间存在是一定的关联关系。例如:
- 比如单一职责原则与接口隔离原则,本质都是要职责专一类提供单一的功能的实现,接口不要有大而全的功能,约定职责专一就能降低耦合,就更有可能被复用;
- 使用组合而不是继承,可以避免子类对父类的修改,这种情况也就符合了里氏替换原则,也就符合了开闭原则;
- 设计原则是软件开发过程中,前人以“高内聚,低耦合”、“提高复用性”、“提高可维护性”为根本目标;在实践中总结出来的经验,进而演化出来的具体的行为准则 ;设计模式则是符合设计规则的具体的类/接口的设计解决方案,也就是设计原则的具体化形式 。
- 一个设计良好的程序应该遵循的是设计原则,而并非一定是某个设计模式。
- 所有的原则都是指导方针,而不是硬性规则,是在很多场景下一种优秀的解决方案,而并不是一成不变的。在实际的项目中,你既不能完全放弃使用继承,也可能让一个类完全不同“陌生人”讲话,也不可能子类完全不重写父类的方法。面向抽象进行编程,你也不可能让项目中所有的类都有抽象对应,这也是不可能的,也不能是被允许的。设计模式设计原则是经验之谈,当然是非常宝贵的经验,也是经过实践检验过的 ,但是最大的忌讳就是生搬硬套,矫枉过正,那将是最失败的设计模式的应用方式 。
三、如何学习设计模式
- 找稳定点和变化点,把变化点隔离出来;
- 先满足设计原则,慢慢迭代出设计模式。
- 培养计算机编程里两个最重要的思维:抽象思维、分治思维;其中,分治思维是指,将复杂的问题,通过分模块等形式化繁为简,分别处理。
四、类之间的关系
根据类与类之间的耦合度从弱到强排列,UML中的类图有以下几种关系:依赖关系,关联关系,聚合关系,组合关系,泛化关系和实现关系。其中,泛化和实现关系耦合度相等,是最强的。
1. 依赖
- 与关联关系不同,依赖关系是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化,依赖关系也有可能发生变化。
- 依赖关系用一条带箭头的虚线表示。依赖关系也有方向,双向依赖是一种非常糟糕的结构,我们应该总是保持单向依赖,杜绝双向依赖的产生。
- 在最终代码中,依赖关系体现为类构造方法及类方法的传入参数,箭头的指向为调用关系;依赖关系除了临时知道对方外,还“使用”对方的方法和属性;
class MobilePhone
{
public:
void transfer();
}
class Person
{
public:
/*依赖关系体现为类构造方法及类方法的传入参数*/
Person(MobilePhone mp);
void fun()
{
MobilePhone* mp = new MobilePhone;
}
}
2. 关联
- 关联关系描述不同类的对象之间的结构关系;它是一种静态的关系,通常与运行状态无关,一般由常识等因素决定;它一般用来定义对象之间静态的、天然的结构;所以关联关系是一种"强关联"的关系。
- 关联关系用一条实线表示。关联关系默认不强调方向,表示对象之间相互知道;如果特别强调方向,则表示只有其中一方知道。
- 在最终代码中,关联对象通常是以成员变量的形式实现的;
// 单向关联
class Teacher
{
public:
void teach();
}
class Student
{
public:
void study();
private:
/*关联对象通常是以成员变量的形式实现的*/
Teacher m_teacher;
}
// 双向关联
class Teacher
{
public:
void teach();
private:
/*关联对象通常是以成员变量的形式实现的*/
list<Student> stus;
}
class Student
{
public:
void study();
private:
/*关联对象通常是以成员变量的形式实现的*/
list<Teacher> teas;
}
3. 聚合
- 聚合关系用于表示实体对象之间的关系,表示整体由部分构成的语义(has-a)。与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分仍然存在。例如:一个部门由多个员工组成,部门撤销了,员工不会消失,他们仍然存在。
- 聚合关系用一条带空心菱形箭头的实线表示,菱形指向整体。
- 代码中,与关联关系一样,聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而单独存在。比如学校和老师的关系,学校可以包含老师,但是如果学校停办了,老师依然存在。
4. 组合
- 组合关系同样表示整体由部分组成的语义(contains-a)。但组合关系是一种强依赖的特殊聚合关系,如果整体不存在了,则部分也不存在了。例如:公司由多个部门组成,如果公司不存在了,则部门也不存在了。
- 组合关系用一条带实心菱形箭头的实线表示,菱形指向整体。
5. 泛化
- 泛化关系表现为继承非抽象类
- 泛化关系是对象之间耦合度最高的一种关系,表示一般与特殊的关系,是父类与子类之间关系,是一种继承关系,是is-a的关系
- 泛化关系用一条带空心三角形箭头的实线表示,箭头指向父类;
- 在代码实现时,使用面向对象的继承非抽象类机制来实现泛化关系。
class Person
{
public:
void speak();
private:
string name;
int age;
}
/*使用面向对象的继承非抽象类机制来实现泛化关系*/
class Student : public Person
{
public:
void study();
private:
long studentNo;
}
/*使用面向对象的继承非抽象类机制来实现泛化关系*/
class Teacher : public Person
{
public:
void teaching()
private:
long teacherNo;
}
6. 实现
- 实现关系表现为继承抽象类;
- 实现关系用一条带空心三角形箭头的虚线表示,箭头从实现类指向接口;