简介

设计模式是软件开发过程中共性问题的可重用解决方案。

设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别或对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。

六大原则

单一职责原则(SRP)

Single Responsibility Principle

一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

开闭原则(OCP)

Open-Closed Principle

一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展

里氏代换原则(LSP)

Liskov Substitution Principle

所有引用基类(父类)的地方必须能透明地使用其子类的对象。

依赖倒置原则(DIP)

Dependence Inversion Principle

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,其核心思想是:要面向接口编程,不要面向实现编程。

接口隔离原则(ISP)

Interface  Segregation Principle

使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

迪米特法则(LoD)/最少知识原则(LKP)

Law of Demeter / Least Knowledge Principle

一个软件实体应当尽可能少地与其他实体发生相互作用。

  • 从迪米特法则的定义和特点可知,它强调以下两点:
    1. 从依赖者的角度来说,只依赖应该依赖的对象。
    2. 从被依赖者的角度说,只暴露应该暴露的方法。
  • 所以,在运用迪米特法则时要注意以下 6 点。
    1. 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
    2. 在类的结构设计上,尽量降低类成员的访问权限。
    3. 在类的设计上,优先考虑将一个类设置成不变类。
    4. 在对其他类的引用上,将引用其他对象的次数降到最低。
    5. 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
    6. 谨慎使用序列化(Serializable)功能。

实例

Sunny软件公司所开发CRM系统包含很多业务操作窗口,在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应,例如,当一个按钮(Button)被单击时,对应的列表框(List)、组合框(ComboBox)、文本框(TextBox)、文本标签(Label)等都将发生改变,在初始设计方案中,界面控件之间的交互关系可简化为如图所示结构:

classDiagram Button --|> List Button --|> ComboBox List -- TextBox Button --|> Label List -- ComboBox Button --|> TextBox TextBox -- ComboBox

在图中,由于界面控件之间的交互关系复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除新控件。
现使用迪米特对其进行重构。

classDiagram Medidator --|> Button Medidator --|> List Medidator --|> ComboBox Medidator --|> Label Medidator --|> TextBox

在本实例中,可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类之后,界面控件之间不再发生直接引用,而是 将请求先转发给中间类,再由中间类来完成对其他控件的调用 。当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码。

总结

  • 单一职责原则:实现类要职责单一
  • 里氏替换原则:不要破坏继承体系
  • 依赖倒置原则:要面向接口编程
  • 接口隔离原则:在设计接口的时候要精简单一
  • 迪米特法则:要降低耦合
  • 开闭原则:要对扩展开放,对修改关闭

参考

创建型模式

单例模式

单例模式(Singleton Pattern)涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 单例模式特点:

    1. 单例类只有一个实例对象;
    2. 该单例对象必须由单例类自行创建;
    3. 单例类对外提供一个访问该单例的全局访问点。
  • 优点:

    1. 单例模式可以保证内存里只有一个实例,减少了内存的开销。
    2. 可以避免对资源的多重占用。
    3. 单例模式设置全局访问点,可以优化和共享资源的访问。
  • 缺点:

    1. 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径, 违背了开闭原则
    2. 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
    3. 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

饿汉式单例

该模式的特点是类一旦加载就创建一个单例,保证在调用getInstance方法之前单例已经存在了。
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。

1
2
3
4
5
6
7
8
9
public class SingletonExample {
//private 避免类在外部被实例化
private SingletonExample() {
}
private static SingletonExample _instance = new SingletonExample();
public static SingletonExample getInstance() {
return _instance;
}
}

懒汉式单例

该模式的特点是类加载时没有生成单例,只有当第一次调用getInstance方法时才去创建这个单例。

  • 先写一个最基础的实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SingletonExample {
    private SingletonExample() {
    }
    private static SingletonExample _instance;
    public static SingletonExample getInstance() {
    if (_instance==null) {
    _instance = new SingletonExample();
    }
    return _instance;
    }
    }
    如果编写的是多线程程序,上例代码 不能保证线程安全! 。假设在单例类被实例化之前,有两个线程同时在获取单例对象,线程1在执行完 if (instance == null) 后,线程调度机制将 CPU 资源分配给线程2,此时线程2在执行if (instance == null)时也发现单例类还没有被实例化,这样就会导致单例类被实例化两次。
  • 为了防止这种情况发生,需要对getInstance()方法同步:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class SingletonExample {
    private SingletonExample() {
    }
    private static volatile SingletonExample _instance = null; //保证instance在所有线程中同步
    public static synchronized SingletonExample getInstance() {
    //getInstance 方法前加同步
    if (_instance == null) {
    _instance = new SingletonExample();
    }
    return _instance;
    }
    }
    上例代码中的关键字volatilesynchronized可以保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源。
  • 双重加锁(double check)则能解决这个问题。双重加锁实现本质也是一种懒汉模式,相比第2种实现方式将会有较大的性能提升。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class SingletonExample {
    private SingletonExample() {
    }
    private volatile static SingletonExample _instance;
    public static SingletonExample getInstance() {
    if (_instance==null) {
    synchronized (SingletonExample.class) {
    if (_instance==null) {
    _instance = new SingletonExample();
    }
    }
    }
    return _instance;
    }
    }
  • 为什么要使用volatile
    不使用volatile,只能保证第一个线程的安全性,不能保证后面线程的安全性

