设计模式之原型模式
2017-04-09 17:49
225 查看
1 原型模式概念
1.1 介绍
原型模式是一个创建型的模式。原型二字表明了改模式应该有一个样板实例,用户从这个样板对象中复制一个内部属性一致的对象,这个过程也就是我们称的“克隆”。被复制的实例就是我们所称的“原型”,这个原型是可定制的。原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可使程序运行更高效。1.2 定义
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。1.3 使用场景
(1)类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等,通过原型拷贝避免这些消耗。(2)通过new产生的一个对象需要非常繁琐的数据准备或者权限,这时可以使用原型模式。
(3)一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝。
2 原型模式UML类图(通用)
原型模式主要用于对象的复制,它的核心是就是类图中的原型类Prototype。Prototype类需要具备以下两个条件:
(1)实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。
(2)重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此Prototype类需要将clone方法的作用域修改为public类型。
2 简单实现示范
2.1 原型
/* * 书本类型,扮演的是ConcretePrototype角色,而Cloneable扮演Prototype角色 */ public class Book implements Cloneable { private String title;// 标题 private ArrayList<String> image = new ArrayList<String>();// 图片名列表 public Book() { super(); } /** * 重写拷贝方法 */ @Override protected Book clone() { try { Book book = (Book) super.clone(); return book; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public ArrayList<String> getImage() { return image; } public void addImage(String img) { this.image.add(img); } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } /** * 打印内容 */ public void showBook() { System.out.println("----------------------Start----------------------"); System.out.println("title:" + title); for (String img : image) { System.out.println("image name:" + img); } System.out.println("----------------------End----------------------"); } }
2.2 Client端1
(1)代码public class Client { public static void main(String[] args) { // 1.构建书本对象 Book book1 = new Book(); // 2.编辑书本,添加图片 book1.setTitle("书1"); book1.addImage("图1"); book1.showBook(); // 以原型文档为原型,拷贝一份副本 Book book2 = (Book) book1.clone(); book2.showBook(); // 修改图书副本,不会影响原始书本 book2.setTitle("书2"); book2.showBook(); // 再次打印原始书本 book1.showBook(); } }
(2)结果:
----------------------Start---------------------- title:书1 image name:图1 ----------------------End---------------------- ----------------------Start---------------------- title:书1 image name:图1 ----------------------End---------------------- ----------------------Start---------------------- title:书2 image name:图1 ----------------------End---------------------- ----------------------Start---------------------- title:书1 image name:图1 ----------------------End----------------------
(3)解释:
book2是book的一份拷贝,它和book1的内容是一样的。而book2修改了“标题”-字符串(基本的数据类型),并不影响book1的标题,这就保证了book1的安全性。
2.3 浅拷贝应用
(1)代码:public class Client { public static void main(String[] args) { // 1.构建书本对象 Book book1 = new Book(); // 2.编辑书本,添加图片 book1.setTitle("书1"); book1.addImage("图1"); book1.showBook(); // 以原型文档为原型,拷贝一份副本 Book book2 = (Book) book1.clone(); book2.showBook(); // 修改图书副本,不会影响原始书本 book2.setTitle("书2"); book2.addImage("图2"); book2.showBook(); // 再次打印原始书本 book1.showBook(); } }
(2)结果:
----------------------Start---------------------- title:书1 image name:图1 ----------------------End---------------------- ----------------------Start---------------------- title:书1 image name:图1 ----------------------End---------------------- ----------------------Start---------------------- title:书2 image name:图1 image name:图2 ----------------------End---------------------- ----------------------Start---------------------- title:书1 image name:图1 image name:图2 ----------------------End----------------------
(3)解释:
细心观察发现,最后两个书本内容输出是一致的。引用类型的新对象book2的image只是单纯指向了this.image引用,并没有重新构造一个image对象,然后将原始书本的图片添加到新的image对象中,这样导致book2中的image与原始书本中的是同一个对象。因此,修改其中一个书本的图片,另一个书本也会受到影响。
如何解决?因为Object类的clone方法只会拷贝对象中的基本的数据类型,对于数组、集合、容器对象、引用对象等都不会拷贝;所以采用深拷贝。
2.3 深拷贝应用
(1)代码:/** * 重写拷贝方法 */ @Override protected Book clone() { try { Book book = (Book) super.clone(); // 对image对象也调用clone()函数,进行拷贝 book.image = (ArrayList<String>) this.image.clone(); return book; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; }
(2)结果:
----------------------Start---------------------- title:书1 image name:图1 ----------------------End---------------------- ----------------------Start---------------------- title:书1 image name:图1 ----------------------End---------------------- ----------------------Start---------------------- title:书2 image name:图1 image name:图2 ----------------------End---------------------- ----------------------Start---------------------- title:书1 image name:图1 ----------------------End----------------------
(3)解释:
将book.image指向this.image的一份拷贝,而不是this.image本身,实现了完全的拷贝,这样再互不影响。
3 Android源码中的原型模式
3.1 Intent
Uri uri = Uri.parse("smsto:110"); Intent intent = new Intent(Intent.ACTION_SEND,uri); intent.putExtra("sms_body", "The SMS text"); //克隆 Intent intent2 = (Intent)intent.clone(); startActivity(intent2);
4 总结
4.1 优点
(1)原型模式是在内存中二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量对象时,原型模式可能更好的体现其优点。(2)还有一个重要的用途就是保护性拷贝,也就是对某个对象对外可能是只读的,为了防止外部对这个只读对象的修改,通常可以通过返回一个对象拷贝的形式实现只读的限制。
4.2 缺点
(1)这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,在实际开发中应该注意这个潜在问题。优点是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。(2)通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比通过new操作速度快,只有当通过new构造对象较为耗时或者说成本较高时,通过clone方法才能够获得效率上的提升。
5 注意事项
5.1 构造函数不会被执行
一个实现了Cloneable并重写了clone方法的类A,有一个无参构造或有参构造B,通过new关键字产生了一个对象S,再然后通过S.clone()方式产生了一个新的对象T,那么在对象拷贝时构造函数B是不会被执行的。我们来写一小段程序来说明这个问题。public class Thing implements Cloneable { public Thing(){ System.out.println("构造函数被执行了..."); } @Override public Thing clone(){ Thing thing=null; try { thing = (Thing)super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return thing; } }
然后我们再来写一个Client类,进行对象的拷贝:
public class Client { public static void main(String[] args) { Thing thing = new Thing(); //产生一个对象 Thing cloneThing = thing.clone(); //拷贝一个对象 } }
运行结果如下所示:
构造函数被执行了...
对象拷贝时构造函数确实没有被执行,这点从原理来讲也是可以讲得通的,Object类的clone方法的原理是从内存中(具体地说就是堆内存)以二进制流的方式进行拷贝,重新分配一个内存块,那构造函数没有被执行是对的。
5.2 浅拷贝和深拷贝
在开发中,为减少错误,建议在使用原型模式时尽量使用深拷贝,避免操作副本时影响原始对象的问题。如果是在涉及类的继承时,父类有多个引用的情况就非常复杂,建议的方案是深拷贝和浅拷贝分开实现。5.3 clone与final两个冤家
要使用clone方法,类的成员变量上不要增加final关键字。因为final类型是不允许重赋值的。6 原型模式实战
以下代码是用户在登录之后通过LoginSession的setLoginedUser函数将登录用的信息设置在Session中,setLoginedUser是私有的,外部模块无法调用。(1)Address.java
/** * @author Guan * 用户地址类 */ public class Address { public String city; public String district; public String street; public Address(String city, String district, String street) { this.city = city; this.district = district; this.street = street; } @Override public String toString() { return "Address{" + "city='" + city + '\'' + ", district='" + district + '\'' + ", street='" + street + '\'' + '}'; } }
(2)User.java
/** * @author Guan * 用户实体类 */ public class User { public int age; public String name; public String phoneNum; public Address address; @Override public String toString() { return "User{" + "age=" + age + ", name='" + name + '\'' + ", phoneNum='" + phoneNum + '\'' + ", address=" + address + '}'; } }
(3)LoginSession.java
/** * @author Guan * 登录Session */ public class LoginSession { static LoginSession loginSession = null; private User loginedUser;// 已登录用户 public LoginSession() { } public static LoginSession getLoginSession() { if (loginSession == null) { loginSession = new LoginSession(); } return loginSession; } public User getLoginedUser() { // return loginedUser.clone();// 返回已登录用户的一个拷贝 return loginedUser;// 返回原始已登录用户 } // 只通过私有方法修改用户信息 void setLoginedUser(User loginedUser) { this.loginedUser = loginedUser; } }
(4)Login.java
/** * @author Guan * 登录接口 */ public interface Login { void login(); }
(5)LoginImpl.java
/** * @author Guan * 登录实现类 */ public class LoginImpl implements Login{ public void login() { // 登录到服务器,获取到用户信息 User loginedUser = new User(); // 将服务器返回的完整信息设置给loginedUser对象 loginedUser.age = 22; loginedUser.name = "xiaoguan"; loginedUser.address = new Address("深圳市","宝安区","一路"); // 登录完后将用户信息设置到Session中LoginSession.getLoginSession()中 LoginSession.getLoginSession().setLoginedUser(loginedUser); } }
(6)Client.java
public class Client { public static void main(String[] args) { // 已登录用户 LoginImpl loginImpl = new LoginImpl(); loginImpl.login(); User tempUser = LoginSession.getLoginSession().getLoginedUser(); User user = LoginSession.getLoginSession().getLoginedUser();// 获得已登录的User对象 user.address = new Address("新深圳市","新宝安区","新一路"); // 只能通过私有方法setLoginedUser更新用户信息 LoginSession.getLoginSession().setLoginedUser(user); System.out.println("tempUser:" + tempUser); System.out.println("已登录用户:" + LoginSession.getLoginSession().getLoginedUser()); } }
(7)结果&分析
在用户点击更新按钮时候,直接调用以上代码更新用户地址,在网络请求成功还是未成功情况下用户信息都被修改了,结果打印如下:
(8)优化
所以需要使用原型模式来进行保护性拷贝,使得在任何地方调用getLoginedUser函数获取到的用户对象都是一个拷贝对象,即使客户端不小心修改了这个拷贝对象,也不会影响最初的已登录用户对象,确保了它的安全。修改代码如下:
/** * @author Guan * 用户实体类 */ public class User implements Cloneable{ /** * 拷贝 */ public User clone() { User user = null; try { user = (User) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return user; } }
/** * @author Guan * 登录Session */ public class LoginSession { public User getLoginedUser() { return loginedUser.clone();// 返回已登录用户的一个拷贝 // return loginedUser;// 返回原始已登录用户 } }
结果如下:
7 参考文章与链接
《Android源码设计模式解析与实战》《设计模式之禅》
《Android源码设计模式解析与实战》读书笔记(四)
《 java 原型模式》
相关文章推荐
- 设计模式学习笔记(六)——Prototype原型模式
- 设计模式学习笔记(四)——Prototype原型
- 设计模式就是来解决应变---原型(prototype)
- 设计模式与泡mm的关系之Prototype原型模式及原型模式的再思考
- 设计模式学习笔记(六)——Prototype原型模式
- 设计模式之Prototype(原型)
- (创建模式)设计模式之Prototype(原型)
- 设计模式原型模式(Prototype)- 面馆里的菜单
- 设计模式之Prototype原型设计模式
- 设计模式之原型法(ProtoType)----对象创建型模式
- 设计模式原型模式(Prototype)- 面馆里的菜单
- 设计模式袖珍版 连续转载之 - Prototype(原型)
- C#设计模式之原型(ProtoType)
- C#设计模式之原型(ProtoType)
- 设计模式原型模式(Prototype)- 面馆里的菜单
- 设计模式之Prototype(原型)
- C#设计模式之原型(ProtoType)
- 设计模式之Prototype(原型)
- 设计模式(3)-原型模式(Prototype)
- 设计模式(5)-原型模式(Prototype)