设计模式及原则

设计模式及原则

设计模式

什么是设计模式?

设计模式是可以应用于常见问题的通用可重用解决方案
创建软件应用程序是为了满足不断变化和发展的需求。一个成功的应用程序还应该提供一种简单的方法来扩展它以满足不断变化的期望。后面所讲的设计原则,以及后续所学习的设计模式都是为了这个目的:应用于常见问题的通用可重用解决,满足不断变化和发展的需求。

面向对象的设计原则

面向对象的设计原则被称为SOLID。SOLID原则包括单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。在设计和开发软件时可以应用这些原则,以便创建易于维护和开发的程序。

单一职责原则

该原则是指软件模块应该只有一个被修改的理由,目的是为了让封装工作达到最佳的状态。

例如常用于Java中类的修改:修改某个类时,需要修改的原因不止一个,而且这个过程常常会影响其他类的修改。每个更改的职责/理由都会增加新的依赖关系,使代码不那么健壮。
e.g. 一个Car类,假设对Car类添加方法来处理增、删、改、查的数据库操作。此时Car不仅会封装逻辑,还会封装数据库操作。使得类的维护和测试更加困难,因为代码是紧密耦合的。
没有遵循单一职责原则增加功能的Car类
图:没有遵循单一职责原则增加功能的Car类
解决方案:创建两个类,一个用于封装Car逻辑(Car),另一个用于负责持久性(CarDao)。
逻辑封装与持久性分离
图:逻辑封装与持久性分离

开闭原则

“模块、类和函数应该对扩展开放,对修改关闭。”

修改某个已完成部分分险和成本往往更大,我们应该在此基础上继续进行建设(通过继承和多态的方式),而不是去修改原本的。~是最重要的设计原则之一,是大多数设计模式的基础。

里氏替换原则

派生类型必须完全可替代其基类型。基于面向对象语言中的子类型多态,派生对象可以用其父类型替换。
简单来说:当类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。此时可以说,类B是类A比较完整的派生对象,父类型可以替换子类型的多态(不同类型实体接口相同)。

接口隔离原则

使用的接口方法满足客户端功能实现最低要求即可。即,“客户端不应该依赖于它所不需要的接口”。

依赖倒置原则

“高级模块不应该依赖低级模块,两者都应该依赖抽象。”
“抽象不应该依赖于细节,细节应该依赖于抽象。”

这个原则的目的就是为了降低耦合,常用的方法是将高级逻辑与低级模块分开。可以尝试让它们都依赖于抽象进而减少二者之间的依赖关系。如此就可以替换或扩展其中任何一个模块而不影响其他模块。

e.g. 以下的vehicle为抽象类
依赖倒置原则的使用
图:依赖倒置原则的使用

创建型模式(Creational Pattern)

单例模式

概念

单例模式,故名思义是用来保证一个对象只能创建一个实例。单例模式是最简单也是最常用的一种设计模式,但因为过度使用或者在不恰当的时候使用,有的时候会被视为一种反模式。

如何实现?

只由单个类组成。为确保单例实例的唯一性,所有的单例构造器都要被声明为私有的(private),再通过声明静态(static)方法实现全局访问获得该单例实例。

注意一下要点:

  1. 单例构造器都要被声明为私有的(private)
  2. 通过申明静态方法全局访问获得该单例示例
  3. 自行向整个系统提供该实例
    单例模式实现示意图
    图:单例模式实现示意图

代码示例

以下为一段使用Java实现的单例模式,注意,要点在代码注释中再次写明:
Singleton.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {
private static Singleton instance;

// 单例构造器都要被声明为私有的(private)
private Singleton() {
System.out.println("Singleton is Instantiated.");
}

public static Singleton getInstance() {
// 注意,检查实例是否为空,如果不为空则表明对象在之前被创建过
if (instance == null)
// 自行向整个系统提供该实例
instance = new Singleton();

return instance;
}

public void doSomething() {
System.out.println("Something is Done.");
}
}

MyMainClass.java

1
2
3
4
5
6
7
8
public class MyMainClass {

public static void main(String[] s) {
// 通过申明静态方法全局访问获得该单例示例
Singleton singleton = Singleton.getInstance();
}

}

同步锁单例模式

在多线程应用中使用这种模式,如果实例为空,可能存在两个线程同时调用getInstance方法的情况。如果发生这种情况,第一个线程会首先使用新构造器实例化单例对象,同时第二个线程也会检查单例实例是否为空,由于第一个线程还没完成单例对象的实例化操作,所以第二个线程会发现这个实例是空的,也会开始实例化单例对象。

解决方法:创建一个代码块来检查实例是否空线程安全。在Java中,可以有以下两种方式:

  1. 向getInstance方法的声明中添加synchronized关键字
    1
    public static Singleton synchronized getInstance()
  2. 用synchronized代码块包装if(instance==null)条件
    1
    2
    3
    4
    synchronized (Singleton.class) {
    if (instance == null)
    instance = new Singleton();
    }

拥有双重校验锁机制的同步锁单例模式

同步锁单例模式虽然能够保证线程安全,但同时带来了延迟。用来检查实例是否被创建的代码是线程同步的,也就是说此代码块在同一时刻只能被一个线程执行,但是同步锁(locking)只有在实例没被创建的情况下才起作用。如果单例实例已经被创建了,那么任何线程都能用非同步的方式获取当前的实例。

因此我们设置只有在单例对象未实例化的情况下,才能在synchronized代码块前添加附加条件移动线程安全锁。且instance==null条件需要被检查了两次,保证在synchronized代码块中也要进行一次检查。

1
2
3
4
5
6
7
8
synchronized (Singleton.class) {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
}

无锁的线程安全模式

Java中单例模式的最佳实现形式中,类只会加载一次,通过在声明时直接实例化静态成员的方式来保证一个类只有一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LockFreeSingleton {
// 在声明时直接实例化静态成员
private static final LockFreeSingleton instance = new LockFreeSingleton();

private LockFreeSingleton() {
System.out.println("Singleton is Instantiated.");
}

public static synchronized LockFreeSingleton getInstance() {
return instance;
}

public void doSomething() {
System.out.println("Something is Done.");
}
}

// TODO

两种单例模式

按照实例对象被创建的时机,可以将单例模式分为两类:

  1. 提前加载单例模式:在应用开始时创建单例实例
  2. 延迟加载单例模式:在getInstance方法首次被调用时才调用单例构造器(例如上文提到的无锁的线程安全模式)

优缺点

工厂模式

建造者模式

原型模式

对象池模式

Reference

卡马尔米特·辛格(Kamalmeet Singh). Java设计模式及实践 (Java核心技术系列) (Chinese Edition) (Kindle Location 387). Kindle Edition.