设计模式学习笔记(七):原型模式

1 概述

1.1 引言

对于某些岗位来讲,工做周报的内容会大同小异,若是用户每次都须要从空白的周报进行输入无疑会浪费用户不少的时间,若是周报可以按照用户的自定义来生成模板,或者从已有模板修改小部分获得新模板,这样用户的输入效率会大大提升。原型模式正是为解决这类问题而生。java

1.2 定义

原型模式:使用原型实例指定建立对象的种类,而且经过克隆这些原型建立新的对象。apache

原型模式是一种对象建立型模式。编程

原型模式的工做原理很简单,将一个原型对象传给那个要发动建立的对象,这个要发动建立的对象经过请求原型对象克隆本身来实现建立过程。原型模式是一种另类的建立型模式,建立克隆对象的工厂就是原型类自身,工厂方法由克隆方法实现。数组

经过克隆方法建立的对象是全新的对象,它们在内存中拥有新的地址,一般对克隆多产生的对象进行的修改不会对原型对象形成任何的影响,每个克隆的对象都是相互独立的,经过不一样的方式对克隆对象进行修改以后,能够获得一系列类似但不彻底相同的对象。微信

1.3 结构图

在这里插入图片描述

1.4 角色

  • Prototype(抽象原型类):声明克隆方法的接口,是全部具体原型类的公共父类,能够是抽象类也能够是接口,还能是具体实现类
  • ConcretePrototypr(具体原型类):实如今抽象原型类中声明的克隆方法,在克隆方法中返回本身的一个克隆对象
  • Client(客户类):让一个原型对象克隆自身从而建立一个新的对象,在客户类中只须要直接实例化或经过工厂方法等方式建立一个原型对象,再经过调用该对象的克隆方法便可获得多个相同的对象。

2 典型实现

2.1 步骤

  • 定义抽象原型类:定义为接口/抽象类,至少须要定义一个相似clone的方法
  • 定义具体原型类:实现/继承抽象原型类,核心是实现其中的clone
  • 定义客户类:针对抽象原型类编程,首先须要经过实例化或工厂方法等建立一个原型对象,接着经过其中的clone方法获取多个对象

2.2 抽象原型类

这里定义为接口:网络

interface Prototype
{
    Prototype clone();
    String getAttr();
    void setAttr(String attr);
}

2.3 具体原型类

实现抽象原型接口,核心在于如何实现clone,在Java中clone一般有两种实现方式:ide

  • 通用实现方法
  • clone()方法

2.3.1 通用实现方法

通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,并将相关的参数传入新建立的对象中,保证成员变量相同。函数

代码以下:工具

class ConcretePrototype implements Prototype
{
    private String attr;

    @Override
    public String getAttr() {
        return this.attr;
    }

    @Override
    public void setAttr(String attr) {
        this.attr = attr;
    }
    
    @Override
    public Prototype clone()
    {
        Prototype = new ConcretePrototype();
        prototype.setAttr(attr);
        return prototype;
    }
}

2.3.2 clone

java.lang.Object提供了一个clone(),能够将一个Java对象克隆一份,利用clone()能够直接将对象克隆一份,可是必须实现Cloneable接口,不然clone()时会抛出CloneNotSupportedException测试

代码以下:

class ConcretePrototype implements Prototype,Cloneable
{
    private String attr;

    public String getAttr() {
        return this.attr;
    }

    public void setAttr(String attr) {
        this.attr = attr;
    }
    
    public Prototype clone()
    {
        Object object = null;
        try 
        {
            object = super.clone();
        } 
        catch (Exception e) 
        {
            e.printStacktrace();
        }
        return (Prototype)object;
    }
}

通常而言,Java中的clone()知足:

  • 对任何对象x都有x.clone() != x,也就是克隆的对象与原型对象不是同一个对象
  • 对任何对象x都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型同样
  • 若是xequals()定义恰当,那么x.clone().equals(x)应该成立

具体实现步骤以下:

  • 覆盖clone(),并声明为public
  • clone()中调用super.clone()
  • 派生类须要实现Cloneable接口

2.4 客户类

客户类针对抽象原型类编程,经过实例化获取具体原型后,调用其中的clone进行克隆:

