原型(Prototype)模式

原型模式是一种创建型设计模式,允许你通过拷贝现有对象来创建新对象,而不是通过类来创建。它适用于当你需要大量创建相似对象,而只需要修改少量属性的情况。在Java中,实现原型模式可以通过实现Cloneable接口并覆盖clone()方法。音乐编辑器的示例展示了如何使用原型模式来创建和克隆音乐对象,如音符和五线谱,减少系统中类的数量并提高灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原型(Prototype)模式

隶属类别——对象创建型模式


1. 意图

用原型实例创建对象的种类,并且通过拷贝这些原型创建新的对象。

2. 别名

3. 动机

你可以通过定制一个通用的图形编辑器框架和增加一些表示音符、休止符和五线谱的新对象来构造一个乐谱编辑器。这个编译器框架可能有有一个工具选择板用于将这些音乐对象加到乐谱中。这个选择板可能还包括选择、移动和其他操纵音乐对象的工具。用户可以点击四分音符工具并使用它将四分音符加到乐谱中。或者他们可以使用移动工具在五线谱上上下移动一个音符,从而改变它的音调。

我们假定该框架为音符和五线谱这样的图像构件提供了一个抽象的Graphics类。此外,为定义选择板中的那些工具,还提供了一个抽象类Tool。该框架还为一些创建图像对象实例并它们加入到文档中的工具预定义了一个GraphicTool子类。

但GraphicTool给框架设计者带来一个问题。音符和五线谱的类特定于我们的应用,而GraphicTool类却属于框架。GraphicTool不知道创建我们的音乐类的实例,并将我们添加到乐谱中。我们可以为每一种音乐对象创建一个GraphicTool子类,但这样会产生大量的子类,这些子类仅仅在它们所初始化的音乐对象的类别上有所不同。我们知道对象复合是创建子类更灵活的一种选择。问题是,该框架怎么样用它来参数化GraphicTool的实例,而这些实例是由Graphic类所支持创建的。

解决办法是让GraphicTool通过拷贝或者“克隆”一个Graphic子类的实例来创建新的Graphic,我们称这个实例为一个原型。GraphicTool将它应该克隆和添加到文档中的原型作为参数。如果所有Graphic子类都支持一个Clone操作,那么GraphicTool可以克隆所有种类的Graphic,如下图所示:

在这里插入图片描述
因此在我们的音乐编辑器中,用于创建个音乐对象的每一种工具都是一个用不同原型进行初始化的GraphicTool的实例。通过克隆一个音乐对象的原型并将这个克隆添加到乐谱中,每个GraphicTool实例都会产生一个音乐对象。

我们甚至可以进一步使用prototype模式来减少类的数量。我们使用不同的类来表示全音符合半音符,但可能不需要这么做。它们可以是使用不同位图和延时初始化相同的类的实例。一个创建全音符的工具就是这样的GraphicTool,它的原型是一个被初始化称全音符的MusicalNote。这可以极大的减少系统中的类的数目,同时也更易于在音乐编辑器中添加新的音符。

4. 适用性

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

  • 当一个系统独立于它的产品创建、构成和表示时,要使用Prototype模式;
  • 当要实例化的类是在运行时可开指定时,例如,通过动态装载;
  • 为了避免创建一个与产品类层次平行的工厂类层次时;
  • 当一个类的实例只能有几个不同状态组合中的一种时。建立相同数目的原型并克隆它们可能比每次用何时的状态手动实例化该类更方便一些。

5. 结构

在这里插入图片描述

6. 参与者

  • Prototype(Graphic)
    • 声明一个克隆自身的接口
  • ConcretePrototype(Staff、WholeNote、HalfNote)
    • 实现一个克隆自身的操作
  • Client(GraphicTool)
    • 让一个原型克隆自身从而创建一个新的对象。

7. 协作

  • 客户请求一个原型克隆自身。

8. 效果

Prototype有许多和Abstract Factory和Builder一样的效果:它对客户隐藏了具体的产品类,因此减少了客户知道的类的数目,此外,这些模式使客户无需改变即可使用与特定应用相关的类。

