1. 软件设计

1.1 复杂性

首先以《大话设计模式》一书中的一个例子来引入:

话说曹操在大胜之后,宴请文武,在酒酣耳热的时候,诗兴大发,不觉吟到:”喝酒唱歌,人生真爽……“。众文武称:”好诗!“,于是一臣子命印刷工匠去印刷,以便流传天下。那个时候活字印刷术还没出现,都是先直接雕刻整体,也就是在一个板子上刻完。工匠刻完后,将样张呈现给曹操一看,曹操感觉不妥,说道”喝与唱此话过俗,应改为“对酒当歌较好。于是此臣就命工匠重新来过。工匠眼看连夜刻版之工,彻底白费,心中叫苦不选,只得照办。样张再次出来请曹操过目,曹操细细一品,觉得还是不好,说:”人生真爽太过直接,不够意境,因此应改为“对酒当歌,人生几何?当臣转告工匠之时,工匠卒。

从事软件开发方向的开发者们知道,软件产品的功能迭代速度是非常快的,软件设计的复杂性,究其根本就是—变化。一个产品在众多版本的迭代后,其复杂性越来越大,可维护性越来越低,最终犹如空中楼阁,摇摇欲坠。

在这里插入图片描述

1.2 解决

如何解决复杂性?

  • 可以借助【快排】【归并】的思想——即分而治之,将大问题分解为多个小问题,将复杂问题分解为多个简单问题。

  • 还有就是用面向对象的思想抽象: 由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节, 而去处理泛化和理想化了的对象模型。

上面两种方式描述还是很抽象,具体的距离和方案可以再学习 23 种设计模式再回来看,是很容易理解的。

1.3 面向对象

通常所说的设计模式隐含地表示“面向对象设计模式”。到底什么是面向对象?
面向对象的三大特点:

封装:抽象属性和行为

继承:实现抽象,或复用现有代码

多态:相同父类,不同表现

以活字印刷术为例,活字印刷术的字模就是有一个字,行为就是印刷,将两个属性进行抽象,然后封装如下。

1
2
3
4
5
6
7
class TypePatternProtocol {
public:
// 字模
const char* _words;
// 印刷
virtual void print() = 0;
};

学过C++的都知道,抽象类是不能实例化的。现实也是,绝知此事要躬行。现在我们去做一个字模。

上面对活字印刷术的字模进行了封装描述,要制造需要符合上面的属性,于是继承上面的抽象类。

去博物馆看活字印刷术样本,其字模只有一个字,这里演示,写两个字。

1
2
3
4
5
6
7
8
9
10
class FaceWord: public TypePatternProtocol {
// 印刷
virtual void print() {
std::cout << _words << std::endl;
}
public:
FaceWord() {
_words = "面向";
}
};

我们还有很多其他的字需要制造,虽然我们规范了活字印刷术的属性与行为,但是不同的字模,我们可以打印出不同的字。这就是多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ObjectWord: public TypePatternProtocol {
// 印刷
virtual void print() {
std::cout << _words << std::endl;
}
public:
ObjectWord() {
_words = "对象";
}
};

void type_pattern_print( TypePatternProtocol *word) {
word->print();
}

int main() {
type_pattern_print(new FaceWord());
type_pattern_print(new ObjectWord());
return 0;
}


// >> 面向
// >> 对象

2. 设计原则

2.1 说明

“面向对象设计模式”不意味着设计模式就是面向对象设计模式。而是满足设计原则,也可以称之为面向对象设计原则。
设计模式一定不要理解为是算法,设计模式都是基于后面所说的设计原则所推导的。

设计原则比设计模式更重要,它是衡量设计模式的尺子。因此需要充分理解设计原则,万变不离其宗,即便有千千万万的设计模式,也是从设计原则出发的。同理,我们在日常开发中,需要去尽量满足这七大原则。

2.2 七大原则

  • 依赖倒置原则:前面说到变化与稳定,依赖倒置原则的本质就是变化应该依赖于稳定。也就是说:抽象不应该依赖实现细节,实现细节应该依赖抽象。也就是我们应该面向接口编程,而不是针对实现编程。

  • 开闭原则:对扩展开放,对修改关闭。当实现一个类的时候,应该可以让其被扩展,但是对内部的修改是要禁止的。比如一个手机,为了增加保护,可以对其增加保护套和保护膜,但是不能更改手机壳等原来本身的属性。

  • 单一职责原则:一个类应该只能有一个引起其变化的原因,也就是其承担的责任应该只能有一个。责任可以理解为作用。举个例子,让你停止一局王者荣耀的游戏的原因只能是这局打完,而不是对象给你打电话,或者手机关机等等诸多原因,尤其在设计类的时候需要避免这种功能情况。

  • 里氏替换原则:子类必须能替换父类。这个意思就是说,从父类继承来的属性和方法,应该保持能够使用。比如从父类继承来的方法,重写的时候,里面直接抛异常,这就导致这个方法不能直接使用。比如鸟类是会飞的,那么属于鸟类的动物就要会飞。在编程的世界中,企鹅由于不会飞,因此不是继承自鸟类。

  • 接口隔离原则:不应该让用户依赖不需要的接口,应该建立专门的接口,是接口小而完备。接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。

  • 组合/聚合复用原则:举个例子来说明聚合与组合。诗句和诗句一起组合成了诗,诗和诗聚合成了诗集。诗集少一首诗并不会有什么影响,但是诗句少了一句诗就不是原来的诗了。继承在某种程度上破坏了封装性,子类父类耦合度高。而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。

  • 迪米特原则:又被称之为知道最少原则,在类的结构设计时,应该尽量降低类或者成员的访问权限。好的封装就会降低类与类之间的耦合,越提高复用。

3. 总结

我这里所说的设计模式其实并不是说要多么复杂的一个系统、框架、算法,而是一种基于设计原则的一种设计技巧与规范。

在日常开发中,要能够识别该场景中稳定与变化的部分,提前设计一个合理的代码框架或者逻辑结构。即使后续迭代,但是其稳定的部分不会大改,同时变化的部分也可以合理控制。

对于开发者来说,设计一个合理的工程架构非常必要的,约束开发规范,降低维护成本,提高结构稳定性。

否则就像网上所举例子:修补水管其中一个漏洞,但另一个地方可能会破损喷水。其实就是更改其中一处代码,可能引发各处问题,牵一发而动全身。

ok。瑞思拜~~