public class Test
{
    public static void main(String[] args) {
        Prototype prototype1 = new ConcretePrototype();
        prototype1.setAttr("test");
        Prototype prototype2 = prototype1.clone();
        System.out.println(prototype1.getAttr() == prototype2.getAttr());
        System.out.println(prototype1 == prototype2);
    }
}

3 实例

开发一个工做周报系统,工做周报的内容都大同小异,只有一些小地方存在差别,可是系统每次默认建立的都是空白报表,用户不断复制粘贴来填写重复内容。使用原型模式对其进行优化,快速建立相同或相似的工做周报。

设计以下:

  • 抽象原型类:无(也能够认为是Object
  • 具体原型类:WeeklyLog

代码以下:

public class Test
{
    public static void main(String[] args) {
        WeeklyLog weeklyLog1 = new WeeklyLog();
        weeklyLog1.setContent("content");
        weeklyLog1.setName("Weekly log 1");
        weeklyLog1.setDateTime(LocalDateTime.now());

        System.out.println(weeklyLog1.getName());
        System.out.println(weeklyLog1.getContent());
        System.out.println(weeklyLog1.getDateTime());

        WeeklyLog weeklyLog2 = weeklyLog1.clone();
        weeklyLog2.setName("Weekly log 2");
        System.out.println(weeklyLog2.getName());
        System.out.println(weeklyLog2.getContent());
        System.out.println(weeklyLog2.getDateTime());
    }
}

class WeeklyLog implements Cloneable
{
    private String name;
    private LocalDateTime dateTime;
    private String content;

    //getter and setter
    //...

    public WeeklyLog clone()
    {
        Object obj = null;
        try
        {
            obj = super.clone();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return (WeeklyLog)obj;
    }
}

4 浅克隆与深克隆

通常来讲,工做周报可能会携带附件,使用上面的原型模式来进行工做周报的复制没有问题,可是附件(通常是另外一个类)不会进行复制。这是由于浅克隆与深克隆的缘由,下面具体来看一下。

4.1 浅克隆

在浅克隆中,若是原型对象的成员变量是值类型,将复制一份给克隆对象。(在Java中)值类型包括:

  • int
  • double
  • byte
  • boolean
  • char
  • float
  • long
  • short

也就是这些类型的值都会完整复制一份给克隆对象,对于引用类型,则将引用对象的地址复制一份给克隆对象。(在Java中)引用类型就是除了基本类型以外的全部类型,常见的有:

  • 接口
  • 数组

对于引用类型,原型对象与克隆对象指向相同的内存地址,也就是其实并无被复制,而是共享一份地址相同的值。
在Java中能够经过Objectclone()实现浅克隆,也就是上面例子的作法。

4.2 深克隆

在深克隆中,不管变量是值类型仍是引用类型都会完整复制一份给克隆对象。

在Java中实现深克隆能够经过序列化等方式实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个复制品,而原对象仍然存在于内存中。想要进行序列化必须实现Serializable接口。

代码以下:

public class Test
{
    public static void main(String[] args) {
        WeeklyLog weeklyLog1 = new WeeklyLog();
        WeeklyLog weeklyLog2 = null;
        Attachement attachement = new Attachement();
        weeklyLog1.setAttachement(attachement);
        try
        {
            weeklyLog2 = weeklyLog1.deepClone(); 
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        System.out.println(weeklyLog1 == weeklyLog2);
        System.out.println(weeklyLog1.getAttachement() == weeklyLog2.getAttachement());
    }
}

class Attachement implements Serializable
{
    private String name;
    //getter and setter
    //...
}

class WeeklyLog implements Serializable
{
    private String name;
    private LocalDateTime dateTime;
    private String content;
    private Attachement attachement;

    //getter and setter
    //...

    public WeeklyLog deepClone() throws IOException , ClassNotFoundException , OptionalDataException
    {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(this);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);

        return (WeeklyLog)objectInputStream.readObject();
    }
}

固然除了使用ByteArrayOutput/InputStream以及ObjectInput/OutputStream外,还能够利用如下工具类进行深克隆:

  • org.apache.commons.lang3.SerializationUtils.clone():须要实现Serializable接口
  • Gson:无需实现Serializable接口,toJson()+fromJson()
  • Jackson:也是无需实现Serializable接口,readValue()+writeValueAsString()

5 原型管理器

5.1 定义

原型管理器是将多个原型对象存储在一个集合中供客户端使用的专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,若是须要某个原型对象的克隆,能够经过复制集合中对应的原型对象来获取。在原型管理器中针对抽象原型类进行编程。
结构图以下:
在这里插入图片描述

5.2 实例

平常办公中会有许多公文须要建立,例如《可行性分析报告》,《立项建议书》,《软件需求规格说明书》,《项目进展报告》等,为了提升工做效率须要为各种公文建立模板,用户能够经过这些模板快速建立新的公文,这些公文模板进行统一的管理,系统根据用户的请求的不一样生成不一样的新公文。

首先是抽象原型以及具体原型的代码:

interface OfficialDocument extends Cloneable
{
    OfficialDocument clone();
    void display();
}

//可行性分析报告
class FAR implements OfficialDocument 
{
    public OfficialDocument clone()
    {
        OfficialDocument far = null;
        try
        {
            far = (OfficialDocument)super.clone();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return far;
    }

    public void display()
    {
        System.out.println("可行性分析报告");
    }
}

//软件需求规格说明书
class SRS implements OfficialDocument
{
    public OfficialDocument clone()
    {
        OfficialDocument srs = null;
        try
        {
            srs = (OfficialDocument)super.clone();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return srs;
    }

    public void display()
    {
        System.out.println("软件需求规格说明书");
    }
}

接着是原型管理器的代码,使用枚举单例实现:

enum PrototypeManager 
{
    INSTANCE;

    private Hashtable<String,OfficialDocument> hashtable = new Hashtable<>();
    private PrototypeManager()
    {
        add("far",new FAR());
        add("srs",new SRS());
    }

    public void add(String key,OfficialDocument document)
    {
        hashtable.put(key, document);
    }

    public OfficialDocument get(String key)
    {
        return ((OfficialDocument)hashtable.get(key)).clone();
    }
}

测试代码:

public class Test
{
    public static void main(String[] args) {
        PrototypeManager manager = PrototypeManager.INSTANCE;
        OfficialDocument document1,document2,document3,document4;

        document1 = manager.get("far");
        document1.display();

        document2 = manager.get("far");
        document2.display();
        System.out.println(document1 == document2);

        document3 = manager.get("srs");
        document3.display();

        document4 = manager.get("srs");
        document4.display();
        System.out.println(document3 == document4);
    }
}

6 主要优势

  • 简化建立过程:当建立新的对象实例较为复杂时,使用原型模式能够简化对象的建立过程,经过复制一个已有实例能够提升新实例的建立效率
  • 扩展性较好:因为在原型模式中提供了抽象原型类,在客户端能够针对抽象原型类进行编程,而将具体原型类写在配置文件中,增长或减小具体原型类对系统都没有任何影响
  • 简化建立结构:原型模式提供了简化的建立结构。工厂方法模式经常须要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不须要这样,原型模式中产品的复制是经过封装在原型类中的克隆方法实现的,无需专门的工厂类来建立产品
  • 保存状态:可使用深克隆的方式保存对象的状态。使用原型模式将对象复制一份并将其状态保存起来,以便在须要的时候使用,例如恢复到某一历史状态,可辅助实现撤销操做

7 主要缺点

  • 修改不方便:须要为每个类配备一个克隆方法,并且该克隆方法位于一个类的内部,当对已有的类进行改造的时候,须要修改源代码,违背了OCP(开放闭合原则)
  • 深克隆须要嵌套类支持:在实现深克隆时须要编写较为复杂的代码,并且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象都必须支持深克隆,实现起来可能比较麻烦

8 适用场景

  • 建立新对象成本较大,好比初始化须要较长时间,占用太多的CPU资源或网络资源,新的对象能够经过原型模式对已有对象进行复制获取,若是是类似对象能够对成员变量稍做修改
  • 若是系统要保存对象的状态,而对象的变化状态很小,或者对象自己占用内存较少,可使用原型模式配合备忘录模式
  • 须要避免使用分层次的工厂类来建立分层次的对象,而且类的实例对象只有一个或不多的几个组合状态,经过复制原型对象获得新实例可能比使用构造函数建立一个实例方便

9 总结

在这里插入图片描述

若是以为文章好看,欢迎点赞。

同时欢迎关注微信公众号:氷泠之路。

在这里插入图片描述

相关文章
相关标签/搜索