前言
设计原则
单一职责原则(Single Responsibility Principle)
定义
不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
问题由来
类 T 负责两个不同的职责:职责 P1,职责 P2。
当由于职责 P1 需求发生改变而需 要修改类 T 时,有可能会导致原本运行正常的职责 P2 功能发生故障。
解决方案
将类 T 分成两个不同的类来实现。比如 C 语言中会将头文件分类 string.h stdio.h, Qt 中 QLable QButton 等。C++中会有 string fstream 类,就是单一原则的体现。
开闭原则(Open Closed Principle)
定义
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
问题由来
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改 时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需 要原有代码经过重新测试。
解决方案
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已 有的代码来实现变化。
例如:
man 可以做doAction,
不考虑开闭,就是man添加 jump,run,read方法。直接使用
但是这样每次都会修改 影响代码的维护。
class Man{ public: void jump(); void read(); void run(); } void main(){ Man *aman = new Man; man.jump(); man.read(); man.run(); }
遵循开闭原则的话,man就通过调用Ido的do()来实现动作 也不需要每个man把三个对象都调用一遍
class Ido{ public: virtual void do() = 0; } class Read :public Ido{ void do(){ //read(); } } class Run:public Ido{ void do(){ //run(); } } class Jump:public Ido{ void do(){ //jump(); } } class Man{ public: void doAction(Ido *doptr){ doptr.do()} } void main(){ Man *aman = new Man; Ido* it= new Read(); man.doAction(it); Ido* it2= new Run(); man.doAction(it2); Ido* it3= new Jump(); man.doAction(it3); }
依赖倒置原则(Dependence Inversion Principle)
定义
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细 节应该依赖抽象。
问题由来
类 A 直接依赖类 B,假如要将类 A 改为依赖类 C,则必须通过修改类 A 的代码来达成。这种场景下,类 A 一般是高层模块,负责复杂的业务逻辑;类 B 和类 C 是低层模 块,负责基本的原子操作;假如修改类 A,会给程序带来不必要的风险。
解决办法
接口分离原则(Interface Segregation Principle)
定义
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接 口上。
问题由来
类 A 通过接口 I 依赖类 B,类 C 通过接口 I 依赖类 D,如果接口 I 对于类 A 和类 B 来说不是最小接口,则类 B 和类 D 必须去实现他们不需要的方法。
解决办法
将臃肿的接口 I 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依 赖关系。也就是采用接口隔离原则。
迪米特法则(Law of Demeter)
定义
面向对象有一个概念是迪米特法则,其规则如下:
每个对象对其他对象的认识必须限制,只能与自己最近的对象
每个对象应当只和它的朋友联系,而不是陌生人
每个对象应当只和他的直接朋友联系。
问题由来
在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
解决方案
迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。
设计模式的门面模式(Facade)和中介模式(Mediator),都是迪米特法则应用的例子。
里氏替换原则(Liskov Substitution Principle)
定义
如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那 么类型 T2 是类型 T1 的子类型。 所有引用基类的地方必须能透明地使用其子类的对象
问题由来
当使用继承时,遵循里氏替换原则。类 B 继承类 A 时,除添加新的方法完成新增 功能 P2 外,尽量不要 shadow(遮盖)父类 A 的方法。 继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊 端。
比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性, 如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且 父类修改后,所有涉及到子类的功能都有可能会产生故障。
优点:
1. 提高代码的重用性,子类拥有父类的方法和属性;
2. 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;
缺点:侵入性、不够灵活、高耦合
1. 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性;
2. 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。
解决方案
里氏替换原则通俗的来讲就是: 子类可以扩展父类的功能,但不能改变父类原有的 功能。 它包含以下 4层含义:
1.子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
2.子类中可以增加自己特有的方法。
3.当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
用我自己的话讲就是:
子类可以完全替换父类,而不影响程序编译;尽量不要重写父类。
UML图
设计视图
类的关系
类图
泛化
定义 是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征 和行为。
箭头指向 带三角箭头的实线,箭头指向父类。
类图关系:
实现
定义 是一种类与接口的关系,表示类是接口所有特征和行为的实现
箭头指向 带三角箭头的虚线,箭头指向接口。
类图关系