漫谈设计模式之创建型模式

Christopher Alexander说过:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样你就能一次又一次地使用该方案而不必做重复劳动。”

一般而言,一个模式有四个基本要素:模式名称、问题、解决方案、效果。

总体来说,设计模式分为三大类:创建型模式、结构型模式、行为型模式。其中创建型模式抽象了实例化过程,它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。创建型模式共五种:抽象工厂模式、建造者模式、工厂方法模式、原型模式、单例模式。

一、 抽象工厂模式

1.1 目的
抽象工厂模式是提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

1.2 适用性
(1) 一个系统要独立于它的产品的创建、组合和表示时;
(2) 一个系统要由多个产品系列中的一个来配置时;
(3) 当你强调一系列相关的产品对象的设计以便进行联合使用时;
(4) 当你提供一个产品类库,而只想显示它们的接口而不是实现时。

1.3 结构和实现


抽象工厂模式
public interface Sender {  
    public void Send();  
}  

//两个实现类:
public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
}  

public class SmsSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
}  

//两个工厂类:
public class SendMailFactory implements Provider {  
    @Override  
    public Sender produce(){  
        return new MailSender();  
    }  
}  

public class SendSmsFactory implements Provider{    
    @Override  
    public Sender produce() {  
        return new SmsSender();  
    }  
}  

//接口:
public interface Provider {  
    public Sender produce();  
}  

//测试类
public class Test {  
    public static void main(String[] args) {  
        Provider provider = new SendMailFactory();  
        Sender sender = provider.produce();  
        sender.Send();  
    }  
}  

1.4 优缺点
(1) 分离了具体的类。如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好。
(2) 使得易于交换产品系列。一个具体工厂类在一个应用中仅出现一次。
(3) 有利于产品的一致性。当一个系列中的产品对象被设计成一起工作时,一个应用只能使用同一个系列中的对象。
(4) 难以支持新种类的产品。难以扩展抽象工厂以生产新种类的产品。因为抽象工厂方法确定了可以被创建的产品集合,但是支持新种类的产品需要扩展该工厂接口,这涉及抽象工厂类及其所有子类的改变。

二、 建造者模式

2.1 目的
工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后结合起来得到的。将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

2.2 适用性
(1) 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时;
(2) 当构造过程必须允许被构造的对象有不同的表示时。

2.3 代码实现

public class Builder {  
    private List<Sender> list = new ArrayList<Sender>();  
    public void produceMailSender(int count){  
        for(int i=0; i<count; i++){  
            list.add(new MailSender());  
        }  
    }  
      
    public void produceSmsSender(int count){  
        for(int i=0; i<count; i++){  
            list.add(new SmsSender());  
        }  
    }  
}  

//测试类:
public class Test {  
    public static void main(String[] args) {  
        Builder builder = new Builder();  
        builder.produceMailSender(10);  
    }  
}  

2.4 优点
(1) 它使你可以改变一个产品的内部表示。Builder对象提供给导向器一个构造产品的抽象接口。该接口使得生成器可以隐藏这个产品的表示和内部结构,同时也隐藏了该产品是如何装配的。
(2) 它将构造代码和表示代码分开。Builder模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。客户不需要知道定义产品内部结构的类的所有信息。
(3) 它使你可对构造过程进行更精细的控制。

三、 工厂方法模式

3.1 目的
定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。

3.2 适用性
(1) 当一个类不知道它所必须创建的对象的类的时候;
(2) 当一个类希望由它的子类来指定它所创建的对象的时候。

3.3 结构与实现
(1) 普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:


普通工厂模式
//共同接口:
public interface Sender {  
    public void Send();  
}  
//创建实现类:
public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
}  

public class SmsSender implements Sender {  
  
    @Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
}  
//建工厂类:
public class SendFactory {  
    public Sender produce(String type) {  
        if ("mail".equals(type)) {  
            return new MailSender();  
        } else if ("sms".equals(type)) {  
            return new SmsSender();  
        } else {  
            System.out.println("请输入正确的类型!");  
            return null;  
        }  
    }  
}  
//测试:
public class FactoryTest {  
    public static void main(String[] args) {  
        SendFactory factory = new SendFactory();  
        Sender sender = factory.produce("sms");  
        sender.Send();  
    }  
}  

(2)多个工厂方法模式,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图:


多个工厂模式

将上面的代码做下修改,改动下SendFactory类就行,如下:

public class SendFactory {  
   public Sender produceMail(){  
        return new MailSender();  
    }  
      
    public Sender produceSms(){  
        return new SmsSender();  
    }  
}  
//测试类如下:
public class FactoryTest {  
    public static void main(String[] args) {  
        SendFactory factory = new SendFactory();  
        Sender sender = factory.produceMail();  
        sender.Send();  
    }  
}  

(3)静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

public class SendFactory {      
    public static Sender produceMail(){  
        return new MailSender();  
    }  
      
    public static Sender produceSms(){  
        return new SmsSender();  
    }  
}  
public class FactoryTest {  
    public static void main(String[] args) {      
        Sender sender = SendFactory.produceMail();  
        sender.Send();  
    }  
}  

大多数情况下,我们会选用第三种——静态工厂方法模式。

四、 原型模式

4.1 目的
原始模型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。

4.2 适用性
(1) 当要实例化的类是运行时刻指定时,例如,通过动态装载;或者
(2) 为了避免创建一个与产品类层次平行的工厂类层次时;或者
(3) 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。

4.3 代码实现

//创建一个原型类:
public class Prototype implements Cloneable {  
    public Object clone() throws CloneNotSupportedException {  
        Prototype proto = (Prototype) super.clone();  
        return proto;  
    }  
}  

