Java设计模式系列五(原型模式)

原创
2019/04/14 07:52
阅读数 47

Java设计模式系列文章:

  1. Java设计模式系列一(前言)

  2. Java设计模式系列二(单例模式)

  3. Java设计模式系列三(工厂方法模式)

  4. Java设计模式系列四(抽象工厂模式)



本篇将和大家详解Java设计模式之——原型模式。


什么是原型模式

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


原型模式是非常常用、同时又非常简单的设计模式。比如,我们经常看电影或者电视剧中的替身一样,原有对象就是真人,而替身就相当于通过真人克隆出来的人,他们具有一样的外形、容貌、言行举止等等,真人和替身都是独立的对象,如果替身被杀死了,对真人并没有影响。


还有一个生活中最常见的类似的例子就是Ctrl+C和Ctrl+V,通过对原有对象的复制和粘贴,我们可以创建大量的具有相同数据的对象,在不改变原有的对象的情况下,可以对创建的新的对象进行数据相关的修改。


原型模式三个角色

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

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

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


原型模式的特点

  1. 由原型对象自身创建目标对象,也就是说,对象创建这一动作发自原型对象本身。

  2. 目标对象是原型对象的一个克隆,也就是说,通过Prototype模式创建的对象,不仅仅与原型对象具有相同的结构,还与原型对象具有相同的值。

  3. 根据对象克隆深度层次的不同,有浅度克隆和深度克隆区分。


下面来通过代码详细讲解原型模式,我们还是以手机为例,假如原型对象手机类有品牌、颜色、保修期等属性,我们只需要通过实现了Cloneable 接口,重写Object.的clone() 方法,就可复制出结构一样的手机A、手机B、C....

UML图如下:


原型模式代码实例

1、创建具体原型类,实现Cloneable类,重写clone()方法

package com.weiya.prototype;
import lombok.Data;
/** * Cloneable是标记型的接口, * 它们内部都没有方法和属性, * 实现 Cloneable来表示该对象能被克隆,能使用Object.clone()方法。 * 如果没有实现 Cloneable的类对象调用clone()就会抛出CloneNotSupportedException */@Datapublic class Phone implements Cloneable{ // 品牌 private String brand; // 颜色 private String color; // 保修年限 private int guaranteeYear;
public Phone clone(){ try { System.out.println("克隆对象完成!"); return (Phone) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } }}


2、客户端测试原型模式,调用原型对象的clone方法,可以克隆出多个目标对象,他们与原型对象结构,数据值都一样。

package com.weiya.prototype;
/** * <p class="detail"> * 功能: 测试原型模式 * </p> * * @author BaoWeiwei * @ClassName Client. * @Version V1.0. * @date 2019.04.13 20:16:47 */public class Client { public static void main(String[] args) { Phone phone = new Phone(); phone.setBrand("华为"); phone.setColor("白色"); phone.setGuaranteeYear(1);
Phone phone2 = phone.clone();
System.out.println(phone == phone2); System.out.println("原型对象:"+phone.toString()); System.out.println("克隆对象:"+phone2.toString()); }}

运行结果:

通过结果我们可以看出,克隆出的对象的独立的对象,和原型对象指向不同的对象引用地址,只是数据相同。


然后我们修改克隆对象的值,看看原型对象是否有变化:

可以发现,修改克隆对象的值,并不会对原型对象有任何影响。


那么,是不是真的不会改变原型对象的值呢?如果原型对象里有数组,引用对象等属性呢?这里就涉及到上文中提到的原型模式的第三个特点,浅度克隆(浅复制)和深度克隆(深复制)。下面就来说一下两者的区别。


  • 浅克隆(shallow clone),浅克隆是指拷贝对象时仅仅copy对象本身和对象中的基本变量,而不拷贝对象包含的引用指向的对象。换句话说,Object类提供的clone只是拷贝原型对象本对象,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址。

  • 深克隆(deep clone),不仅copy对象本身,而且copy对象包含的引用指向的所有对象。换句话说,就是被克隆对象的所有变量都含有与原型对象相同的值,除去那些引用对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深克隆把要复制的对象所引用的对象也复制一遍。



浅克隆代码

1、编写原型对象,加入引用对象属性

