抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

23种设计模式

单例模式(Singleton)

一、单例模式的定义

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

二、单例模式的特点

1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。

三、线程安全问题

一方面在获取单例的时候,要保证不能产生多个实例对象;
另一方面,在使用单例对象的时候,要注意单例对象内的实例变量是会被多线程共享的,推荐使用无状态的对象,不会因为多个线程的交替调度而破坏自身状态导致线程安全问题,比如我们常用的VO,DTO等(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题)。

四、实现单例模式的八种方式

1.饿汉式(静态常量)【可用】
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

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

private static final Singleton instance = new Singleton();

/**
* 私有化构造方法
*/
private Singleton() { }

public static Singleton getInstance() {
return instance;
}
}

2.饿汉式(静态代码块)【可用】
这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {

private static Singleton instance;

/**
* 静态代码块类创建Singleton实例
*/
static {
instance = new Singleton();
}

private Singleton() { }

public static Singleton getInstance() {
return instance;
}
}

3.懒汉式(线程不安全)【不可用】
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

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

private static Singleton singleton;

private Singleton() { }

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

4.懒汉式(线程安全,同步方法)【不推荐用】
解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。
缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

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

private static Singleton singleton;

private Singleton() { }

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

5.懒汉式(线程安全,同步代码块)【不可用】
由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {

private static Singleton singleton;

private Singleton() { }

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}

6.双检锁/双重校验锁(DCL,即 double-checked locking)【推荐使用】
Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
优点:线程安全、延迟加载、效率较高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {

private static volatile Singleton singleton;

private Singleton() { }

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

7.静态内部类【推荐使用】
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全、延迟加载、效率高。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {

private Singleton() { }

private static class SingletonInstance {
private static final Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return SingletonInstance.instance;
}
}

8.枚举【推荐使用】
借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

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
29
/**
* 枚举模式:最安全
*/
public class SingletonExample6 {

// 私有构造函数
private SingletonExample6() {

}

public static SingletonExample6 getInstance() {
return Singleton.INSTANCE.getInstance();
}

private enum Singleton {
INSTANCE;

private SingletonExample6 singleton;

// JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonExample6();
}

public SingletonExample6 getInstance() {
return singleton;
}
}
}

五、单例模式的优点

系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

六、单例模式的缺点

当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。

七、单例模式的使用场景

• 需要频繁的进行创建和销毁的对象;
• 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
• 工具类对象;
• 频繁访问数据库或文件的对象。

工厂模式(Factory)

一、什么是工厂模式

工厂模式将目的将创建对象的具体过程屏蔽隔离起来,从而达到更高的灵活性,工厂模式可以分为三类:

  • 简单工厂模式(Simple Factory)
  • 工厂方法模式(Factory Method)
  • 抽象工厂模式(Abstract Factory)

《设计模式》一书中将工厂模式分为两类:工厂方法模式与抽象工厂模式。将简单工厂模式看为工厂方法模式的一种特例,两者归为一类。

二、简单工厂模式

简单工厂模式的核心是定义一个创建对象的接口,将对象的创建和本身的业务逻辑分离,降低系统的耦合度,使得两个修改起来相对容易些,当以后实现改变时,只需要修改工厂类即可。

三、工厂方法模式

工厂方法模式将工厂抽象化,并定义一个创建对象的接口。每增加新产品,只需增加该产品以及对应的具体实现工厂类,由具体工厂类决定要实例化的产品是哪个,将对象的创建与实例化延迟到子类,这样工厂的设计就符合“开闭原则”了,扩展时不必去修改原来的代码。在使用时,用于只需知道产品对应的具体工厂,关注具体的创建过程,甚至不需要知道具体产品类的类名,当我们选择哪个具体工厂时,就已经决定了实际创建的产品是哪个了。

但缺点在于,每增加一个产品都需要增加一个具体产品类和实现工厂类,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。

  • 抽象工厂 AbstractFactory: 工厂方法模式的核心,是具体工厂角色必须实现的接口或者必须继承的父类,在 Java 中它由抽象类或者接口来实现。
  • 具体工厂 Factory:被应用程序调用以创建具体产品的对象,含有和具体业务逻辑有关的代码
  • 抽象产品 AbstractProduct:是具体产品继承的父类或实现的接口,在 Java 中一般有抽象类或者接口来实现。
  • 具体产品 Product:具体工厂角色所创建的对象就是此角色的实例。

四、抽象工厂模式

在工厂方法模式中,我们使用一个工厂创建一个产品,一个具体工厂对应一个具体产品,但有时候我们需要一个工厂能够提供多个产品对象,而不是单一的对象,这个时候我们就需要使用抽象工厂模式。

在介绍抽象工厂模式前,我们先厘清两个概念:

  • 产品等级结构:产品等级结构指的是产品的继承结构,例如一个空调抽象类,它有海尔空调、格力空调、美的空调等一系列的子类,那么这个空调抽象类和他的子类就构成了一个产品等级结构。
  • 产品族:产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品。比如,海尔工厂生产海尔空调、海尔冰箱,那么海尔空调则位于空调产品族中。

