《深入设计模式》一书阅读摘录。

1、设计模式是什么?

设计模式是软件设计中常见问题的典型解决方案。 它们就像能根据需求进行调整的预制蓝图, 可用于解决代码中反复出现的设计问题。

2、创建型模式

创建型模式提供了创建对象的机制, 能够提升已有代码的灵活性和可复用性。

(1)工厂方法模式

工厂方法模式是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。

示例:父类提供了一个Transport接口,并提供deliver方法,子类通过继承父类实例化具体类型。

产品对象层次结构

工厂方法模式优缺点:

  • 你可以避免创建者和具体产品之间的紧密耦合。

  • 单一职责原则。 你可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护。

  • 开闭原则。 无需更改现有客户端代码, 你就可以在程序中引入新的产品类型。

  • 应用工厂方法模式需要引入许多新的子类, 代码可能会因此变得更复杂。 最好的情况是将该模式引入创建者类的现有层次结构中。

(2)抽象工厂模式

抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。

示例:

1、声明抽象工厂——包含系列中所有产品构造方法的接口。 例如 create­Chair创建椅子 、 create­Sofa创建沙发和 create­Coffee­Table创建咖啡桌 。 这些方法必须返回抽象产品类型, 即我们之前抽取的那些接口: 椅子沙发咖啡桌等等。

2、我们都将基于 抽象工厂接口创建不同的工厂类。 每个工厂类都只能返回特定类别的产品, 例如, 现代家具工厂Modern­Furniture­Factory只能创建 现代椅子Modern­Chair 、 现代沙发Modern­Sofa和 现代咖啡桌Modern­Coffee­Table对象。

工厂类的层次结构

抽象工厂模式优缺点

  • 你可以确保同一工厂生成的产品相互匹配。

  • 你可以避免客户端和具体产品代码的耦合。

  • 单一职责原则。 你可以将产品生成代码抽取到同一位置, 使得代码易于维护。

  • 开闭原则。 向应用程序中引入新产品变体时, 你无需修改客户端代码。

  • 由于采用该模式需要向应用中引入众多接口和类, 代码可能会比之前更加复杂。

(3)建造者模式

建造者模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。

示例:我们想要建造不同风格的房子,如果为每种可能的房子对象都创建一个子类, 这可能会导致程序变得过于复杂。

大量子类会带来新的问题

生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。

该模式会将对象构造过程划分为一组步骤, 比如 build­Walls创建墙壁和 build­Door创建房门创建房门等。 每次创建对象时, 你都需要通过生成器对象执行一系列步骤。 重点在于你无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。

应用生成器模式

生成器模式优缺点:

  • 你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。

  • 生成不同形式的产品时, 你可以复用相同的制造代码。

  • 单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。

  • 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。

(4)原型模式

原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。

示例:有丝分裂会产生一对完全相同的细胞。 原始细胞就是一个原型, 它在复制体的生成过程中起到了推动作用。

细胞分裂

原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个 克隆方法。

克隆方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。 你甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。

支持克隆的对象即为原型

原型模式优缺点:

  • 你可以克隆对象, 而无需与它们所属的具体类相耦合。

  • 你可以克隆预生成原型, 避免反复运行初始化代码。

  • 你可以更方便地生成复杂对象。

  • 你可以用继承以外的方式来处理复杂对象的不同配置。

  • 克隆包含循环引用的复杂对象可能会非常麻烦。

(5)单例模式

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

  1. 保证一个类只有一个实例。最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
  2. 为该实例提供一个全局访问节点

单例模式优缺点:

  • 你可以保证一个类只有一个实例。

  • 你获得了一个指向该实例的全局访问节点。

  • 仅在首次请求单例对象时对其进行初始化。

  • 违反了单一职责原则。 该模式同时解决了两个问题。

  • 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。

  • 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。

  • 单例的客户端代码单元测试可能会比较困难

单例模式的几种实现方式:

1、懒汉式,线程不安全

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

2、懒汉式,线程安全

1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

3、饿汉式

1
2
3
4
5
6
7
public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

4、双检锁/双重校验锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

两次判空:

  1. 如果单例已经创建了,直接调用synchronized加锁比较耗性能。所以首先判断有没有创建,没创建再加锁。
  2. 第二层非空检查的原因是在同时多个线程调用时,A线程获得锁并创建成功实例,之后释放锁,前面一起竞争的B线程获得锁,首先判断非空,代表已经创建了,所以不会继续去创建实例。

volatile(内存可见性、禁止指令重排):

  1. A、B两个线程创建单例,此时A已经赋值,但是没有完成变量初始化,而B线程看到instance已经赋值就拿来使用,因为instance没有完成初始化,所以使用过程中可能产生无法预料的后果。