参考

简单工厂

工厂方法

抽象工厂

建造者模式

原型模式

原型模式(Prototype Pattern)是用原型实例指定创建对象的种类,并通过拷贝这些原型创建新对象的创建型模式。

这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

  • 优点:
    • Java 自带的原型模式基于内存二进制流的复制,在性能上比直接new一个对象更加优良
    • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
  • 缺点:
    • 需要为每一个类都配置一个 clone 方法
    • clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码, 违背了开闭原则
    • 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。

原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式。在实际应用中,原型模式很少单独出现。经常与其他模式混用,他的原型类Prototype也常用抽象类来替代。

  • 原型模式包含以下主要角色。
    1. 抽象原型类:规定了具体原型对象必须实现的接口。
    2. 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
    3. 访问类:使用具体原型类中的 clone() 方法来复制新的对象。

浅克隆

在浅克隆中,如果原型对象的成员变量是值类型(byte,short,int,long,char,double,float,boolean).那么就直接复制,如果是复杂的类型,(枚举,String,对象)就只复制对应的内存地址。

  • 创建一个盒子类,提供一个添加物品的方法,实现Cloneable接口并重新clone方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Box implements Cloneable {
    private ArrayList<String> items = new ArrayList<String>();
    public Box() {
    }
    public void addItem(String item) {
    items.add(item);
    }
    @Override
    protected Box clone() throws CloneNotSupportedException {
    return (Box) super.clone();
    }
    @Override
    public String toString() {
    return String.format("盒子(@%s)[物件列表(@%s)%s]",Integer.toHexString(this.hashCode()),Integer.toHexString(items.hashCode()),items.toString());
    }
    }
  • 这样,一个浅克隆就写好了,调用看看
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void main(String[] args) throws Exception {
    Box box1 = new Box();
    box1.addItem("物件1");
    Box box2 = box1.clone();
    System.out.println(box1);
    System.out.println(box2);
    /*结果:
    盒子(@4926097b)[物件列表(@1b70c43)[物件1]]
    盒子(@19dfb72a)[物件列表(@1b70c43)[物件1]]
    */
    }
  • 虽然两个盒子的引用地址已经不相同,但是,上面已经提及,浅克隆 只复制引用对象的内存地址 。也就是说,在这之后调用box1.addItem("物件2");也会对box2造成影响,会变成下面这种情况

    盒子(@4926097b)[物件列表(@36e18842)[物件1, 物件2]]
    盒子(@19dfb72a)[物件列表(@36e18842)[物件1, 物件2]]

这种情况下,我们需要深克隆。

深克隆

  • 在上面的Box的clone方法的基础上进行修改
    1
    2
    3
    4
    5
    6
    7
    @Override
    protected Box clone() throws CloneNotSupportedException {
    Box clone = (Box) super.clone();
    //items也需要克隆
    clone.items = (ArrayList<String>) this.items.clone();
    return clone;
    }
  • 再进行测试
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args) throws Exception {
    Box box1 = new Box();
    box1.addItem("物件1");
    Box box2 = box1.clone();
    box1.addItem("物件2");
    System.out.println(box1);
    System.out.println(box2);
    /*结果:
    盒子(@4926097b)[物件列表(@36e18842)[物件1, 物件2]]
    盒子(@19dfb72a)[物件列表(@1b70c43)[物件1]]
    */
    }
    这样就实现了完全的克隆,两个对象之间没有任何瓜葛,你改你的,我改我的,互不影响,这种克隆就叫做深克隆。

参考

结构型模式

适配器模式

组合模式

装饰模式

代理模式

代理模式是以扩展目标对象功能为目的、通过代理对象来间接操控目标对象的设计模式。