下面列出prototype的另外一些优点:

    1. 运行时刻增加和删除产品 Prototype允许只通过客户注册原型实例就可以将一个新的具体产品类加并入系统。它比其它创建型模式更为灵活,因为客户可以在运行时刻建立和删除原型。
    1. 改变值以指定新对象 高度动态的系统允许你通过对象复合定义新的行为——例如,通过为一个对象变量指定值——并且不定义新的类。你通过实例化已有类并且将这些实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。

      这种设计使得用户可以无需编程即可定义新“类”。实际上,克隆一个原型类似实例化一个类。Prototype模式可以极大的减少系统所需要的类的数目。在我们的音乐编辑器中,一个GraphicTool类可以创建无数音乐对象。

    1. 改变结构以指定新对象 许多应用由部件和子部件来创建对象。例如电路设计编辑器就是由子电路来构造电路的。为方便起见,这样的应用通常允许你实例化复杂的、用户指定的结构,比方说,一次又一次的重复使用一个特定的子电路。

      Prototype模式也支持这一点。我们仅需将这个子电路作为一个原型增加到可用的电路元素选择板中。只要复合电路对象将Clone实现为一个深拷贝(deep copy),具有不同结构的电路就可以是原型了。

    1. 减少子类的构造 Factory Method 经常产生一个与产品类层次平行的Creator类层次。Prototype模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象。因为你根本不需要的Creator类层次。这一优点主要适用于像C++这样不将类作为一级类对象的语言。像Smalltalk和Objective C 这样的语言从中获益较少,因为你总是可以用一个类对象作为生成这。在这些语言中,类对象已经起到了原型一样的作用了。
    1. 用类动态配置应用 一些运行时刻环境允许你动态将类装载到应用中。在像C++这样的语言中,Prototype模式时利用这种功能的关键。

      一个希望创建动态载入类的实例的应用不能静态引用类的构造器。而应该由运行环境在载入是自动创建每个类的实例,并用原型管理器来注册这个实例(参见实现一节)。这样应用就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序向连接。

Prototype的缺点:

    1. 每一个Prototype子类都要实现Clone操作 这可能很困难,还好吧,Java中Cloneable接口,实现这个接口就可以了。当内部包括一些不支持拷贝或有循环引用的对象时1,实现克隆可能也会很困难的。

9. 实现

因为在像C++这样的静态语言中,类不是对象,并且运行时刻只能得到很少或者得不到任何类型信息,所以Prototype特别有用。而在Java、Smalltalk或Objective C这样的语言中Prototype就不是那么重要,因为这些语言提供等价于原型的东西(即类对象在Java是Class对象)来创建每个类的实例。Prototype模式在像Self这样的基于原型的语言中时固有的,所有对象的创建都是通过克隆一个原型实现的。

当实现原型时,要考虑一个下面一些问题:

  • 1)使用一个原型管理器 当一个系统中原型数目不固定时(也就是说,它们可以动态创建和销毁),要保持一个可用原型的注册表。客户不会来自己管理原型,但会在注册表中存储和监所原型。客户在克隆一个原型前会向注册表请求该原型。我们称这个注册表为原型管理器。

    原型管理器是一个关联存储器(associative store), 它返回一个与给定关键字相匹配的原型。它有一些操作可以用来通过关键字注册原型和解除注册。客户可以在运行时更改甚或浏览这个注册表。这使得客户无需编写代码就可以扩展并得到系统清单。

  • 2)实现克隆操作 Prototype模式最困难的部分在于正确实现Clone操作。当对象结构包含循环引用,这尤为棘手。

    大多数语言都对克隆对象提供了一些支持。例如,Java提供了Cloneable接口,SmallTalk提供了一个Copy的实现被所有Object的子类所继承。C++提供了一个拷贝构造器。但这些设施并不能解决“浅拷贝”和“深拷贝”的问题。也就是说,克隆一个对象是依次克隆它的实例变量呢,或者还是有克隆对象和原对象共享这些变量呢?

    浅拷贝简单并且通常也足够了,他是Smalltalk所缺省提供的。C++中的缺省拷贝构造器实现按成员拷贝,这意味着在拷贝的和原来的对象之间是共享指针的。但克隆一个结构复杂的原型通常需要深拷贝,因为复制对象和元对象必须相互独立。因此你必须保证克隆对象的构建也是对原型的构建的克隆。克隆迫使你绝对如果所有东西都被共享了该怎么办。

    如果系统中的对象提供了Sava和Load操作,那么你只需通过保存对象和立刻载入对象,就可以为Clone操作提供了一个缺省实现。Save操作将该对象保存在内存缓冲区中,而Load则通过从该缓冲区中重构这个对象来创建一个复本。

  • 3)初始化克隆对象 当一些客户对克隆对象已经相当满意是,另一些客户将会希望使用他们所选择的一些值来初始化该对象的一些或者所有的内部状态。一般来说不可能在Clone中传递这些值,因为这些值得数目由于原型的类的不同而会有所不同。一些原型可能需要多个初始化参数,另一些可能什么也不要。在Clone中传递参数会破坏克隆接口的统一性。

    可能会这样,原型的类已经为(重)设定一些关键的状态值定义好了操作。如果这样的话,客户在克隆后马上那个就可以使用这些操作。否则,你就可能不得不引入一个Initialize操作,该操作使用初始化参数并据此设定克隆对象的内部状态。注意深拷贝Clone操作——一些复制在你重新初始化它们之前可能必须要被删除掉(删除可以显示地做也可以在Initialize内部做)。

