0%

java-设计模式(一)

GOF23种设计模式

设计模式主要分三个类型:创建型、结构型和行为型。

创建型

  1. Singleton,单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点
  2. Abstract Factory,抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们的具体类。
  3. Factory Method,工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到了子类。
  4. Builder,建造模式:将一个复杂对象的构建与他的表示相分离,使得同样的构建过程可以创建不同的表示。
  5. Prototype,原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。

行为型

  1. Iterator,迭代器模式:提供一个方法顺序访问一个聚合对象的各个元素,而又不需要暴露该对象的内部表示。
  2. Observer,观察者模式:定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知自动更新。
  3. Template Method,模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,TemplateMethod使得子类可以不改变一个算法的结构即可以重定义该算法得某些特定步骤。
  4. Command,命令模式:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队和记录请求日志,以及支持可撤销的操作。
  5. State,状态模式:允许对象在其内部状态改变时改变他的行为。对象看起来似乎改变了他的类。
  6. Strategy,策略模式:定义一系列的算法,把他们一个个封装起来,并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户。
  7. China of Responsibility,职责链模式:使多个对象都有机会处理请求,从而避免请求的送发者和接收者之间的耦合关系
  8. Mediator,中介者模式:用一个中介对象封装一些列的对象交互。
  9. Visitor,访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这个元素的新操作。
  10. Interpreter,解释器模式:给定一个语言,定义他的文法的一个表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子
  11. Memento,备忘录模式:在不破坏对象的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

结构型

  1. Composite,组合模式:将对象组合成树形结构以表示部分整体的关系,Composite使得用户对单个对象和组合对象的使用具有一致性。
  2. Facade,外观模式:为子系统中的一组接口提供一致的界面,fa?ade提供了一高层接口,这个接口使得子系统更容易使用。
  3. Proxy,代理模式:为其他对象提供一种代理以控制对这个对象的访问
  4. Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
  5. Decrator,装饰模式:动态地给一个对象增加一些额外的职责,就增加的功能来说,Decorator模式相比生成子类更加灵活。
  6. Bridge,桥模式:将抽象部分与它的实现部分相分离,使他们可以独立的变化。
  7. Flyweight,享元模式

一、工厂模式

1.1、简单工厂模式(静态工厂模式)

当你需要某个对象的时候,通常你是new出来的,但是这样代码耦合度太高,因此我们通过一个工厂来生产出来这个对象

  • 工厂:负责实现创建所有实例的内部逻辑,并且提供一个外界调用的方法,创建所需产品对象
  • 抽象产品:用来描述产品的公共接口
  • 具体产品:描述生产的具体产品
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* @ Product.java
* 抽象产品
* 描述产品的公共接口
*/
abstract class Product {
//产品介绍
abstract void intro();
}

/**
* @ AProduct.java
* 具体产品A
* (可以看成是一种饮料:可乐)
*/
public class AProduct extends Product{
@Override
void intro() {
System.out.println("可乐");
}
}

/**
* @ BProduct.java
* @具体产品B
* @(可以看成是一种饮料:奶茶)
*/
public class BProduct extends Product{
@Override
void intro() {
System.out.println("奶茶");
}
}

/**
* @ CProduct.java
* 具体产品C
* (可以看成是一种饮料:咖啡)
*/
public class CProduct extends Product{
@Override
void intro() {
System.out.println("咖啡");
}
}
/*Factory.java*/
/**
* 工厂
* 负责实现创建所有实例的内部逻辑,并提供一个外界调用的方法,创建所需的产品对象。
*/
public class Factory {
/**
* 供外界调用的方法
* (可以看成是对外提供的三种按钮)
* @param type
* @return 产品实例
*/
public static Product getProduct(String type) {
switch (type) {
case "A":
return new AProduct();
case "B":
return new BProduct();
case "C":
return new CProduct();
default:
return null;
}
}
}
/*test.java*/
public class Test {
public static void main(String[] args) {
//创建具体的工厂
Factory factory = new Factory();
//根据传入的参数生产不同的产品实例
//(按下不同的按钮,获取饮料)
Product A = Factory.getProduct("A");
A.intro();
Product B = Factory.getProduct("B");
B.intro();
Product C = Factory.getProduct("C");
C.intro();
}
}

优点:将创建使用工作分开,不必关心类对象如何创建,实现了解耦;
缺点:违背“开放 - 关闭原则”,一旦添加新产品就不得不修改工厂类的逻辑,这样就会造成工厂逻辑过于复杂。

1.2、工厂方法模式(工厂模式)

可以理解成将上述的静态工厂模式中的工厂进行拆分,让对应的实例产品都有一个单独的工厂对其进行生产。

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
/**
* @ Product.java
* 抽象产品
*/
abstract class Product {
//产品介绍
abstract void intro();
}