1、什么是抽象工厂模式

抽象工厂模式主要用于创建相关对象的家族。当一个产品族中需要被设计在一起工作时,通过抽象工厂模式,能够保证客户端始终只使用同一个产品族中的对象;并且通过隔离具体类的生成,使得客户端不需要明确指定具体生成类;所有的具体工厂都实现了抽象工厂中定义的公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。

​ 但该模式的缺点在于添加新的行为时比较麻烦,如果需要添加一个新产品族对象时,需要更改接口及其下所有子类,这必然会带来很大的麻烦。

  • 抽象工厂 AbstractFactory:定义了一个接口,这个接口包含了一组方法用来生产产品,所有的具体工厂都必须实现此接口。
  • 具体工厂 ConcreteFactory:用于生产不同产品族,要创建一个产品,用户只需使用其中一个工厂进行获取,完全不需要实例化任何产品对象。
  • 抽象产品 AbstractProduct:这是一个产品家族,每一个具体工厂都能够生产一整组产品。
  • 具体产品 Product

工厂模式小结:

1、工厂方法模式与抽象工厂模式的区别在于:

(1)工厂方法只有一个抽象产品类和一个抽象工厂类,但可以派生出多个具体产品类和具体工厂类,每个具体工厂类只能创建一个具体产品类的实例。

(2)抽象工厂模式拥有多个抽象产品类(产品族)和一个抽象工厂类,每个抽象产品类可以派生出多个具体产品类;抽象工厂类也可以派生出多个具体工厂类,同时每个具体工厂类可以创建多个具体产品类的实例

五、适用场景

工厂模式适用场景如下:

  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
  • 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
  • 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性

策略模式(Strategy)

策略模式的定义

策略模式是指对一系列的算法定义,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。

模型图:

Context:上下文角色,起到封装的作用,持有一个Strategy对象的引用

Strategy:策略角色(抽象) 通常为接口

ConcreteStrategy:具体的策略角色

实例说明

比如现在的百度网盘普通的用户没有额外的空间,会员会额外的获得2T空间 ,而超级会员额外获得5T空间+各种福利。

1、Strategy,抽象的策略,也就是获取的额外的空间

1
2
3
4
public interface AbstractStrategy {
   // 获取额外空间的方法
   public  void  getExSpace();
}

2、ConcreteStrategy,也就是普通用户,会员,超级会员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 普通用户
public class OrdinaryUser implements AbstractStrategy{
   
   @Override
   public void getExSpace() {
       Log.d("qzs","普通用户没有额外的空间获取");
   }
}

// 会员
public class Vip implements AbstractStrategy {
   @Override
   public void getExSpace() {
       Log.d("qzs","会员用户有2T额外的空间获取");
   }
}

// 超级会员
public class SuperVip implements AbstractStrategy {
   @Override
   public void getExSpace() {
       Log.d("qzs","超级会员用户有5T额外的空间获取");
   }
}

3、Context,上下文,为了封装

1
2
3
4
5
6
7
8
9
10
11
12
public class SpaceContext {
   private  AbstractStrategy abstractStrategy;

   public  SpaceContext(AbstractStrategy abstractStrategy){
       this.abstractStrategy=abstractStrategy;
   }

   // 调用抽象策略角色中的方法
   public void getExSpace(){
       this.abstractStrategy.getExSpace();
   }
}

4、调用

1
2
3
4
5
6
7
8
9
10
SpaceContext spaceContext;
       // 如果是普通用户
       spaceContext=new SpaceContext(new OrdinaryUser());
       spaceContext.getExSpace();
       // 如果是会员
       spaceContext=new SpaceContext(new Vip());
       spaceContext.getExSpace();
       // 如果是超级会员
       spaceContext=new SpaceContext(new SuperVip());
       spaceContext.getExSpace();

优缺点及适用场景

1、优点

  • 策略模式提供了管理相关的算法族的办法,算法可以切换。
  • 避免使用多重条件转移语句。

2、缺点

  • 客户端知道所有的策略类,并自行决定使用哪一个策略类,策略类完全暴露了。

  • 策略类有时会非常多。

3、适用场景

  • 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。

  • 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。

  • 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。

装饰者模式(Decorator)

装饰者模式的定义

装饰者模式(Decorator Pattern)也称为包装模式(Wrapper Pattern),以透明动态的方式来动态扩展对象的功能,也是继承关系的一种代替方案。

Component:抽象组件(可以是抽象类或者接口),被装饰的原始对象
ConcreteComponent:具体实现类,被装饰的具体对象
Decorator:抽象装饰者,职责就是为了装饰我们的组件对象,内部一定要有一个指向组件对象的引用
ConcreteDecoratorA:装饰者具体实现类,只对抽象装饰者做出具体实现
ConcreteDecoratorB:同上

