08.原型模式(Prototype)

发布时间 2023-07-03 18:02:12作者: huihui_teresa

使用原型模式来解决问题

定义

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

应用原型模式来解决问题的思路

原型模式会要求对象实现一个可以“克隆”自身的接口,这样就可以通过拷贝或者是克隆一个实例对象本身来创建一个新的实例。如果把这个方法定义在接口上,看起来就像是通过接口来创建了新的接口对象。

这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,也不关心它的具体实现,只要它实现了克隆自身的方法,就可以通过这个方法来获取新的对象,而无须再去通过 new 来创建。

结构和说明

  • Prototype:声明一个克隆自身的接口,用来约束想要克隆自己的类,要求它们都要实现这里定义的克隆方法。
  • ConcretePrototype:实现 Prototype 接口的类,这些类真正实现了克隆自身的功能。
  • Client: 使用原型的客户端,首先要获取到原型实例对象,然后通过原型实例克隆自身来创建新的对象实例。

示例代码

namespace NetCore3Console.Prototype
{
    /// <summary>
    /// 声明一个克隆自身的接口
    /// </summary>
    public interface IPrototype
    {
        /// <summary>
        /// 克隆自身的方法
        /// </summary>
        /// <returns></returns>
        IPrototype Clone();
    }

    /// <summary>
    /// 克隆的具体实现对象
    /// </summary>
    public class ConcretePrototype1 : IPrototype
    {
        //族简单的克隆,新建一个自身对象,由于没有属性,就不再复制值了
        public IPrototype Clone()
        {
            IPrototype prototype = new ConcretePrototype1();
            return prototype;
        }
    }
    public class ConcretePrototype2 : IPrototype
    {
        //族简单的克隆,新建一个自身对象,由于没有属性,就不再复制值了
        public IPrototype Clone()
        {
            IPrototype prototype = new ConcretePrototype2();
            return prototype;
        }
    }

    /// <summary>
    /// 使用原型的客户端
    /// </summary>
    public class Client
    {
        //持有需要使用的原型接口对象
        private IPrototype _prototype;
        /// <summary>
        /// 构造方法,传入需要使用的原型接口对象
        /// </summary>
        /// <param name="prototype"></param>
        public Client(IPrototype prototype)
        {
            _prototype = prototype;
        }

        /// <summary>
        /// 示意方法,执行某个功能操作
        /// </summary>
        public void Operation()
        {
            //需要创建原型接口的对象
            var newPrototype = _prototype.Clone();
        }
    }
}

认识原型模式

原型式功能

原型模式的功能实际上包含两个方面:

  • 一个是通过克隆来创建新的对象实例;
  • 另一个是为克隆出来的新的对象实例复制原型实例属性的值

原型模式要实现的主要功能就是:通过克隆来创建新的对象实例。一般来讲,新创建出来的实例的数据是和原型实例一样的。但是具体如何实现克隆,需要由程序自行实现,原型模式并没有统一的要求和实现算法。

原型与new

原型模式从某种意义上说,就像是 new 操作,在前面的例子实现中,克隆方法就是使用new 来实现的。但请注意,只是“类似于new”而不是“就是new”

克隆方法和 new 操作最明显的不同就在于: new 一个对象实例,一般属性是没有值的,或者是只有默认值;如果是克隆得到的一个实例,通常属性是有值的,属性的值就是原型对象实例在克隆的时候,原型对象实例的属性的值。

原型实例和克隆的实例

原型实例和克隆出来的实例,本质上是不同的实例,克隆完成后,它们之间是没有关联的,如果克隆完成后,克隆出来的实例的属性值发生了改变,是不会影响到原型实例的。

原型管理器

如果一个系统中原型的数目不固定,比如系统中的原型可以被动态地创建和销毁,那么就需要在系统中维护一个当前可用的原型的注册表,这个注册表就被称为原型管理器。

有了原型管理器后,一般情况下,除了向原型管理器里面添加原型对象的时候是通过new 来创造的对象,其余时候都是通过向原型管理器来请求原型实例,然后通过克隆方法来获取新的对象实例,这就可以实现动态管理,或者动态切换具体的实现对象实例。

using System;
using System.Collections.Generic;

