1、什么是原型模式?

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。

2、为什么要使用原型模式?

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。

3、例子

3.1、简历(简单)

以简历为例,假设要求有一个简历类,要有姓名、性别、年龄和工作年限,一式三份。
PS:接下来我会以版本的演进来跟你说明原型模式

Version1(重复实例化)

按照常理,设计一个简历类,重复实例化3次即可。

  1. public class Main {
  2. public static void main(String[] args) {
  3. Resume a = new Resume("小A");
  4. a.SetPersonalInfo("男", "29");
  5. a.SetWorkExperience("1998-2001", "XX公司");
  6. System.out.println(a);
  7. Resume b = new Resume("小A");
  8. b.SetPersonalInfo("男", "29");
  9. b.SetWorkExperience("1997-2002", "XX公司");
  10. System.out.println(b);
  11. Resume c = new Resume("小A");
  12. c.SetPersonalInfo("男", "29");
  13. c.SetWorkExperience("1996-2003", "XX公司");
  14. System.out.println(c);
  15. }
  16. }
  17. class Resume {
  18. private String name;
  19. private String sex;
  20. private String age;
  21. private String timeArea;
  22. private String company;
  23. public Resume(String name) {
  24. this.name = name;
  25. }
  26. //设置个人信息
  27. public void SetPersonalInfo(String sex, String age) {
  28. this.sex = sex;
  29. this.age = age;
  30. }
  31. //设置工作经历
  32. public void SetWorkExperience(String timeArea, String company) {
  33. this.timeArea = timeArea;
  34. this.company = company;
  35. }
  36. //输出简历的信息
  37. @Override
  38. public String toString() {
  39. return "Resume{" +
  40. "name='" + name + '\'' +
  41. ", sex='" + sex + '\'' +
  42. ", age='" + age + '\'' +
  43. ", timeArea='" + timeArea + '\'' +
  44. ", company='" + company + '\'' +
  45. '}';
  46. }
  47. }

image.png

Version2 (通用原型模式)

在 version1 中三份简历需要三次实例化,这样的客户端代码写起来是很麻烦的,如果要二十份,你就需要二十次实例化。并且,如果我希望单独修改其中的N份简历,比如98年改成99年,那就要改N次。

为了解决这个问题,接下来我们以典型的原型模式来解决这个问题。

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

  1. public class Main {
  2. public static void main(String[] args) {
  3. Resume resume = new Resume();
  4. resume.setName("小A");
  5. resume.setAge("27");
  6. resume.setSex("男");
  7. resume.setTimeArea("2002-2005");
  8. resume.setCompany("君合律师事务所");
  9. Resume resume1 = (Resume) resume.clone();
  10. Resume resume2 = (Resume) resume.clone();
  11. Resume resume3 = (Resume) resume.clone();
  12. Resume resume4 = (Resume) resume.clone();
  13. Resume resume5 = (Resume) resume.clone();
  14. System.out.println(resume);
  15. System.out.println(resume1);
  16. System.out.println(resume2);
  17. System.out.println(resume3);
  18. System.out.println(resume4);
  19. System.out.println(resume5);
  20. }
  21. }
  22. /**
  23. * 抽象原型类
  24. */
  25. abstract class Prototype {
  26. @Override
  27. public abstract Prototype clone();
  28. }
  29. /**
  30. * 具体原型类
  31. */
  32. class Resume extends Prototype {
  33. private String name;
  34. private String sex;
  35. private String age;
  36. private String timeArea;
  37. private String company;
  38. public String getName() {
  39. return name;
  40. }
  41. public void setName(String name) {
  42. this.name = name;
  43. }
  44. public String getSex() {
  45. return sex;
  46. }
  47. public void setSex(String sex) {
  48. this.sex = sex;
  49. }
  50. public String getAge() {
  51. return age;
  52. }
  53. public void setAge(String age) {
  54. this.age = age;
  55. }
  56. public String getTimeArea() {
  57. return timeArea;
  58. }
  59. public void setTimeArea(String timeArea) {
  60. this.timeArea = timeArea;
  61. }
  62. public String getCompany() {
  63. return company;
  64. }
  65. public void setCompany(String company) {
  66. this.company = company;
  67. }
  68. //输出简历的信息
  69. @Override
  70. public String toString() {
  71. return "Resume{" +
  72. "name='" + name + '\'' +
  73. ", sex='" + sex + '\'' +
  74. ", age='" + age + '\'' +
  75. ", timeArea='" + timeArea + '\'' +
  76. ", company='" + company + '\'' +
  77. '}';
  78. }
  79. @Override
  80. public Prototype clone() {
  81. Resume resume = new Resume();
  82. resume.setName(this.name);
  83. resume.setAge(this.age);
  84. resume.setSex(this.sex);
  85. resume.setTimeArea(this.timeArea);
  86. resume.setCompany(this.company);
  87. return resume;
  88. }
  89. }