3、行为型模式

行为模式负责对象间的高效沟通和职责委派。

image-20231005202030507.png

(1)观察者模式

观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。

示例:如果你订阅了一份杂志或报纸, 那就不需要再去报摊查询新出版的刊物了。 出版社 (即应用中的 “发布者”) 会在刊物出版后 (甚至提前) 直接将最新一期寄送至你的邮箱中。

杂志和报纸订阅

观察者模式优缺点:

  • 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。

  • 你可以在运行时建立对象之间的联系。

  • 订阅者的通知顺序是随机的。

示例:

1、创建Subject类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.ArrayList;
import java.util.List;

public class Subject {

private List<Observer> observers
= new ArrayList<Observer>();
private int state;

public int getState() {
return state;
}

public void setState(int state) {
this.state = state;
notifyAllObservers();
}

public void attach(Observer observer){
observers.add(observer);
}

public void notifyAllObservers(){
for (Observer observer : observers) {
observer.update();
}
}
}

2、创建Observer类

1
2
3
4
public abstract class Observer {
protected Subject subject;
public abstract void update();
}

3、创建实体观察者类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BinaryObserver extends Observer{

public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}

@Override
public void update() {
System.out.println( "Binary String: "
+ Integer.toBinaryString( subject.getState() ) );
}
}

4、使用 Subject 和实体观察者对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();

//new HexaObserver(subject);
//new OctalObserver(subject);
new BinaryObserver(subject);

System.out.println("First state change: 15");
subject.setState(15);
System.out.println("Second state change: 10");
subject.setState(10);
}
}

(2)策略模式

在策略模式(Strategy Pattern)中一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

在策略模式定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码。

在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

示例:假如你需要前往机场。 你可以选择乘坐公共汽车、 预约出租车或骑自行车。 这些就是你的出行策略。 你可以根据预算或时间等因素来选择其中一种策略。

各种出行策略

策略模式建议找出负责用许多不同方式完成特定任务的类, 然后将其中的算法抽取到一组被称为策略的独立类中。

名为上下文的原始类必须包含一个成员变量来存储对于每种策略的引用。 上下文并不执行任务, 而是将工作委派给已连接的策略对象。

策略模式优缺点

  • 你可以在运行时切换对象内的算法。

  • 你可以将算法的实现和使用算法的代码隔离开来。

  • 你可以使用组合来代替继承。

  • 开闭原则。 你无需对上下文进行修改就能够引入新的策略。

  • 如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。 使用该模式只会让程序过于复杂。

  • 客户端必须知晓策略间的不同——它需要选择合适的策略。

示例:

1、创建一个接口。

1
2
3
4
5
//Strategy.java

public interface Strategy {
public int doOperation(int num1, int num2);
}

2、创建实现接口的实体类。

1
2
3
4
5
6
7
8
//OperationAdd.java

public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
1
2
3
4
5
6
7
8
//OperationSubtract.java

public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
1
2
3
4
5
6
7
8
//OperationMultiply.java

public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}

3、创建 Context 类。

1
2
3
4
5
6
7
8
9
10
11
//Context.java

public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}

4、使用 Context 来查看当它改变策略 Strategy 时的行为变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//StrategyPatternDemo.java

public class StrategyPatternDemo {
public static void main(String[] args) {
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

context = new Context(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

context = new Context(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}

(3)模板方法模式

模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。

示例:模板方法可用于建造大量房屋。 标准房屋建造方案中可提供几个扩展点, 允许潜在房屋业主调整成品房屋的部分细节。

建造大型房屋

模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。

模板方法模式优缺点:

  • 你可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。

  • 你可将重复代码提取到一个超类中。

  • 部分客户端可能会受到算法框架的限制。

  • 通过子类抑制默认步骤实现可能会导致违反里氏替换原则

  • 模板方法中的步骤越多, 其维护工作就可能会越困难。

4、结构型模式

结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。

image.png

5、六大原则

单一职责:对象设计要求独立,不能设计万能对象。

开闭原则:对象修改最小化。

里式替换:程序扩展中抽象被具体可以替换(接口、父类、可以被实现类对象、子类替换对象)

迪米特:高内聚,低耦合。尽量不要依赖细节。

依赖倒置:面向抽象编程。也就是参数传递,或者返回值,可以使用父类类型或者接口类型。从广义上讲:基于接口编程,提前设计好接口框架。

接口隔离:接口设计大小要适中。过大导致污染,过小,导致调用麻烦



本站总访问量