/**
* @ ProductA.java
* 具体产品A
*/
public class ProductA extends Product{
@Override
void intro() {
System.out.println("饮料A");
}
}

/**
* @ ProductB.java
* 具体产品B
*/
public class ProductB extends Product{
@Override
void intro() {
System.out.println("饮料B");
}
}

工厂:Factory.java、FactoryA.java 、FactoryB.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
30
31
32
/**
* @ Factory.java
* 抽象工厂
*/
abstract class Factory {
//生产产品
abstract Product getProduct();
}

/**
* @ FactoryA.java
* 具体工厂A
* 负责具体的产品A生产
*/
public class FactoryA extends Factory{
@Override
Product getProduct() {
return new ProductA();
}
}

/**
* @ FactoryB.java
* @具体工厂B
* 负责具体的产品B生产
*/
public class FactoryB extends Factory{
@Override
Product getProduct() {
return new ProductB();
}
}

测试:Test.java

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) {
//创建具体的工厂
FactoryA factoryA = new FactoryA();
//生产相对应的产品
factoryA.getProduct().intro();
FactoryB factoryB = new FactoryB();
factoryB.getProduct().intro();
}
}

一个抽象产品类,可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类只能创建一个具体产品类的实例

优点:

  1. 符合开-闭原则:新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可
  2. 符合单一职责原则:每个具体工厂类只负责创建对应的产品

缺点:

  1. 增加了系统的复杂度:类的个数将成对增加
  2. 增加了系统的抽象性和理解难度
  3. 一个具体工厂只能创建一种具体产品

1.3、抽象工厂模式

为了解决工厂模式中一个工厂只能生产一个具体产品的问题。抽象工厂模式使用抽象类添加了抽象工厂,然后让具体工厂继承该抽象工厂达到一个具体工厂可以生产多种具体产品的效果。

抽象工厂:描述具体工厂的公共接口
具体工厂:描述具体工厂,创建产品的实例,供外界调用
抽象产品族:描述抽象产品的公共接口
抽象产品:描述具体产品的公共接口
具体产品:具体产品

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
38
39
40
41
42
43
44
45
46
47
48
/**
* @ Product.java
* 抽象产品族 (食品)
*/
abstract class Product {
//产品介绍
abstract void intro();
}

/**
* @ ProductA.java
* 抽象产品 (饮料)
*/
abstract class ProductA extends Product{
@Override
abstract void intro();
}

/**
* @ ProductB.java
* 抽象产品 (零食)
*/
abstract class ProductB extends Product{
@Override
abstract void intro();
}

/**
* @ ProductAa.java
* 具体产品 (矿泉水)
*/
public class ProductAa extends ProductA{
@Override
void intro() {
System.out.println("矿泉水");
}
}

/**
* @ ProductBb.java
* 具体产品 (面包)
*/
public class ProductBb extends ProductB{
@Override
void intro() {
System.out.println("面包");
}
}

工厂:Factory.java、FactoryA.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
/**
* @ Factory.java
* 抽象工厂
*/
abstract class Factory {
//生产饮料
abstract Product getProductA();
//生产零食
abstract Product getProductB();
}

/**
* @ FactoryA.java
* 具体工厂A
* 负责具体的A类产品生产
*/
public class FactoryA extends Factory{
@Override
Product getProductA() {
//生产矿泉水
return new ProductAa();
}
@Override
Product getProductB() {
//生产面包
return new ProductBb();
}
}

测试:Test.java

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
//创建零食售卖机(具体工厂),
FactoryA factoryA = new FactoryA();
//获取矿泉水与面包(具体产品)
factoryA.getProductA().intro();
factoryA.getProductB().intro();
}
}

多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。 每个具体工厂类可以创建多个具体产品类的实例。.

优点:

  1. 降低耦合
  2. 符合开-闭原则
  3. 符合单一职责原则
  4. 不使用静态工厂方法,可以形成基于继承的等级结构。

缺点:难以扩展新种类产品

二、单例模式(singleton)

确保一个类只有一个实例,并提供该实例的全局访问点。

单例的实现主要是通过以下两个步骤

  1. 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例
  2. 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

1、单例模式的应用场景和优缺点

举一个小例子,在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。

继续说回收站,我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如我每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。

适用场景:

  • 1.需要生成唯一序列的环境
  • 2.需要频繁实例化然后销毁的对象。
  • 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 4.方便资源相互通信的环境