一个原型类只需要实现Cloneable接口,覆写clone方法,重点是调用super.clone(),进而调用Object的clone()方法。结合对象的浅复制和深复制详细介绍一下:

  • 浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。
  • 深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。
//深浅复制的例子:
public class Prototype implements Cloneable, Serializable {  
    private static final long serialVersionUID = 1L;  
    private String string;  
    private SerializableObject obj;  
  
    /* 浅复制 */  
    public Object clone() throws CloneNotSupportedException {  
        Prototype proto = (Prototype) super.clone();  
        return proto;  
    }  
  
    /* 深复制 */  
    public Object deepClone() throws IOException, ClassNotFoundException {  
        /* 写入当前对象的二进制流 */  
        ByteArrayOutputStream bos = new ByteArrayOutputStream();  
        ObjectOutputStream oos = new ObjectOutputStream(bos);  
        oos.writeObject(this);  
  
        /* 读出二进制流产生的新对象 */  
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());  
        ObjectInputStream ois = new ObjectInputStream(bis);  
        return ois.readObject();  
    }  
  
    public String getString() {  
        return string;  
    }  
  
    public void setString(String string) {  
        this.string = string;  
    }  
  
    public SerializableObject getObj() {  
        return obj;  
    }  
  
    public void setObj(SerializableObject obj) {  
        this.obj = obj;  
    }  
  
}  
  
class SerializableObject implements Serializable {  
    private static final long serialVersionUID = 1L;  
}  

4.4 优点
(1) 运行时刻增加和删除产品。原型模式允许只通过客户注册原型实例就可以将一个新的具体产品类并入系统。
(2) 改变值以指定新对象。高度动态的系统允许你通过对象复合定义新的行为。
(3) 改变构造以指定新对象。许多应用由部件和子部件来创建对象。
(4) 减少子类的构造。
(5) 用类动态配置应用。

五、 单例模式

5.1 目的
保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例对象能保证在一个JVM中,该对象只有一个实例存在。单例模式具有以下几个特点:

1、单例类有且仅有一个实例。  
2、单例类必须自己创建自己的唯一实例。  
3、单例类必须给所有其他对象提供单一实例。

5.2 适用性
(1) 对唯一实例的受控访问。单例类封装它的唯一实例,所以它可以严格的控制客户怎么以及何时访问它。
(2) 缩小名空间。避免了那些存储唯一实例的全局变量污染名空间。
(3) 允许对操作和表示的精华。单例类可以有子类,而且用这个扩展类的实例来配置一个应用是很容易的。
(4) 允许可变数目的实例。
(5) 比类操作更灵活。

5.3 实现
单例分为两种:懒汉式单例、饿汉式单例。
5.3.1 懒汉单例
首先介绍下什么是延迟加载,延迟加载是程序真正需要使用的时才去创建实例,不用时不创建实例。依据延迟加载可以把懒汉单例分为以下几类。

(1) 同步延迟加载单例

public class Singleton {  
    /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */  
    private static Singleton instance = null;  
  
    /* 私有构造方法,防止被实例化 */  
    private Singleton() {  
    }  
  
    /* 静态工程方法,创建实例 */  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

(2) 双重检测同步延迟加载单例

public class Singleton {  
  private volatile static Singleton instance = null;  
  private Singleton() {}  

  public static Singleton getInstance() {  
    if (instance == null) {  // 1
        synchronized (Singleton.class) {
         if (instance == null) {// 2
             instance = new Singleton();  // 3
          }  
       }  
    }  
    return instance;  
   }  
} 

双重检测同步延迟加载单例线程并非绝对安全;不能防止反序列化、反射产生新的实例。

线程不安全原因是Java的乱序执行、初始化对象需要时间。
对于语句3,JVM在执行时大致做了下述三件事:
a. 在内存中分配一块内存;
b. 调用构造方法;
c. 将内存的地址指向instance变量。(执行这一步后,instance != null)

如果按照abc的顺序执行也不会有什么问题。但由于Java乱序执行的机制,有可能在真实情况下执行顺序为acb。

假设t1、t2是两个线程。t1执行到1时,发现为null,于是执行到语句3,先执行a,再执行c,在还没有执行b时,时间片给了t2。这时,由于instance已经分配了地址空间,instance不为null了。所以t2在执行到语句1后直接return instance,获得了这个还没有被初始化的对象,然后在使用时就报错了。因为有可能得到了实例的正确引用,但却访问到其成员变量的不正确值。

(3) 内部类实现延迟加载单例

public class Singleton {  
   private Singleton() {}  
   public static class Holder {  
    // 这里的私有没有什么意义  
    /* private */static Singleton instance = new Singleton();  
   }  
   public static Singleton getInstance() {  
    // 外围类能直接访问内部类(不管是否是静态的)的私有变量  
    return Holder.instance;  
   }  
}

这种比上面1、2都好一些,既实现了线程安全,又避免了同步带来的性能影响。

5.3.2 饿汉模式
(1) 非延迟加载单例,也称为饿汉模式

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

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

饿汉模式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。但是如果类创建时失败,则永远无法获得该类的实例化。

5.4 优点
(1)减少了类的频繁创建,特别是减少大型对象的创建,很大程度上节约了系统开销。
(2)减少使用new操作符,降低了系统内存的使用频率,减轻GC压力。
(3)保证系统核心服务独立控制整个系统操作流程,如果有多个实例的话,系统将出现混乱。如同一个军队出现了多个司令员同时指挥,肯定会乱成一团,所以系统的核心控制模块必须使用单例模式,才能保证系统正常运行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容