package com.weiya.prototype;
import lombok.Data;
/** * Cloneable是标记型的接口, * 它们内部都没有方法和属性, * 实现 Cloneable来表示该对象能被克隆,能使用Object.clone()方法。 * 如果没有实现 Cloneable的类对象调用clone()就会抛出CloneNotSupportedException */@Datapublic class Phone2 implements Cloneable{ // 品牌 private String brand; // 颜色 private String color; // 保修年限 private int guaranteeYear; // 手机工厂(引用对象) private PhoneFactory phoneFactory;
    // 浅克隆 public Phone2 clone(){ try { System.out.println("克隆对象完成!"); return (Phone2) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } }}


2、浅克隆测试

package com.weiya.prototype;
/** * <p class="detail"> * 功能:浅克隆测试类 * </p> * * @author BaoWeiwei * @ClassName Client shallow test. * @Version V1.0. * @date 2019.04.13 21:37:39 */public class ClientShallowTest { public static void main(String[] args) { Phone2 phone = new Phone2(); phone.setBrand("华为"); phone.setColor("白色"); phone.setGuaranteeYear(1); PhoneFactory factory = new PhoneFactory(); factory.setAddress("杭州"); phone.setPhoneFactory(factory);
System.out.println("原型对象:"+phone.toString()); System.out.println("原型对象手机工厂地址:"+phone.getPhoneFactory().getAddress());
Phone2 phone2 = phone.clone();
System.out.println(phone == phone2); System.out.println("克隆对象:"+phone2.toString()); System.out.println("克隆对象手机工厂地址:"+phone2.getPhoneFactory().getAddress());
phone2.setBrand("小米"); phone2.getPhoneFactory().setAddress("上海"); System.out.println(phone == phone2); System.out.println("原型对象:"+phone.toString()); System.out.println("原型对象手机工厂地址:"+phone.getPhoneFactory().getAddress()); System.out.println("克隆对象:"+phone2.toString()); System.out.println("克隆对象手机工厂地址:"+phone2.getPhoneFactory().getAddress()); }}


查看运行结果:


通过测试结果,我们可以发现,原型对象的成员变量是值类型的时候,克隆对象都会复制指向新的内部元素地址,但原型对象成员变量有数组、引用对象等类型,浅克隆都不会拷贝,还是指向原型对象的内部元素地址。通俗点说,就是原型对象和克隆对象的引用对象成员变量指向的同一个地址,修改了克隆对象的引用对象的值,会同时改变原型对象的引用对象值。所以在原型对象有数组、引用对象等成员变量是时,使用浅克隆方式是有缺陷的。


深克隆代码

可以理解成解决浅克隆问题的解决方案。深克隆即是把原型对象内部的数组、引用对象等手动拷贝,指向不同的对象地址。

深克隆有两种实现方法,一种是对象实现Cloneable,另一种是实用二进制流实现,需要对象实现Serializable。下面看代码实例。


1、原有对象的成员变量是引用对象,先编写引用对象的clone方法

package com.weiya.prototype;
import lombok.Data;
import java.io.*;
@Datapublic class PhoneFactory implements Serializable,Cloneable { // 工厂名称 private String name; // 工厂地址 private String address;
// 实现Cloneable写法 public PhoneFactory clone() { try { return (PhoneFactory) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } }
// 二进制流写法,需要对象实现序列化 public PhoneFactory 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 (PhoneFactory) ois.readObject(); }}


2、编写原型对象

package com.weiya.prototype;
import lombok.Data;
import java.io.IOException;
/** * Cloneable是标记型的接口, * 它们内部都没有方法和属性, * 实现 Cloneable来表示该对象能被克隆,能使用Object.clone()方法。 * 如果没有实现 Cloneable的类对象调用clone()就会抛出CloneNotSupportedException */@Datapublic class Phone3 implements Cloneable{ // 品牌 private String brand; // 颜色 private String color; // 保修年限 private int guaranteeYear; // 手机工厂(引用对象) private PhoneFactory phoneFactory;
// 深克隆 public Phone3 clone(){ Phone3 phone3 = null; try { System.out.println("克隆对象完成!"); phone3 = (Phone3) super.clone();// phone3.setPhoneFactory(this.phoneFactory.clone()); phone3.phoneFactory = this.phoneFactory.deepClone(); return phone3; } catch (CloneNotSupportedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return phone3; }}


3、深克隆测试

package com.weiya.prototype;
/** * <p class="detail"> * 功能:浅克隆测试类 * </p> * * @author BaoWeiwei * @ClassName Client shallow test. * @Version V1.0. * @date 2019.04.13 21:37:39 */public class ClientDeepTest { public static void main(String[] args) { Phone3 phone = new Phone3(); phone.setBrand("华为"); phone.setColor("白色"); phone.setGuaranteeYear(1); PhoneFactory factory = new PhoneFactory(); factory.setAddress("杭州"); phone.setPhoneFactory(factory);
System.out.println("原型对象:"+phone.toString()); System.out.println("原型对象手机工厂地址:"+phone.getPhoneFactory().getAddress());
Phone3 phone2 = phone.clone();
System.out.println(phone == phone2); System.out.println("克隆对象:"+phone2.toString()); System.out.println("克隆对象手机工厂地址:"+phone2.getPhoneFactory().getAddress());
phone2.setBrand("小米"); phone2.getPhoneFactory().setAddress("上海"); System.out.println(phone == phone2); System.out.println("原型对象:"+phone.toString()); System.out.println("原型对象手机工厂地址:"+phone.getPhoneFactory().getAddress()); System.out.println("克隆对象:"+phone2.toString()); System.out.println("克隆对象手机工厂地址:"+phone2.getPhoneFactory().getAddress()); }}


查看运行结果:

可以看出,使用深克隆方式,原型对象除了对象本身被复制外,对象所包含的所有的成员变量也被复制。深克隆技术实现了原型对象和克隆对象的完全独立,对任意克隆对象的修改都不会给其他对象产生影响,是一种更为理想的克隆实现方式。


原型模式的扩展

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


我们以王者荣耀来举例,除了稳住别浪以外,我们还需要推掉对方水晶才能获胜,我们用什么推掉对方水晶呢?当然是英雄啦,像作者以前法师贼6,可攻可守可辅助,经常获得妹子各种花式仰慕,就差以身相许了,哈哈,扯远了。回到正题,说到英雄,我们都知道王者的英雄有战士、法师、射手、坦克、辅助之分,每一类英雄都有一些共同的属性,那是不是每次王者团队在创建某一类英雄时,都要重新一个一个的创建呢,显然是不用的,比如法师,他们大多数有共同的属性,那么就可以把这些共同的属性变成一个法师模板,每次根据请求不同创建新的英雄。


代码如下:

1、定义公用接口和实现类

package com.weiya.prototype;
/** * <p class="detail"> * 功能://抽象法师接口,也可定义为抽象类,提供clone()方法的实现,将业务方法声明为抽象方法 * </p> * * @author Baoweiwei * @ClassName Apc. * @Version V1.0. * @date 2019.04.13 23:21:18 */public interface APC extends Cloneable{
// clone共同属性 public APC clone();
// 创建属性 public void createHero();}


package com.weiya.prototype;
public class ZhangLiang implements APC { public APC clone() { APC zhangLiang = null; try { zhangLiang = (APC) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return zhangLiang; }
public void createHero() { System.out.println("创建了法师张良..."); }}


package com.weiya.prototype;
public class ZhouYu implements APC { public APC clone() { APC zhouYu = null; try { zhouYu = (APC) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return zhouYu; }
public void createHero() { System.out.println("创建了法师周瑜..."); }}


2、编写原型管理器

package com.weiya.prototype;
import java.util.Hashtable;
/** * <p class="detail"> * 功能:原型管理器,为方便使用饿汉式单例模式实现 * </p> * * @author BaoWeiwei * @ClassName Prototype manager. * @Version V1.0. * @date 2019.04.13 23:31:17 */public class PrototypeManager {
//定义一个Hashtable,用于存储原型对象 private Hashtable ht = new Hashtable(); private static PrototypeManager pm = new PrototypeManager();

//为Hashtable增加法师对象 private PrototypeManager(){ ht.put("zhangliang", new ZhangLiang()); ht.put("zhouyu", new ZhouYu()); } //增加新的法师对象 public void addAPC(String key, APC apc){ ht.put(key, apc); } //通过浅克隆获取新的法师对象 public APC getAPC(String key){ return ((APC) ht.get(key)).clone(); }
public static PrototypeManager getPrototypeManager(){ return pm; }}


3、测试原型管理器

package com.weiya.prototype;
/** * <p class="detail"> * 功能:原型管理器测试 * </p> * * @author BaoWeiwei * @ClassName Prototype manager test. * @Version V1.0. * @date 2019.04.13 23:37:05 */public class PrototypeManagerTest { public static void main(String[] args) { //获取原型管理器对象 PrototypeManager pm = PrototypeManager.getPrototypeManager(); APC ap1, ap2, ap3, ap4; ap1 = pm.getAPC("zhangliang"); ap1.createHero(); ap2 = pm.getAPC("zhangliang"); ap1.createHero(); System.out.println(ap1 == ap2);
ap3 = pm.getAPC("zhouyu"); ap3.createHero(); ap4 = pm.getAPC("zhouyu"); ap4.createHero(); System.out.println(ap3 == ap4); }}


查看运行结果:


通过示例与运行结果,我们可以看出,在PrototypeManager中定义了一个Hashtable类型的集合对象,使用“键值对”来存储原型对象,客户端可以通过Key(如“zhouyu”或“zhangliang”)来获取对应原型对象的克隆对象。PrototypeManager类提供了类似工厂方法的getAPC()方法用于返回一个克隆对象。在本实例代码中,我们将PrototypeManager设计为单例类,使用饿汉式单例实现,如果有对单例模式不懂的,可以参考我的另一篇文章:Java设计模式系列二(单例模式)


总结

原型模式使用场景

  1. 系统要保存对象状态的,而对象的状态改变很小。

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

  3. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。 

  4. 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。需要避免使用分层次的工厂类来创建分层次的对象,并且类的对象就只用一个或很少的组合状态!


原型模式优点:

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

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

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

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


原型模式缺点:

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

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


至此,原型模式我们已经讲解完了,内容有点多,但是其实很好理解,也很简单,项目中也经常会用到,只要用心人生处处是风景。


如果觉得本文有用,请推荐给更多有需要的人,谢谢!如果发现问题,欢迎留言,请随时批评改正,谢谢!

Java设计模式系列文章:

  1. Java设计模式系列一(前言)

  2. Java设计模式系列二(单例模式)

  3. Java设计模式系列三(工厂方法模式)

  4. Java设计模式系列四(抽象工厂模式)


本文分享自微信公众号 - 码之初(ma_zhichu)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部