namespace NetCore3Console.Prototype.Prototype1
{
    /// <summary>
    /// 原型的接口
    /// </summary>
    public interface IPrototype
    {
        IPrototype Clone();
        string GetName();
        void SetName(string name);
    }

    /*
     * 两个具体的实现
     */
    public class ConcretePrototype1 : IPrototype
    {
        private string _name;
        public IPrototype Clone()
        {
            var prototype = new ConcretePrototype1();
            prototype.SetName(_name);
            return prototype;
        }
        public string GetName()
        {
            return _name;
        }
        public void SetName(string name)
        {
            _name = name;
        }
        public override string ToString()
        {
            return $"Now in Prototype1,name={_name}";
        }
    }
    public class ConcretePrototype2 : IPrototype
    {
        private string _name;
        public IPrototype Clone()
        {
            var prototype = new ConcretePrototype2();
            prototype.SetName(_name);
            return prototype;
        }
        public string GetName()
        {
            return _name;
        }
        public void SetName(string name)
        {
            _name = name;
        }
        public override string ToString()
        {
            return $"Now in Prototype2,name={_name}";
        }
    }

    /// <summary>
    /// 原型管理器
    /// </summary>
    public class PrototypeManager
    {
        private static object o = new object();

        //用来记录原型的编号和原型示例的对应关系
        private static Dictionary<string, IPrototype> map = new Dictionary<string, IPrototype>();

        //私有化构造方法,避免外部无谓的创建实例
        private PrototypeManager() { }

        /// <summary>
        /// 向原型管理器里面添加,或是修改某个原型注册
        /// </summary>
        /// <param name="prototypeId"></param>
        /// <param name="prototype"></param>
        public static void SetPrototype(string prototypeId,IPrototype prototype)
        {
            lock (o)
            {
                map.Add(prototypeId,prototype);
            }
        }
        /// <summary>
        /// 删除原型注册
        /// </summary>
        /// <param name="id"></param>
        public static void RemovePrototype(string id)
        {
            lock (o)
            {
                map.Remove(id);
            }
        }
        /// <summary>
        /// 获取原型实例
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static IPrototype GetPrototype(string id)
        {
            lock (o)
            {
                Prototype1.IPrototype prototype;
                var isHave = map.TryGetValue(id, out prototype);
                if (!isHave)
                    throw new Exception("你获取的实例不存在");
                return prototype;
            }
        }
    }
}

大家会发现,原型管理器是类似一个工具类的实现方式,而且对外的几个方法都是加了同步的,这主要是因为如果在多线程环境下使用这个原型管理器的话,那个 map 属性很明显就成了大家竞争的资源,因此需要加上同步。

优缺点

优点:

  • 对客户端隐藏具体的实现类型
    原型模式的客户端只知道原型接口的类型,并不知道具体的实现类型,从而减少了客户端对这些具体实现类型的依赖。
  • 在运行时动态改变具体的实现类型
    原型模式可以在运行期间,由客户来注册符合原型接口的实现类型,也可以动态地改变具体的实现类型,看起来接口没有任何变化,但其实运行的已经是另外一个类实例了。因为克隆一个原型就类似于实例化一个类。

缺点:

原型模式最大的缺点就在于每个原型的子类都必须实现 clone 的操作,尤其在包含引用类型的对象时,clone 方法会比较麻烦,必须要能够递归地让所有的相关对象都要正确地实现克隆。

思考原型模式

本质

原型模式的本质:克隆生成对象。

克隆是手段,目的是生成新的对象实例。正是因为原型的目的是为了生成新的对象实例,原型模式通常是被归类为创建型的模式。

原型模式也可以用来解决“只知接口而不知实现的问题”,使用原型模式,可以出现一种独特的“接口造接口”的景象,这在面向接口编程中很有用。同样的功能也可以考虑使用工厂来实现。

另外,原型模式的重心还是在创建新的对象实例,至于创建出来的对象,其属性的值是否一定要和原型对象属性的值完全一样,这个并没有强制规定,只不过在目前大多数实现中,克隆出来的对象和原型对象的属性值是一样的。

何时选用原型模式

  • 如果一个系统想要独立于它想要使用的对象时,可以使用原型模式,让系统只面向接口编程,在系统需要新的对象的时候,可以通过克隆原型来得到。
  • 如果需要实例化的类是在运行时刻动态指定时,可以使用原型模式,通过克降原型来得到需要的实例。