Version3(java 的原型模式)

通过 Version2 的实现方式,你应该已经理解了原型模式。下面我以 Version3 这个版本来说明在 Java 中如何实现原型模式。

在 Java 中,所有的类都继承自 java.lang.Object,事实上,Object 类提供一个了 protect 的 clone() 方法,可以将一个Java对象复制一份。因此在Java中可以直接使用 Object 提供的 clone() 方法来实现对象的克隆,Java语言中的原型模式实现很简单,就是实现 Java 提供的 Cloneable 接口即可。
PS:在 Version 4/5 ,我会解释什么是浅拷贝和深拷贝

  1. public class Main {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. Resume resume = new Resume();
  4. resume.setName("小A");
  5. resume.setAge("27");
  6. resume.setSex("男");
  7. resume.setTimeArea("2002-2005");
  8. resume.setCompany("君合律师事务所");
  9. Resume resume1 = (Resume) resume.clone();
  10. Resume resume2 = (Resume) resume.clone();
  11. Resume resume3 = (Resume) resume.clone();
  12. Resume resume4 = (Resume) resume.clone();
  13. Resume resume5 = (Resume) resume.clone();
  14. System.out.println(resume);
  15. System.out.println(resume1);
  16. System.out.println(resume2);
  17. System.out.println(resume3);
  18. System.out.println(resume4);
  19. System.out.println(resume5);
  20. }
  21. }
  22. /**
  23. * 简历类,实现 Cloneable 接口
  24. */
  25. class Resume implements Cloneable {
  26. private String name;
  27. private String sex;
  28. private String age;
  29. private String timeArea;
  30. private String company;
  31. public String getName() {
  32. return name;
  33. }
  34. public void setName(String name) {
  35. this.name = name;
  36. }
  37. public String getSex() {
  38. return sex;
  39. }
  40. public void setSex(String sex) {
  41. this.sex = sex;
  42. }
  43. public String getAge() {
  44. return age;
  45. }
  46. public void setAge(String age) {
  47. this.age = age;
  48. }
  49. public String getTimeArea() {
  50. return timeArea;
  51. }
  52. public void setTimeArea(String timeArea) {
  53. this.timeArea = timeArea;
  54. }
  55. public String getCompany() {
  56. return company;
  57. }
  58. public void setCompany(String company) {
  59. this.company = company;
  60. }
  61. //输出简历的信息
  62. @Override
  63. public String toString() {
  64. return "Resume{" +
  65. "name='" + name + '\'' +
  66. ", sex='" + sex + '\'' +
  67. ", age='" + age + '\'' +
  68. ", timeArea='" + timeArea + '\'' +
  69. ", company='" + company + '\'' +
  70. '}';
  71. }
  72. @Override
  73. public Object clone() throws CloneNotSupportedException {
  74. return super.clone();
  75. }
  76. }

Version4(原型模式:浅拷贝)

