六、[C++]六大设计原则

6、[C++]六大设计原则

单一职责原则

单一职责原则: 就一个类而言, 应该仅有一个引起它变化的原因.

日常编程, 我们习惯性的很自然给一个类加这样那样的功能, 比如之前讲过的计算器, 很可能我们会设计出一个类, 把计算 显示等功能都写那个类中, 这样的代码维护很麻烦, 复用不可能, 也缺乏灵活性, 因此我们对那个类进行了重构才引出了简单工厂模式策略模式.

如果一个类承担的职业过多, 就等于把这些职责耦合在一起, 一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力. 这种耦合会导致脆弱的设计, 当变化发生时, 设计会遭受到意想不到的破坏.

软件设计真正要做的许多内容, 就是发现职责并把那此职责相互分离.

怎么判断是否应该分离?

那就是如果你能够想到多于一个的动机去改变一个类, 那么这个类就具有多于一个职责, 就应该考虑类的职责分离.


开始-封闭原则

开始-封闭原则: 软件实体(类 模块 函数等等), 应该是可以扩展但不可修改的.

也就是说对于扩展是开放的, 对于更改是封闭的, 再换句话说就是设计软件要容易维护又不容易出问题的最好办法是多扩展少修改.
要达到这样的效果就是我们在设计类的时候, 时刻要考虑尽量让这个类是足够好, 写好了就不要去修改了, 如果新需求来, 我们增加一些类就完事了, 原来的代码能不动则不动.

但是, 无论是多么的”封闭”, 都会存在一些无法对之封闭的变化, 既然不可能完全封闭, 设计人员就必须对于它设计的模块应该对哪种变化封闭做出选择, 他必须先猜测出最有可能发生的变化种类, 然后构造抽象来隔离那些变化.

再回想一下我们之前的计算器例子, 一开始的时候, 我们把计算的算法都封装在一个类当中:

 1 class Operation {
 2 public:
 3     static double GetResult(double dNum_A, double dNum_B, char cOp) {
 4         double dResult = 0.0;
 5         switch (cOp) {
 6         case '+':
 7             dResult = dNum_A + dNum_B;
 8             break;
 9         case '-':
10             dResult = dNum_A - dNum_B;
11             break;
12         case '*':
13             dResult = dNum_A * dNum_B;
14             break;
15         case '/':
16             dResult = dNum_A / dNum_B;
17             break;
18         }
19 
20         return dResult;
21     }
22 };

 

而在设计时已经猜测出以后可能会添加其他的运算, 所以我们把运算的算法抽象了出来.

在一开始设计时, 我们有可能很难预先猜测, 但我们却可以在发生小变化时, 就及早去想办法应对发生大变化的可能. 换句话说:在我们最初编程时, 假设变化不会发生, 而当变化真实发生时, 我们就创建抽象一隔离以后发生的同种类变化.

面对需求, 对程序的发动是通过增加新代码进行的, 而不是更改现有的代码.

当然开发人员应该仅对程序中呈现出频繁变化的那部分做出抽象, 然而, 对于应用程序中的每个部分都刻意地进行抽象同样也不是一个好注意, 拒绝不成熟的抽象和抽象本身一样重要.


里氏代换原则

问题:
当前有一个鸟类, 有一个飞行方法, 现在要求设计一个大雁类和一个企鹅类, 要怎么设计?

从生物学上来看大雁和企鹅都属于鸟, 那么是不是可以让大雁类和企鹅类都继承鸟类呢?

首先继承, 子类将拥有父类的所有成员变量和成员函数(private成员在子类中依然存在,但是却无法访问到), 那么企鹅虽然是鸟, 但它不会飞…如果企鹅继承了鸟类, 那么它就可以飞了…这明显不对, 所以企鹅不能继承鸟类.

这就是里氏代换原则: 子类型必须能够替换掉它们的父类型.
也就是说, 一个软件实体如果使用的是一个父类的话, 那么一定适用于其子类, 把程序里的父类都替换成它的子类, 程序的行为不会发生变化.

还是拿之前的计算器(策略模式)说话, 在Context中引用了Operation抽象类, 而在实际使用中, 我们根据需要生成Operation的子类来替换掉Operation抽象类, 而Context本身并不知道发生了什么变化, 它依然调用的是Operation抽象类中的方法.

正是因为子类可以替换其父类而不影响软件的功能, 父类才能真正被复用, 子类也能在父类的基础上增加新行为, 也才使得无需修改父类就可以扩展它.

在学习继承时, 我们就知道子类是一种特殊的父类, 子类对象可以当作父类对象使用也就是因为里氏代换原则.


依赖倒转原则

依赖倒转原则: 抽象不应该依赖细节, 细节应该依赖于抽象, 高层模块不应该依赖低层模块, 都应该依赖抽象.
说白了, 就是应该针对接口编程而不要对实现编程

在我们的计算器(策略模式)程序中, Context类依赖于Operation抽象类而不依赖具体的算法类, 具体的算法类也依赖于Operation抽象类, 根据里氏代换原则, 只要接口Operation抽象类是稳定的, 那么任何一个更改都不用担心其他受到影响(我们可以通过实例化新的子类来处理), 这使得无论高层模块还是低层模块都可以很容易地被复用.

为什么叫倒转? 这是针对面向过程来说的, 在面向过程的开发中, 为了使用常用的代码可以复用, 一般都会把这些常用的代码写成许许多多函数的程序库, 这样我们做新项目的时候, 就去调用这些函数就可以了.
例如:我们做的项目大多要访问数据库, 所以我们就把数据库的代码写成了函数, 每次做新项目时就去调用这些函数,这也就是高层依赖于低层模块了.

依赖倒转原则其实可以说是面向对象设计的标志, 编程时考虑的都是如何针对抽象编程而不是针对细节编程, 即程序中所有的依赖关系都是终止于抽象类或者接口.