国内网站建设费用联盟

《设计模式JAVA版》之原型设计模式

只看楼主 收藏 回复
  • - -
楼主
  

原型设计模式


前言:7天连续报道了6起虐童事件,这个体制到底是怎么了?



看过《火影忍者》这部经典动漫的朋友应该都知道,主角漩涡鸣人S级的忍术是多重影分身之术,对于鸣人而言他就是本体也就是原型,而所有的影分身就属于目标。影分身跟本体有着一模一样的本事。在设计模式中也存在一个类似的模式,可以通过一个原型对象克隆出多个一模一样的对象,该模式称之为原型模式。原型模式也不太好理解主要难理解的地方在于深浅克隆,我会以画内存结构的形式来帮助大家理解浅克隆和深克隆。



先给一个思考题,读者朋友可以留言说一下自己观点:

思考题:能否将原型类代码中的clone()方法写成:public Prototype clone() { return this; }?为什么?




1 、大同小异的工作周报 

Sunny软件公司一直使用自行开发的一套OA (Office Automatic,办公自动化)系统进行日常工作办理,但在使用过程中,越来越多的人对工作周报的创建和编写模块产生了抱怨。追其原因,Sunny软件公司的OA管理员发现,由于某些岗位每周工作存在重复性,工作周报内容都大同小异,如图7-1工作周报示意图。这些周报只有一些小地方存在差异,但是现行系统每周默认创建的周报都是空白报表,用户只能通过重新输入或不断复制粘贴来填写重复的周报内容,极大降低了工作效率,浪费宝贵的时间。如何快速创建相同或者相似的工作周报,成为Sunny公司OA开发人员面临的一个新问题。

Sunny公司的开发人员通过对问题进行仔细分析,决定按照如下思路对工作周报模块进行重新设计和实现:

       (1)除了允许用户创建新周报外,还允许用户将创建好的周报保存为模板;

       (2)用户在再次创建周报时,可以创建全新的周报,还可以选择合适的模板复制生成一份相同的周报,然后对新生成的周报根据实际情况进行修改,产生新的周报。

       只要按照如上两个步骤进行处理,工作周报的创建效率将得以大大提高。这个过程让我们想到平时经常进行的两个电脑基本操作:复制和粘贴,快捷键通常为Ctrl + C和Ctrl + V,通过对已有对象的复制和粘贴,我们可以创建大量的相同对象。如何在一个面向对象系统中实现对象的复制和粘贴呢?不用着急,本文我们介绍的原型模式正为解决此类问题而诞生。


2 、原型模式概述


在使用原型模式时,我们需要首先创建一个原型对象,再通过复制这个原型对象来创建更多同类型的对象(目标对象)。试想,如果连旋涡鸣人的模样都不知道,怎么有影分身之术呢?原型模式的定义如下:

原型模式(Prototype  Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。

原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象(目标对象),这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在真实开发中的使用频率还是非常高的。原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。

      需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。

       原型模式的结构如图所示:

在原型模式结构图中包含如下几个角色:

      ●Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。

      ● ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。

      ● Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。

原型模式的核心在于如何实现克隆方法,下面将介绍两种在Java语言中常用的克隆实现方法:

1.通用实现方法

      通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,并将相关的参数传入新创建的对象中,保证它们的成员属性相同。示意代码如下所示:


class ConcretePrototype implements Prototype
{
private String  attr; //成员属性
public void  setAttr(String attr)
{
    this.attr = attr;
}
public String  getAttr()
{
    return this.attr;
}
public Prototype  clone() //克隆方法
{
    Prototype  prototype = new ConcretePrototype(); //创建新对象
    prototype.setAttr(this.attr);
    return prototype;
}
}

在客户类中我们只需要创建一个ConcretePrototype对象作为原型对象,然后调用其clone()方法即可得到对应的克隆对象,如下代码所示:


Prototype obj1  = new ConcretePrototype();
obj1.setAttr("Sunny");
Prototype obj2  = obj1.clone();


这种方法可作为原型模式的通用实现,它与编程语言特性无关,任何面向对象语言都可以使用这种形式来实现对原型的克隆。


2. Java语言提供的clone()方法

      学过Java语言的人都知道,所有的Java类都继承自java.lang.Object。事实上,Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。

      需要注意的是能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。如下代码所示:

class ConcretePrototype implements  Cloneable
{
……
public Prototype  clone()
{
  Object object = null;
  try {
     object = super.clone();//关键点在这里  } catch (CloneNotSupportedException exception) {
     System.err.println("Not support cloneable");
  }
  return (Prototype )object;
}
……
}

在客户端创建原型对象和克隆对象也很简单,如下代码所示:


Prototype obj1  = new ConcretePrototype();
Prototype obj2  = obj1.clone();

一般而言,Java语言中的clone()方法满足:

(1) 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;

(2) 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;

(3) 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。

      为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:

(1) 在派生类中覆盖基类的clone()方法,并声明为public;

(2) 在派生类的clone()方法中,调用super.clone();

(3)派生类需实现Cloneable接口。

      此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。

3、 原型类设计模式深浅克隆内存分析

咱们先从一个最简单的案例讲起:

上面已经对原型设计模式做了很详细的概述了,这里直接给出代码:

Person类:

public class Person implements Cloneable{
    private String name;
    private int age;
    private String sex;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    protected Person clone() {
        try {
            return (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

}

我们定义了一个Person类,并实现了Cloneable接口,那么Person类就成了原型类。这里面定义了三个字段,唯一需要注意的是这里的clone方法:

    @Override
    protected Person clone() {
        try {
            return (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

直接调用JAVA提供的clone()方法,此时就已经创建了一个新的Person实例,然后返回出去。

在客户端调用这段代码:

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setAge(25);
        p1.setName("风清扬");
        p1.setSex("男");
        System.out.println(p1.getAge());
        System.out.println(p1.getName());
        System.out.println(p1.getSex());

        System.out.println("--------------------------");

        Person p2 = p1.clone();
        System.out.println(p2.getAge());
        System.out.println(p2.getName());
        System.out.println(p2.getSex());

        System.out.println("--------------------------");

        p1.setAge(30);
        System.out.println(p1.getAge());
        System.out.println(p2.getAge());

        System.out.println("--------------------------");

        p2.setName("令狐冲");
        System.out.println(p1.getName());
        System.out.println(p2.getName());

    }
}

运行结果:

25

风清扬

--------------------------

25

风清扬

--------------------------

30

25

--------------------------

风清扬

令狐冲

咱们一段段的来看。

        Person p1 = new Person();
        p1.setAge(25);
        p1.setName("风清扬");
        p1.setSex("男");
        System.out.println(p1.getAge());
        System.out.println(p1.getName());
        System.out.println(p1.getSex());

        System.out.println("--------------------------");

        Person p2 = p1.clone();
        System.out.println(p2.getAge());
        System.out.println(p2.getName());
        System.out.println(p2.getSex());

首先创建原型类的实例对象,分别给三个字段赋值。然后通过刚刚创建的实例调用它的clone方法就得到一个目标实例对象,这个被克隆的实例就跟漩涡鸣人的影分身一个道理,你有什么我就有什么完全一样。然后都做了打印测试,发现结果果然一模一样。

这里的内存草图如下;

可见,这里的克隆就相当于在堆内存中又开辟了一块内存区间,且把原来内存中的所有数据全部都拷贝走了,长的简直一模一样啊!

然后再看代码:

        p1.setAge(30);
        System.out.println(p1.getAge());
        System.out.println(p2.getAge());

        System.out.println("--------------------------");

        p2.setName("令狐冲");
        System.out.println(p1.getName());
        System.out.println(p2.getName());

这里就是测试原型对象与目标对象的改变是否互相影响。首先修改了原型的年龄,然后打印了原型和目标的年龄,发现目标并没有改变。然后修改了目标的姓名,打印目标和原型的姓名,发现目标的修改没有影响原型。这是为什么呢???

其实上面的内存结构草图已经给出了答案,对象的克隆虽然内容完全一样,但是你是开辟了另一块内存区间的,两个地址不一样,两者的修改半毛钱关系都没有。再想,旋涡鸣人的一个影分身被杀掉了,本体或者其他的影分身并没有什么影响。


那么问题来了:上面的数据类型,都属于基本数据类型(String按照基本数据类型来分析,JAVA基础相信你应该知道),那么加入引用数据类型也是如此吗?接下来就会暴露问题引入深浅拷贝问题。

在Person类中加入一个引用类型的变量:


private List<String> friends;

public List<String> getFriends() {
    return friends;
}

public void setFriends(List<String> friends) {
    this.friends = friends;
}

然后在客户端代码中:


public class Main {
    public static void main(String[] args) {
        Person p1 = new Person();
        List<String> friends = new ArrayList<>();
        friends.add("110");
        friends.add("119");
        friends.add("120");
        p1.setFriends(friends);
        System.out.println(p1.getFriends());

        System.out.println("--------------------------");

        Person p2 = p1.clone();
        List<String> friends2 = p2.getFriends();
        System.out.println(friends2);

        System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");

        friends.add("114");
        friends2.add("911");

        System.out.println(p1.getFriends());
        System.out.println("--------------------------");
        System.out.println(p2.getFriends());

    }
}

打印log:

[110, 119, 120]

--------------------------

[110, 119, 120]

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

[110, 119, 120, 114, 911]

--------------------------

[110, 119, 120, 114, 911]

这段代码的意思是,原型类给引用类型的变量赋值,然后克隆一份拷贝,这份拷贝对象拥有原型所有数据。然后分别对原型和拷贝对象的引用类型的数据做了修改,发现不管修改哪一个都会互相影响。这其实就是浅拷贝问题,咱们还是通过内存图来看一下比较清楚:

只是拷贝了一层,对于引用类型的friends,并没有再一次拷贝,而是两个对象指向了同一块内存区域0XAA,这里出现的问题,就是平时所说的浅拷贝问题。解决浅拷贝问题,就是深拷贝了。那么咱们解决这个问题:


@Override
protected Person clone() {
    try {
        Person person = (Person) super.clone();
        List<String> friends = new ArrayList<>();
        List<String> strings = person.getFriends();
        for (String friend : strings) {
            friends.add(friend);
        }
        person.setFriends(friends);
        return person;
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    return null;
}

修改Person的clone代码如上,就相当于第一种克隆方式。然后这样运行代码后:

[110, 119, 120]

--------------------------

[110, 119, 120]

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

[110, 119, 120, 114]

--------------------------

[110, 119, 120, 911]

发现,原型和拷贝的改变都不会互相影响,这也就是所说的深拷贝。通过内存结构看一下是如何做到的:

这是两个对象分别维护了一块内存区域,肯定不会互相影响了!


接下来再应用一个完整的案例,来叙述原型模式。


4、完整解决方案


Sunny公司开发人员决定使用原型模式来实现工作周报的快速创建,快速创建工作周报结构图如图所示:

 在图中,WeeklyLog充当具体原型类,Object类充当抽象原型类,clone()方法为原型方法。WeeklyLog类的代码如下所示:

//工作周报WeeklyLog:具体原型类,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class WeeklyLog implements Cloneable
{
       private  String name;
       private  String date;
       private  String content;
       public  void setName(String name) {
              this.name  = name;
       }
       public  void setDate(String date) {
              this.date  = date;
       }
       public  void setContent(String content) {
              this.content  = content;
       }
       public  String getName() {
              return  (this.name);
       }
       public  String getDate() {
              return  (this.date);
       }
       public  String getContent() {
              return  (this.content);
       }
     //克隆方法clone(),此处使用Java语言提供的克隆机制
       public WeeklyLog clone()
       {
              Object obj = null;
              try
              {
                     obj = super.clone();
                     return (WeeklyLog)obj;     
              }
              catch(CloneNotSupportedException e)
              {
                     System.out.println("不支持复制!");
                     return null;
              }
       }
}


在实现cloneable接口的类中,实现clone方法时,切记不要调用this.clone,会发生死循环。

编写如下客户端测试代码:



class Client
{
       public  static void main(String args[])
       {
              WeeklyLog log_previous = new WeeklyLog();  //创建原型对象
              log_previous.setName("张无忌");
              log_previous.setDate("第12周");
              log_previous.setContent("这周工作很忙,每天加班!");

              System.out.println("****周报****");
              System.out.println("周次:" +  log_previous.getDate());
              System.out.println("姓名:" +  log_previous.getName());
              System.out.println("内容:" +  log_previous.getContent());
              System.out.println("--------------------------------");

              WeeklyLog  log_new;
              log_new  = log_previous.clone(); //调用克隆方法创建克隆对象
              log_new.setDate("第13周");
              System.out.println("****周报****");
              System.out.println("周次:" + log_new.getDate());
              System.out.println("姓名:" + log_new.getName());
              System.out.println("内容:" + log_new.getContent());
       }
}

编译并运行程序,输出结果如下:

****周报****
周次:第12周
姓名:张无忌
内容:这周工作很忙,每天加班!
--------------------------------
****周报****
周次:第13周
姓名:张无忌
内容:这周工作很忙,每天加班!

通过已创建的工作周报可以快速创建新的周报,然后再根据需要修改周报,无须再从头开始创建。原型模式为工作流系统中任务单的快速生成提供了一种解决方案。

思考:

如果在Client类的main()函数中增加如下几条语句:

System.out.println(log_previous == log_new);

System.out.println(log_previous.getDate() == log_new.getDate());

System.out.println(log_previous.getName() == log_new.getName());

System.out.println(log_previous.getContent() == log_new.getContent());

预测这些语句的输出结果。

5、 带附件的周报

      通过引入原型模式,Sunny软件公司OA系统支持工作周报的快速克隆,极大提高了工作周报的编写效率,受到员工的一致好评。但有员工又发现一个问题,有些工作周报带有附件,例如经理助理“小龙女”的周报通常附有本周项目进展报告汇总表、本周客户反馈信息汇总表等,如果使用上述原型模式来复制周报,周报虽然可以复制,但是周报的附件并不能复制,这是由于什么原因导致的呢?如何才能实现周报和附件的同时复制呢?我们在本节将讨论如何解决这些问题。

      在回答这些问题之前,先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

1.浅克隆

      在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制,如图所示:

  在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。为了让大家更好地理解浅克隆和深克隆的区别,我们首先使用浅克隆来实现工作周报和附件类的复制,其结构如图所示:

附件类Attachment代码如下:


//附件类
class Attachment
{
       private  String name; //附件名
       public  void setName(String name)
       {
              this.name  = name;
       }
       public  String getName()
       {
              return  this.name;
       }
     public void download()
     {
            System.out.println("下载附件,文件名为" + name);
     }
}

修改工作周报类WeeklyLog,修改后的代码如下:


//工作周报WeeklyLog
class WeeklyLog implements Cloneable
{
     //为了简化设计和实现,假设一份工作周报中只有一个附件对象,实际情况中可以包含多个附件,可以通过List等集合对象来实现
       private Attachment attachment;
       private String name;
       private  String date;
       private  String content;
    public void setAttachment(Attachment  attachment) {
              this.attachment = attachment;
       }
       public  void setName(String name) {
              this.name  = name;
       }
       public  void setDate(String date) {
              this.date  = date;
       }
       public  void setContent(String content) {
              this.content  = content;
       }
public Attachment  getAttachment(){
              return (this.attachment);
       }
       public  String getName() {
              return  (this.name);
       }
       public  String getDate() {
              return  (this.date);
       }
       public  String getContent() {
              return  (this.content);
       }
     //使用clone()方法实现浅克隆
       public WeeklyLog clone()
       {
              Object obj = null;
              try
              {
                     obj = super.clone();
                     return (WeeklyLog)obj;
              }
              catch(CloneNotSupportedException  e)
              {
                     System.out.println("不支持复制!");
                     return null;
              }
       }
}

客户端代码如下所示:


class Client
{
       public  static void main(String args[])
       {
              WeeklyLog  log_previous, log_new;
              log_previous  = new WeeklyLog(); //创建原型对象
              Attachment  attachment = new Attachment(); //创建附件对象
              log_previous.setAttachment(attachment);  //将附件添加到周报中
              log_new  = log_previous.clone(); //调用克隆方法创建克隆对象
              //比较周报
              System.out.println("周报是否相同? " + (log_previous ==  log_new));
              //比较附件
              System.out.println("附件是否相同? " +  (log_previous.getAttachment() == log_new.getAttachment()));
       }
}

编译并运行程序,输出结果如下:

 周报是否相同?  false
附件是否相同? true

由于使用的是浅克隆技术,因此工作周报对象复制成功,通过“==”比较原型对象和克隆对象的内存地址时输出false;但是比较附件对象的内存地址时输出true,说明它们在内存中是同一个对象。


2.深克隆

      在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制,如图所示:

Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。下面我们使用深克隆技术来实现工作周报和附件对象的复制,由于要将附件对象和工作周报对象都写入流中,因此两个类均需要实现Serializable接口,其结构如图所示:

 修改后的附件类Attachment代码如下:


import  java.io.*;
//附件类
class  Attachment implements Serializable
{
       private  String name; //附件名
       public  void setName(String name)
       {
              this.name  = name;
       }
       public  String getName()
       {
              return  this.name;
       }
     public void download()
     {
            System.out.println("下载附件,文件名为" + name);
     }
}

工作周报类WeeklyLog不再使用Java自带的克隆机制,而是通过序列化来从头实现对象的深克隆,我们需要重新编写clone()方法,修改后的代码如下:


import  java.io.*;
//工作周报类
class  WeeklyLog implements Serializable
{
       private  Attachment attachment;
       private  String name;
       private  String date;
       private  String content;
       public  void setAttachment(Attachment attachment) {
              this.attachment  = attachment;
       }
       public  void setName(String name) {
              this.name  = name;
       }
       public  void setDate(String date) {
              this.date  = date;
       }
       public  void setContent(String content) {
              this.content  = content;
       }
       public  Attachment getAttachment(){
              return  (this.attachment);
       }
       public  String getName() {
              return  (this.name);
       }
       public  String getDate() {
              return  (this.date);
       }
       public  String getContent() {
              return  (this.content);
       }
   //使用序列化技术实现深克隆
       public WeeklyLog deepClone() throws  IOException, ClassNotFoundException, OptionalDataException
       {
              //将对象写入流中
              ByteArrayOutputStream bao=new  ByteArrayOutputStream();
              ObjectOutputStream oos=new  ObjectOutputStream(bao);
              oos.writeObject(this);

              //将对象从流中取出
              ByteArrayInputStream bis=new  ByteArrayInputStream(bao.toByteArray());
              ObjectInputStream ois=new  ObjectInputStream(bis);
              return  (WeeklyLog)ois.readObject();
       }
}

客户端代码如下所示:

class Client
{
       public  static void main(String args[])
       {
              WeeklyLog  log_previous, log_new = null;
              log_previous  = new WeeklyLog(); //创建原型对象
              Attachment  attachment = new Attachment(); //创建附件对象
              log_previous.setAttachment(attachment);  //将附件添加到周报中
              try
              {
                     log_new =  log_previous.deepClone(); //调用深克隆方法创建克隆对象                  
              }
              catch(Exception e)
              {
                     System.err.println("克隆失败!");
              }
              //比较周报
              System.out.println("周报是否相同? " + (log_previous ==  log_new));
              //比较附件
              System.out.println("附件是否相同? " +  (log_previous.getAttachment() == log_new.getAttachment()));
       }
}

编译并运行程序,输出结果如下:

 周报是否相同?  false
附件是否相同?  false

从输出结果可以看出,由于使用了深克隆技术,附件对象也得以复制,因此用“==”比较原型对象的附件和克隆对象的附件时输出结果均为false。深克隆技术实现了原型对象和克隆对象的完全独立,对任意克隆对象的修改都不会给其他对象产生影响,是一种更为理想的克隆实现方式。

 扩展
Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。


6 、原型管理器的引入和实现


原型管理器(Prototype Manager)是将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。在原型管理器中针对抽象原型类进行编程,以便扩展。其结构如图所示:

下面通过模拟一个简单的公文管理器来介绍原型管理器的设计与实现:

Sunny软件公司在日常办公中有许多公文需要创建、递交和审批,例如《可行性分析报告》、《立项建议书》、《软件需求规格说明书》、《项目进展报告》等,为了提高工作效率,在OA系统中为各类公文均创建了模板,用户可以通过这些模板快速创建新的公文,这些公文模板需要统一进行管理,系统根据用户请求的不同生成不同的新公文。

我们使用带原型管理器的原型模式实现公文管理器的设计,其结构如图所示:

 以下是实现该功能的一些核心代码,考虑到代码的可读性,我们对所有的类都进行了简化:


import java.util.*;

//抽象公文接口,也可定义为抽象类,提供clone()方法的实现,将业务方法声明为抽象方法
interface OfficialDocument extends  Cloneable
{
       public  OfficialDocument clone();
       public  void display();
}

//可行性分析报告(Feasibility Analysis Report)类
class FAR implements OfficialDocument
{
       public  OfficialDocument clone()
      {
              OfficialDocument  far = null;
              try
              {
                     far  = (OfficialDocument)super.clone();
              }
              catch(CloneNotSupportedException  e)
              {
                      System.out.println("不支持复制!");
              }
              return  far;
       }

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

//软件需求规格说明书(Software Requirements Specification)类
class SRS implements OfficialDocument
{
       public  OfficialDocument clone()
       {
              OfficialDocument  srs = null;
              try
              {
                     srs  = (OfficialDocument)super.clone();
              }
              catch(CloneNotSupportedException  e)
              { 
                     System.out.println("不支持复制!");
              }
              return  srs;
       }

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

//原型管理器(使用饿汉式单例实现)
class  PrototypeManager
{
       //定义一个Hashtable,用于存储原型对象
       private Hashtable ht=new Hashtable();
       private static PrototypeManager pm =  new PrototypeManager();

       //为Hashtable增加公文对象   
     private  PrototypeManager()
     {
              ht.put("far",new  FAR());
              ht.put("srs",new  SRS());               
     }

     //增加新的公文对象
       public void addOfficialDocument(String  key,OfficialDocument doc)
       {
              ht.put(key,doc);
       }

       //通过浅克隆获取新的公文对象
       public OfficialDocument  getOfficialDocument(String key)
       {
              return  ((OfficialDocument)ht.get(key)).clone();
       }

       public static PrototypeManager  getPrototypeManager()
       {
              return pm;
       }
}

客户端代码如下所示:


class Client
{
       public  static void main(String args[])
       {
              //获取原型管理器对象
              PrototypeManager pm =  PrototypeManager.getPrototypeManager();  

              OfficialDocument  doc1,doc2,doc3,doc4;

              doc1  = pm.getOfficialDocument("far");
              doc1.display();
              doc2  = pm.getOfficialDocument("far");
              doc2.display();
              System.out.println(doc1  == doc2);

              doc3  = pm.getOfficialDocument("srs");
              doc3.display();
              doc4  = pm.getOfficialDocument("srs");
              doc4.display();
              System.out.println(doc3  == doc4);
       }
}

 编译并运行程序,输出结果如下:

 《可行性分析报告》
《可行性分析报告》
false
《软件需求规格说明书》
《软件需求规格说明书》
false

PrototypeManager中定义了一个Hashtable类型的集合对象,使用“键值对”来存储原型对象,客户端可以通过Key(如“far”或“srs”)来获取对应原型对象的克隆对象。PrototypeManager类提供了类似工厂方法的getOfficialDocument()方法用于返回一个克隆对象。在本实例代码中,我们将PrototypeManager设计为单例类,使用饿汉式单例实现,确保系统中有且仅有一个PrototypeManager对象,有利于节省系统资源,并可以更好地对原型管理器对象进行控制。

7、原型模式总结

      原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的典型应用,下面对该模式的使用效果和适用情况进行简单的总结。

1.主要优点

      原型模式的主要优点如下:

(1) 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。

(2) 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。

(3) 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。

(4) 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。

2.主要缺点

      原型模式的主要缺点如下:

(1) 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。

(2) 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。

3.适用场景

        在以下情况下可以考虑使用原型模式:

(1) 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。

(2) 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。

(3) 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。


下一篇预告:建造者模式



欢迎关注,欢迎投稿。


点击下方原文链接,可以订阅自定义View系列专栏。





举报 | 1楼 回复