在 Version4 中,我为简历类新增了一个附件类作为属性,以此来解释什么是浅拷贝。

  1. public class Main {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. Attachment attachment = new Attachment("介绍信:XXXX", "blog.xxx.com");
  4. Resume resume = new Resume();
  5. resume.setName("小A");
  6. resume.setAge("27");
  7. resume.setSex("男");
  8. resume.setTimeArea("2002-2005");
  9. resume.setCompany("君合律师事务所");
  10. resume.setAttachment(attachment);
  11. Resume resumeCopy = (Resume) resume.clone();
  12. System.out.println(resume);
  13. System.out.println(resumeCopy);
  14. if (resume == resumeCopy) {
  15. System.out.println("两份简历是同一个对象");
  16. } else {
  17. System.out.println("两份简历不是同一个对象");
  18. }
  19. if (resume.getAttachment() == resumeCopy.getAttachment()) {
  20. System.out.println("两份简历中的附件是同一个对象");
  21. } else {
  22. System.out.println("两份简历中的附件不是同一个对象");
  23. }
  24. }
  25. }
  26. /**
  27. * 简历附件类
  28. */
  29. class Attachment {
  30. // 介绍信
  31. private String letterIntroduction;
  32. // 博客地址
  33. private String blogAddress;
  34. public Attachment(String letterIntroduction, String blogAddress) {
  35. this.letterIntroduction = letterIntroduction;
  36. this.blogAddress = blogAddress;
  37. }
  38. public String getLetterIntroduction() {
  39. return letterIntroduction;
  40. }
  41. public void setLetterIntroduction(String letterIntroduction) {
  42. this.letterIntroduction = letterIntroduction;
  43. }
  44. public String getBlogAddress() {
  45. return blogAddress;
  46. }
  47. public void setBlogAddress(String blogAddress) {
  48. this.blogAddress = blogAddress;
  49. }
  50. @Override
  51. public String toString() {
  52. return "Attachment{" +
  53. "letterIntroduction='" + letterIntroduction + '\'' +
  54. ", blogAddress='" + blogAddress + '\'' +
  55. '}';
  56. }
  57. }
  58. /**
  59. * 简历类,实现 Cloneable 接口
  60. */
  61. class Resume implements Cloneable {
  62. private String name;
  63. private String sex;
  64. private String age;
  65. private String timeArea;
  66. private String company;
  67. private Attachment attachment;
  68. public String getName() {
  69. return name;
  70. }
  71. public void setName(String name) {
  72. this.name = name;
  73. }
  74. public String getSex() {
  75. return sex;
  76. }
  77. public void setSex(String sex) {
  78. this.sex = sex;
  79. }
  80. public String getAge() {
  81. return age;
  82. }
  83. public void setAge(String age) {
  84. this.age = age;
  85. }
  86. public String getTimeArea() {
  87. return timeArea;
  88. }
  89. public void setTimeArea(String timeArea) {
  90. this.timeArea = timeArea;
  91. }
  92. public String getCompany() {
  93. return company;
  94. }
  95. public void setCompany(String company) {
  96. this.company = company;
  97. }
  98. public Attachment getAttachment() {
  99. return attachment;
  100. }
  101. public void setAttachment(Attachment attachment) {
  102. this.attachment = attachment;
  103. }
  104. //输出简历的信息
  105. @Override
  106. public String toString() {
  107. return "Resume{" +
  108. "name='" + name + '\'' +
  109. ", sex='" + sex + '\'' +
  110. ", age='" + age + '\'' +
  111. ", timeArea='" + timeArea + '\'' +
  112. ", company='" + company + '\'' +
  113. ", attachment=" + attachment +
  114. '}';
  115. }
  116. @Override
  117. public Object clone() throws CloneNotSupportedException {
  118. return super.clone();
  119. }
  120. }

image.png

根据控制台输出的信息,可以知道目前按照 Version4 的方式实现的原型模式有问题。clone 简历类时,简历类中的附件类并没有被完全复制,而是拷贝了一份引用而已,实际上指向的还是同一个附件。
PS:Java 的基础知识

这就是浅拷贝。

Version5(原型模式:深拷贝)

为了解决 Version4 的简历附件问题(浅拷贝),所以有了 Version5 ,我会以这个版本来解释什么是深拷贝。

要实现对简历类的深拷贝,有两种方案:
1)拷贝所有的引用类型属性
直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。

2)序列化
先将对象序列化,然后再反序列化成新的对象。