常见应用场景:

  • Windows的Task Manager(任务管理器)
  • windows的Recycle Bin(回收站)也是典型的单例应用
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作 ,否则内容不好追加。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源
  • 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统
  • Application 也是单例的典型应用(Servlet编程中会涉及到)
  • 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
  • 在servlet编程中,每个Servlet也是单例
  • 在spring MVC框架/struts1框架中,控制器对象也是单例

优点

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的创建销毁对象,可以提高性能;
  • 避免对共享资源的多重占用,简化访问;
  • 为整个系统提供一个全局访问点。

缺点

  • 不适用于变化频繁的对象;
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
  • 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

单例模式的实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton
{
private static Singleton instance=null; //静态私有成员变量
//私有构造函数
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
11
12
13
14
// 饿汉式单例
public class Singleton1 {

// 指向自己实例的私有静态引用,主动创建
private static Singleton1 singleton1 = new Singleton1();

// 私有的构造方法
private Singleton1(){}

// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton1 getSingleton1(){
return singleton1;
}
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

3、懒汉式——线程安全、调用效率低、延时加载

单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 懒汉式单例
public class Singleton2 {

// 指向自己实例的私有静态引用
private static Singleton2 singleton2;

// 私有的构造方法
private Singleton2(){}

// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static synchronized Singleton2 getSingleton2(){//synchronized保证getSingleton2是一个同步方法,可以保证在多线程情况下单例对象唯一性

//没有synchronized,只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

// 被动创建,在真正需要使用时才去创建
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}

优点:单例只有在使用时才会被实例化,在一定程度上节约了资源

缺点:第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用getInstance都进行同步,造成不必要的同步开销。这种模式一般不建议使用。

4、双重加锁机制(Double Check Lock)——线程安全

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
18
19
20
21
22
23
24
25
26
27
28
29
public class Singleton
{
private static Singleton instance;
//程序运行时创建一个静态只读的进程辅助对象
private static readonly object syncRoot = new object();
private Singleton() { }
public void dosomething()
{
System.out.println("do sth.");
}
public static Singleton GetInstance()
{
//先判断是否存在,不存在再加锁处理
if (instance == null)
{
//在同一个时刻加了锁的那部分程序只有一个线程可以进入
//lock(syncRoot) 获取对象syncRoot的互斥锁,可以简单理解为,当多个线程同时执行到lock的时候,大家排队,一个一个地进行。
lock (syncRoot)//可以使用synchronized (Singleton.class)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}

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
public class Singleton
{
//当声明对象的引用为volatile后,“问题的根源”的三行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止,类在实例化过程中会严格按照1、2、3顺序执行下去。
private volatile static Singleton instance;
//程序运行时创建一个静态只读的进程辅助对象
private Singleton() { }
public void dosomething()
{
System.out.println("do sth.");
}
public static Singleton GetInstance()
{
//先判断是否存在,不存在再加锁处理
if (instance == null)
{
//在同一个时刻加了锁的那部分程序只有一个线程可以进入
synchronized (Singleton.class)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}

第一次 if (instance == null)主要是为了避免不必要的同步,第二次的判断是为了在null的情况下创建实例。

优点:既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。
资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。

缺点:第一次加载慢

参考文章

5、静态初始化

1
2
3
4
5
6
7
8
9
10
11
12
//阻止发生派生,而派生可能会增加实例
public sealed class Singleton
{
//在第一次引用类的任何成员时创建实例,公共语言运行库负责处理变量初始化
private static readonly Singleton instance=new Singleton();

private Singleton() { }
public static Singleton GetInstance()
{
return instance;
}
}

6、静态内部类

解决DCL在某些情况下出现失效的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton{
private Slingleton()
{ }
public static Singleton getInstance()
{
return SingleHolder.sInstance;
}
//静态内部类
private static class SingletonHolder
{
private static final Singleton sInstance = new Singleton();
}
}

当第一次加载singleton类时并不会初始化sInstance,只有在第一次调用Singleton的getInstance方法时才会导致sInstance被初始化。

第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也可以确保单例对象的唯一性,同时也延迟了单例的实例化。推荐。

7、枚举单例

1
2
3
4
5
6
7
public enum SingletonEnum{
INSTANCE;
public void dosomething()
{

}
}

8、使用容器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonManager{
private static Map<String,Object> objMap = new HashMap<String,Object>();
private SingletonManager(){}
public static void registerService(String key,Object instance)
{
if(!objMap.containsKey(key))
{
objMap.put(key,instance);
}
}
public static Object getService(String key)
return objMap.get(key);
}

在程序的初始时,可以将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。

优点:可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低用户的使用成本,降低耦合度。

要想实现效率高的线程安全的单例,我们必须注意以下两点:

  • 尽量减少同步块的作用域;
  • 尽量使用细粒度的锁。