10. 代码示例

在Java实现——首先是Prototype——Cloneable.java

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

ConcretePrototype——Employees.java

public class Employees implements Cloneable{
	private List<String> empList;
	
	public Employees() {
		empList = new ArrayList<>();
	}
	
	public Employees(List<String> list) {
		this.empList = list;
	}
	
	public void loadData() {
		//read all employees from database and put into the list
		empList.add("Pankaj");
		empList.add("Raj");
		empList.add("David");
		empList.add("Lisa");
	}
	
	
	public List<String> getEmpList(){
		return empList;
	}
	
	@Override
	public Object clone() throws CloneNotSupportedException {
		List<String> temp = new ArrayList<String>();
		for (String s : this.getEmpList()) {
			temp.add(s);
		}
		return new Employees(temp);
	}  
}

Client——PrototypePatternTest.java

public class PrototypePatternTest {

	public static void main(String[] args) throws CloneNotSupportedException{
		// TODO Auto-generated method stub
		Employees emps = new Employees();
		emps.loadData();
		
		// Use the clone method to get the Employees object
		Employees empsNew = (Employees)emps.clone();
		Employees empsNew1 = (Employees)emps.clone();
		
		List<String> list = empsNew.getEmpList();
		list.add("Kobe");
		List<String> list1 = empsNew1.getEmpList();
		list1.remove("Pankaj");
		
		System.out.println("emps List: "+emps.getEmpList());
		System.out.println("empsNew List: "+list);
		System.out.println("empsNew1 List: "+list1);
		
	}

}

对于的测试结果:

emps List: [Pankaj, Raj, David, Lisa]
empsNew List: [Pankaj, Raj, David, Lisa, Kobe]
empsNew1 List: [Raj, David, Lisa]

最后放上类的UML图:(这里由于Client是在main方法中声明Empliyees的引用,说UML看不见组合关系,其实只要把Employees作为域就可以了)

在这里插入图片描述

11. 已知应用

可能Prototype的第一个例子出现在Ivan sutherland的Sketchapad系统中.该模式在面向对象语言中第一个广为人知的应用是在ThingLab中,其中用户生成复杂对象。然后把它安装到一个可重用的对象库中从而促使它成为一个原型。Goldberg和Robson都提出原型是一种模式,但Coplien给了一个更为完整的描述。他为C++描述了与Prototype模式相关的属于并给出许多例子和变种。

Model Composer中的“交互技术库”(interaction technique library)存储了支持多种交互技术的对象的原型。将Model Composer创建的任一交互技术放入这个库中,他就可以被作为一种原型使用。Prototype模式使得Model Composer可支持数目无限的交互技术。

12. 相关模式

  • Abstract Factory : Prototype 和 Abstract Factory模式在某种方面而是互相竞争的。但是它们也可以一起使用,Abstract Factory可以存储一个被克隆的原型的集合,并且返回产品对象
  • CompositeDecorator: 大量使用Composite和Decorator模式的设计通常也可从Prototype模式获益。

13. 设计原则口袋

  • 封装变化
  • 针对接口编程,不针对实现编程
  • 类应该对扩展开放,对修改关闭。
  • 依赖抽象,不要依赖具体类。
  • 多用组合,少用继承。
  • 为交互对象间的松耦合设计而努力
  • 只有密友交谈
  • 好莱坞原则——别来找我,我会来找你
  • 单一责任原则——类应该只有一个改变的理由

14. 参考文献

《设计模式:可复用面向对象软件的基础》

《HeadFirst设计模式》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值