设计模式

为什么我们需要设计模式

有一类问题会在软件设计中反复出现,我们能够提出一种抽象的方法来解决这类问题,这就是设计模式。

设计模式的七大原则

  • 单一职责原则
  • 接口隔离原则
  • 依赖反转原则
  • 里氏替换原则
  • 开闭原则
  • 迪米特法则
  • 合成复用原则

23种设计模式

5个创建型

  • 单例模式
  • 工厂模式
  • 抽象工厂模式
  • 原型模式
  • 建造者模式

7个结构性

  • 适配器模式
  • 桥接模式
  • 装饰者模式
  • 组合模式
  • 外观模式
  • 享元模型
  • 代理模式

11个行为型

  • 模版方法模式
  • 命令模式
  • 访问者模式
  • 迭代器模式
  • 观察者模式
  • 中介者模式
  • 备忘录模式
  • 解释器模式
  • 状态模式
  • 策略模式
  • 责任链模式

单一职责原则

一个类只管一个职责。

例子1

如果我们创建了交通工具类,他掌管着很多工具,汽车、飞机、轮船,显然我们不适合让一个类来管理这么多种交通工具,这样导致职责太多,也不适合分别为这三种交通工具建立3个类,这样导致修改过多,正确的做法是创建三个函数,来分别管理他们。

例子2

又如我们有树、链表、数组,我们要便利他们,你肯定不适合创建一个类,一个函数来遍历。应该是一个类三个函数分别遍历树、链表、数组
但是如果这种方法级的单一职责原则导致类过于庞大,应该考虑到使用类级的单一职责原则。

这样可以降低类的复杂度,提高可读性,降低变更的风险。

接口隔离原则

将类之间的依赖降低到最小的接口处。

例子1

接口interface有5个方法,被类B和类D实现,被类A和类C依赖,但是A使用B只依赖接口123,C使用D只依赖接口145,这就导致了你的B多实现了4、5两个方法,D多实现了2、3两个方法。我们应该把interface拆分成3个,1,23,45,B实现1和23,D实现1和45。

例子2

比方说你有一个数组类和一个链表类,都实现了一个接口类,这个接口包含插入、删除、遍历、反转、排序,然后你有一个数组操作类,他只用到了插入删除遍历排序,还有一个链表操作类,他只用到了插入删除遍历反转,这个设计就很糟糕。

你应该创建3个接口,第一个为插入删除遍历,第二个为反转,第三个为排序,让数组实现第一个接口和最后一个接口,让链表实现第一个接口和第二个接口。

依赖反转原则

高层模块不应该依赖底层模块,他们都应该依赖其抽象,抽象不应该依赖具体,具体应该依赖抽象,因为具体是多变的,抽象是稳定的。

例子1

有一个email类,一个person类,person接受邮件的时候要将email作为函数参数来实现,这就导致person依赖email,这很糟糕,万一需求变了,来了个微信类,来了个QQ类,来了个钉钉类,你岂不是要实现一堆person的方法?

你应该设计一个接受者接口,让其作为接受者接口的实现,让person依赖接受者这个接口。

例子2

有一个数组类和一个操作类,操作类需要操作数组的首个元素,我们将数组类作为操作类的函数的参数,这很糟糕,万一需求变了,要求操作链表怎么办?

我们应该定义一个容器接口,让数组类实现它,对于操作类,我们只需要将容器接口作为参数即可,如果需求变化,加入了链表类,也不会导致大量的修改,非常稳定。

里氏替换原则

子类能够替换父类,并不产生故障,子类不要重写父类的方法。如果不能满足就不要这样继承。

例子1

做了一个减法类,然后让另外一个加法类继承减法类,重写了减法类的方法为加法。你觉得这样合适吗?你应该定一个更加基础的类,让加法类和减法类都继承它。

例子2

做了一长方形类,有个函数叫返回面积,让正方形类继承了长方形类,有个主类先定义了长为2,宽为2的长方形,然后让长扩大4倍,就成了82,如果你用正方形类来替换长方形类的位置,扩大4被以后面积成了88,这很糟糕,应该让长方形继承正方形。

开闭原则

一个模块和函数应该对扩展开放,对修改关闭。

就是说我们尽量去扩展原有的功能,而不是修改功能。另一方面源代码应该允许扩展。

例子

有一个数组、链表,有一个排序类,我们让排序类去对数组和链表排序,这个也不是很好,如果我们加入了双端数组,则需要修改排序类。

正确的方法是将排序作为一个成员方法来实现,即在基类中就定义一个排序的虚函数。

迪米特法则

一个类和其他类个关系越少越好。

例子

有类A,B,C,其中B是A的成员,C是B的成员,下面是这个糟糕的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
class C {
public:
void f() {}
};
class B {
public:
C c;
};
class A {
B b;
void doing_some_thing() { b.c.f(); }
};
int main() {}

这里注意到b.c.f();这里导致了A和C扯上了关系,正确的做法应该是在B中声明函数cf();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C {
public:
void f() {}
};
class B {
public:
C c;
void cf() {}
};
class A {
B b;
void doing_some_thing() { b.cf(); }
};
int main() {}

现在A就和C没关系了。

合成复用原则

尽量使用合成而不是继承。

说的就是让一个类的对象做另外一个类的成员变量。

单例模式

单例模式的类,只允许出现一个对象。

饿汉式

构造函数私有化,在内部之间final,new创建自己或者使用静态代码块new,提供静态方法访问。

简单,避免线程同步,在类装载的时候实例化,没有达到懒加载,可能造成内存浪费。

线程不安全的懒汉式

构造函数私有化,在内部创建自己的引用,设为空值,提供静态方法调用,在静态方法中有选择性地new自己。

简单,线程不安全,达到了懒加载效果。

线程安全的懒汉式

在if中进行同步操作,在同步中进行if最后new,注意使用volatile。

简单,线程安全。

静态内部类

字面意思,很强,懒加载,因为类只会加载一次,所以线程安全,这个写法最优秀。

枚举方式

用枚举类型将类导入。

工厂模式

设计一个工厂类,包含一个函数,返回指定类型

工厂方法模式

我们让原先的工厂作为基类,让多个工厂继承他,这就成为了工厂方法模式,比方说最开始的时候我们有多种口味的🍕,我们使用工厂模式完成了,现在来了新的需求,要求有多个地方的🍕,这时候我们就使用继承。为每个地理位置创建一个工厂。

抽象工厂模式

考虑工厂方法模式,让工厂的父类作为接口即可。

原型模式

用原型实例来拷贝另一个对象,java中为.clone(),c++中为=。

深拷贝前拷贝

是否拷贝指针指向的内容。

建造者模式

将复杂对象的建造方式抽象出来,一步一步抽象的建造出来。

产品

创建的产品对象。

抽象建造者

指定建造流程。

具体建造者

实现抽象建造者。

指挥者

隔离客户和对象的生产过程,控制产品对象的生产过程。

建房子

比方你现在要建造一个房子,你需要打地基,砌墙,封顶,你可以建造矮房子,也可以建造高房子,现在你就可以使用建造者模式,房子是产品,建造者能打地基,砌墙,封顶,房子组合为建造者,建造者聚合指挥者,我们依赖指挥者。

StringBuilder

Appendable是抽象建造者,AbstractStringBuilder为建造者,不能实例化,StringBuild为指挥者和具体建造者,但是是由AbstractStringBuilder建造的。

适配器模式

将一个类的接口转化为用户可以使用的接口,如c++的queue和stack为deque的适配器

类适配器

一般为继承,一个类继承了另外一个类,通过简化接口,达到适配的效果

对象适配器