我们大家都知道微商代理,简单地说就是代替厂家卖商品,厂家“委托”代理为其销售商品。关于微商代理,首先我们从他们那里买东西时通常不知道背后的厂家究竟是谁,也就是说,“委托 者”对我们来说是不可见的;其次,微商代理主要以朋友圈的人为目标客户,这就相当于为厂家 做了一次对客户群体的“过滤”。我们把微商代理和厂家进一步抽象,前者可抽象为代理类,后 者可抽象为委托类(被代理类)。通过使用代理,通常有两个优点,并且能够分别与我们提到的 微商代理的两个特点对应起来:
优点一: 可以隐藏委托类的实现;
优点二: 可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。

代理模式的元素是:共同接口、代理对象、目标对象。
代理模式的行为:由代理对象执行目标对象的方法、由代理对象扩展目标对象的方法。

代理模式的宏观特性:对客户端只暴露出接口,不暴露它以下的架构。
代理模式的微观特性:每个元由个类构成。

静态代理

什么是静态代理?
很简单,代理类和实现类实现同一个接口,代理类有一个实现类的引用,客户调用代理类的方法时,代理类就调用实现类的方法。称为静态,是因为代理类和实现类是写死的,就是在编译阶段就确定的。

classDiagram 接口 <|.. 实现类 接口 <|.. 代理类 代理类 <-- Client 实现类 <-- 代理类 class 接口{ <<interface>> }
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
// 接口
interface A {
public void f();
}
// 实现类
class AImpl implements A {
public void f() {
System.out.println("A.f()");
}
}
// 代理类
class AProxy implements A {
A a;
public AProxy(A a) {
this.a = a;
}
public void f() {
// do something else
a.f();
// do something else
}
}
// Client
public static void main() {
A aImpl = new AImpl();
AProxy aProxy = new AProxy(aImpl);
aProxy.f();
}

这个例子好像也没什么用,不过可以在代理类的f()方法调用实现类的方法前后加上一些代码;在构造方法中也可以通过网络类加载器加载别的机器上的类,这样本地调用时,感觉不到远程机器,就像调用本地代码一样

动态代理

如果有好多类要代理,就要写好几个代理类,这样比较麻烦,这时动态代理就有用了。
动态代理的本质就是用户提供类名、方法名、参数,代理类执行方法,返回结果。
用类加载器可以将类加载到虚拟机,用Class clazz表示,有这个对象,就可以执行它的方法。(这就是反射)
这就实现了动态代理。

具体实现:

graph RL Client --> P["Proxy实现类<br>Proxy(InvocationHandler)"] --> I["InvocationHandler实现类<br>实现类的引用<br>invoke"] --> 实现类 P & 实现类 --> interface["<interface>"] Ptips["这个Proxy实现类是<br>由Proxy的static方法在执行时刻生成的<br>这个类和被代理类有相同的接口<br>并且它的构造函数的参数是InvocaHandler。<br>因为这个类是在运行时刻生成的<br>可以根据传入不同的参数生成不同的代理类<br>所以是动态代理。"] -.-> P

动态代理类并不是程序员写的,而是根据传入的参数,由Proxy类在运行时生成的,所以可以传入不同的参数,这样就可以在运行时产生不同的代理类,所以是动态的。

1
2
3
4
5
// InvocationHandler实现类,里面有一个object对象,指向真正的实现类
InvocationHandler handler = new MyInvocationHandler();
// 代理类,是有Proxy生成的,根据这点代码,已知的是,它实现了被代理类的接口,而且它有个参数为InvocationHandler作为参数的构造函数
Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);
Foo f = (Foo)proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);

使用时,一般按下面的写法:

1
Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class<?>[]{Foo.class}, handler);

Spring的AOP(面向切面编程)就是用的动态代理实现的,可以用于日志,权限控制,缓存等,可以在InvocationHandler中的invoke方法内部调用实际方法前后加上一些有用的代码。

参考

亨元(蝇量)模式

外观模式

桥接模式

行为型模式

模板模式

解释器模式

策略模式

状态模式

观察者模式

备忘录模式

中介者模式

命令模式

访问者模式

责任链模式

迭代器模式

并发型模式

在软件工程中,并发型模式是用来处理多线程编程范式的一类设计模式。

主动对象

阻止模式

双重检查锁定模式

Guarded suspension

领导者/追随者模式

监视对象

读写锁模式

调度者模式

线程池

线程本地存储

反应堆模式