public Object deepCopy(Object object) {
  ByteArrayOutputStream bo = new ByteArrayOutputStream();
  ObjectOutputStream oo = new ObjectOutputStream(bo);
  oo.writeObject(object);

  ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
  ObjectInputStream oi = new ObjectInputStream(bi);

  return oi.readObject();
}

Version5 采用的是第1种方案。先让简历类中的所有引用类型属性类去实现 clonable 接口,并在简历类中的 clone 方法将该引用类型的属性进行替换。

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Attachment attachment = new Attachment("介绍信:XXXX", "blog.xxx.com");

        Resume resume = new Resume();
        resume.setName("小A");
        resume.setAge("27");
        resume.setSex("男");
        resume.setTimeArea("2002-2005");
        resume.setCompany("君合律师事务所");
        resume.setAttachment(attachment);

        Resume resumeCopy = (Resume) resume.clone();

        System.out.println(resume);
        System.out.println(resumeCopy);

        if (resume == resumeCopy) {
            System.out.println("两份简历是同一个对象");
        } else {
            System.out.println("两份简历不是同一个对象");
        }

        if (resume.getAttachment() == resumeCopy.getAttachment()) {
            System.out.println("两份简历中的附件是同一个对象");
        } else {
            System.out.println("两份简历中的附件不是同一个对象");
        }
    }
}

/**
 * 简历附件类
 */
class Attachment implements Cloneable {
    // 介绍信
    private String letterIntroduction;

    // 博客地址
    private String blogAddress;

    public Attachment(String letterIntroduction, String blogAddress) {
        this.letterIntroduction = letterIntroduction;
        this.blogAddress = blogAddress;
    }

    public String getLetterIntroduction() {
        return letterIntroduction;
    }

    public void setLetterIntroduction(String letterIntroduction) {
        this.letterIntroduction = letterIntroduction;
    }

    public String getBlogAddress() {
        return blogAddress;
    }

    public void setBlogAddress(String blogAddress) {
        this.blogAddress = blogAddress;
    }

    @Override
    public String toString() {
        return "Attachment{" +
                "letterIntroduction='" + letterIntroduction + '\'' +
                ", blogAddress='" + blogAddress + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

/**
 * 简历类,实现 Cloneable 接口
 */
class Resume implements Cloneable {
    private String name;
    private String sex;
    private String age;
    private String timeArea;
    private String company;
    private Attachment attachment;

    public String getName() {
        return name;
    }

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

    public String getSex() {
        return sex;
    }

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

    public String getAge() {
        return age;
    }

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

    public String getTimeArea() {
        return timeArea;
    }

    public void setTimeArea(String timeArea) {
        this.timeArea = timeArea;
    }

    public String getCompany() {
        return company;
    }

    public void setCompany(String company) {
        this.company = company;
    }

    public Attachment getAttachment() {
        return attachment;
    }

    public void setAttachment(Attachment attachment) {
        this.attachment = attachment;
    }

    //输出简历的信息
    @Override
    public String toString() {
        return "Resume{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age='" + age + '\'' +
                ", timeArea='" + timeArea + '\'' +
                ", company='" + company + '\'' +
                ", attachment=" + attachment +
                '}';
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Resume obj = (Resume) super.clone();
        obj.setAttachment((Attachment) this.attachment.clone());
        return obj;
    }
}

image.png

根据控制台输出的信息,可以看到 Version4 中的问题已经解决了,不同的简历中携带的附件已经不是指向同一个对象了。

4、总结

4.1、原型模式的优缺点

1)优点
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,可以以达到节省创建时间的目的。

并且,客户端如果需要修改部分对象,可以先改一个然后再 clone ,避免了一个一个对象修改的麻烦。

2)缺点
待续

4.2、原型模式的两种实现方式

原型模式有两种实现方法,深拷贝和浅拷贝。

浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。

如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。除非像从数据库中加载 10 万条数据并构建散列表索引,操作非常耗时,这种情况下比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。

4.3、关于原型模式的一些问题

1)何为“对象的创建成本比较大”?
实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。

但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

对编程来说,简单的复制粘贴极有可能造成重复代码的灾难。