实例说明

将人定义为抽象类,有一个抽象方法eat()

1
2
3
public abstract class Person {
public abstract void eat();
}

接着创建一个NormalPerson类继承Person,对eat()方法有了具体实现;NormalPerson类就是我们需要装饰的对象。

1
2
3
4
5
6
public class NormalPerson extends Person {
@Override
public void eat() {
System.out.println("吃饭");
}
}

这里定义一个``PersonFood类来表示装饰者的抽象类,保持了一个对Person`的引用,可以方便调用具体被装饰的对象方法,这样就可以方便的对其进行扩展功能,并且不改变原类的层次结构。

1
2
3
4
5
6
7
8
9
10
11
12
public class PersonFood extends Person {
private Person person;

public PersonFood(Person person){
this.person = person;
}

@Override
public void eat() {
person.eat();
}
}

接着就是具体的装饰类了,这两个类没有本质上的区别,都是为了扩展NormalPerson类,不修改原有类的方法和结构。

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
29
30
31
32
33
34
35
36
37
public class ExpensiveFood extends PersonFood {
public ExpensiveFood(Person person) {
super(person);
}

@Override
public void eat() {
super.eat();
eatSteak();
drinkRedWine();
}

public void eatSteak(){
System.out.println("吃牛排");
}

public void drinkRedWine(){
System.out.println("喝拉菲");
}

}

public class CheapFood extends PersonFood {
public CheapFood(Person person) {
super(person);
}

@Override
public void eat() {
super.eat();
eatNoodles();
}

public void eatNoodles(){
System.out.println("吃面条");
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args){
Person person = new NormalPerson();

PersonFood cheapFood = new CheapFood(person);
cheapFood.eat();

PersonFood expensiveFood = new ExpensiveFood(person);
expensiveFood.eat();
}
}

优缺点

优点:

  • 装饰者模式与继承关系的目的都是要扩展对象的功能,但是装饰者模式可以提供比继承更多的灵活性。

  • 通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。

缺点:

  • 这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。

  • 装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂。

  • 装饰模式是针对抽象组件(Component)类型编程。但是,如果你要针对具体组件编程时,就应该重新思考你的应用架构,以及装饰者是否合适。当然也可以改变Component接口,增加新的公开的行为,实现“半透明”的装饰者模式。在实际项目中要做出最佳选择。

与代理模式的区别:

装饰者模式和代理模式很像,但是两者的目的不尽相同。装饰者模式是以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案;而代理模式则是一个给对象提供一个代理对象,并由代理对象来控制对原有对象的引用。

装饰者模式为本装饰的对象进行功能扩展;代理模式对代理对象进行控制,但不做功能扩展。

迭代器模式(Iterator Pattern)

迭代器模式(Iterator Pattern)是 Java 和 .Net 编程环境中非常常用的设计模式。这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。

迭代器模式属于行为型模式。

介绍

意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。

主要解决:不同的方式来遍历整个整合对象。

何时使用:遍历一个聚合对象。

如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。

关键代码:定义接口:hasNext, next。

应用实例:JAVA 中的 iterator。

优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。

缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

使用场景: 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。

注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。

实现

我们将创建一个叙述导航方法的 Iterator 接口和一个返回迭代器的 Container 接口。实现了 Container 接口的实体类将负责实现 Iterator 接口。

IteratorPatternDemo,我们的演示类使用实体类 NamesRepository 来打印 NamesRepository 中存储为集合的 Names

迭代器模式的 UML 图

步骤 1:

创建接口:

Iterator.java

1
2
3
4
5
6
public interface Iterator {   

public boolean hasNext();

public Object next();
}

Container.java

1
2
3
4
5
public interface Container {   

public Iterator getIterator();

}

步骤 2:

创建实现了 Container 接口的实体类。该类有实现了 Iterator 接口的内部类 NameIterator

NameRepository.java

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
29
public class NameRepository implements Container {   
public String[] names = {"Robert" , "John" ,"Julie" , "Lora"};

@Override
public Iterator getIterator() {
return new NameIterator();
}

private class NameIterator implements Iterator {
int index;

@Override
public boolean hasNext() {
if(index < names.length){
return true;
}
return false;
}

@Override
public Object next() {
if(this.hasNext()){
return names[index++];
}
return null;
}
}

}

步骤 3:

使用 NameRepository 来获取迭代器,并打印名字。

IteratorPatternDemo.java

1
2
3
4
5
6
7
8
9
10
public class IteratorPatternDemo {   

public static void main(String[] args) {
NameRepository namesRepository = new NameRepository();
for(Iterator iter = namesRepository.getIterator(); iter.hasNext();){
String name = (String)iter.next(); System.out.println("Name : " + name);
}
}

}

步骤 4:

执行程序,输出结果:

1
2
3
4
Name : Robert
Name : John
Name : Julie
Name : Lora

评论