一起学习设计模式--05.原型模式

2022-12-06 18:43:05 浏览数 (1)

前言

《西游记》中的孙悟空吹猴毛变出很多猴兵的故事想必大家都知道,《火影》中鸣人的多重影分身大部分人应该也是都知道,他们都可以根据自己的形象,复制(又称克隆)出很多和自己一摸一样的“身外身”来。在设计模式中也存在一个类似的模式,可以通过一个原型对象克隆出多个一模一样的对象,该模式被成为原型模式。

一、大小异同的工作周报

A公司一直使用自行开发的一套OA系统进行日常工作办理,但在使用过程中,越来越多的人对工作周报的创建和编写模块产生了抱怨。究其原因,A公司的OA管理员发现,由于某些岗位每周工作存在重复性,工作周报内容都大同小异。如图:

这些周报只有一些小地方存在差异,但是先行系统每周默认创建的周报都是空白报表,用户只能通过重新输入或不断复制、粘贴来填写重复的周报内容,极大降低了工作效率,浪费宝贵的时间。如何快速创建相同或相似的工作周报,成为A公司OA开发人员面临的一个问题。

开发人员通过对问题仔细分析,决定按照以下思路对工作周报模块进行重构设计和实现:

  1. 除了运行用户创建新的周报外,还允许用户将创建好的周报保存为模板。
  2. 用户在再次创建周报时,可以创建全新的周报,也可以选择合适的模板复制生成一份相同的周报,然后对新生成的周报根据实际情况进行修改,产生新的周报。

只要按以上步骤,就可以大大的提高工作周报的创建效率。但是如何在一个面向对象系统中实现对象的复制和粘贴呢?接下来要学习的原型模式正为解决这类问题而诞生。

二、原型模式概述

在使用原型模式时,需要首先创建一个原型对象,然后再通过复制这个原型对象来创建更多同类型的对象。定义如下:

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

原型模式的工作原理很简单:将一个原型对象传给要发动创建的对象,这个要发动创建的对象通过请求原型对象克隆自己来实现创建过程。通过克隆方法所创建的对象是全新的对象,他们在内存中拥有新的地址。对克隆产生的新对象进行修改不会对原型对象造成任何影响,每个克隆对象都是相互独立的。

原型模式的结构如图:

原型模式结构图中包含以下3个角色:

  1. Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体的实现类。
  2. ConcretePrototye(具体原型类):它实现在抽象原型类声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
  3. Clinet(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该原型对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类 Prototype 编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体的圆形类都很方便。
1、通用实现方法

通用的克隆实现方法是在具体圆形类的克隆方法中实例化一个与自身类型相同的对象并将其返回,并将相关的参数传入新创建的对象中,保证它们的成员变量相同。

代码语言:javascript复制
    /// <summary>
    /// 抽象原型类
    /// </summary>
    public abstract class Prototype
    {
        public string Id { get; private set; }

        protected Prototype(string id)
        {
            this.Id = id;
        }

        //抽象类关键就是这样一个Clone方法
        public abstract Prototype Clone();
    }

    /// <summary>
    /// 具体原型类
    /// </summary>
    public class ConcretePrototypeA : Prototype
    {
        public ConcretePrototypeA(string id) : base(id)
        {
        }

        public override Prototype Clone()
        {
            Prototype prototype = new ConcretePrototypeA(Id);
            return prototype;
        }
    }

客户端测试代码:

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            Prototype cp1 = new ConcretePrototypeA("123");
            Prototype cp2 =  cp1.Clone();

            Console.WriteLine("Cloned:{0}", cp2.Id);

            Console.ReadKey();
        }
    }
2、C#语言提供的 Clone 方法

C# 中要想能够实现克隆的类,必须实现一个标识接口 ICloneable,表示这个类支持被复制。

代码语言:javascript复制
    public class ConcretePrototypeB : ICloneable
    {
        public object Clone()
        {
            return (object) this.MemberwiseClone();
        }
    }

3、完整解决方案

A公司开发人员决定使用原型模式来实现工作周报的快速创建,结构如图:

WeeklyLog 充当具体原型类, object 充当抽象原型类,Clone() 方法为原型方法。WeeklyLog 代码如下:

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            WeeklyLog logPre = new WeeklyLog();
            logPre.Name = "张无忌";
            logPre.Date = "第12周";
            logPre.Content = "这周工作很忙,每天加班!";
            PrintLog(logPre);

            WeeklyLog logNew = logPre.Clone() as WeeklyLog;//调用克隆方法创建克隆对象
            logNew.Date = "第13周";
            PrintLog(logNew);

            WeeklyLog logNext = logNew.Clone() as WeeklyLog;
            logNext.Date = "第14周";
            logNext.Content = "这周还好,调休了2天";
            PrintLog(logNext);

            Console.ReadKey();
        }

        static void PrintLog(WeeklyLog log)
        {
            if (log == null) return;
            Console.WriteLine("***周报***");
            Console.WriteLine("周次:{0}", log.Date);
            Console.WriteLine("姓名:{0}", log.Name);
            Console.WriteLine("内容:{0}", log.Content);
            Console.WriteLine("-----------------------------------------------");
        }
    }

输出结果:

通过已创建的工作周报可以快速的创建新的周报,然后再根据需要修改周报,无须再从头开始创建。原型模式为工作流系统中任务单的快速生成提供了一种解决方案。

四、带附件的周报

通过引入原型模式,OA系统工作周报的编写效率得到了很大提升,受到了员工的一致好评。但是有些员工发现了一个问题,当周报中带有附件时,使用原型模式来复制周报,虽然周报可以复制,但是周报的附件并不能复制。那怎么解决呢?

在回答这个问题之前,先介绍两种不同的克隆方法,浅克隆(Shallow Clone)和深克隆(Deep Clone)。两种方法的主要区别就是是否支持引用类型的成员变量的复制。

值类型包括:int、double、byte、bool、chart等简单数据类型。引用类型包括:类、接口、数组等复杂类型。

1、浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的引用类型成员变量指向相同的内存地址。

代码:

代码语言:javascript复制
    /// <summary>
    /// 附件类
    /// </summary>
    public class Attachment
    {
        public string Name { get; set; }

        public Attachment(string name)
        {
            Name = name;
        }

        public void DownLoad()
        {
            Console.WriteLine("下载文件,文件名为"   Name);
        }
    }

    public class WeeklyLog : ICloneable
    {
        public string Name { get; set; }
        public string Date { get; set; }
        public string Content { get; set; }
        public List<Attachment> Attachments { get; set; }

        public WeeklyLog()
        {
            Attachments = new List<Attachment>();
        }


        public object Clone()
        {
            //使用C#提供的方法实现浅克隆
            return this.MemberwiseClone();
        }
    }

客户端代码:

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            V2.WeeklyLog logPre, logNew;
            logPre = new V2.WeeklyLog();
            logPre.Attachments.Add(new Attachment("第16周工作周报.txt"));

            logNew = logPre.Clone() as V2.WeeklyLog;

            Console.WriteLine("周报是否相同:{0}", logPre == logNew);
            Console.WriteLine("附件是否相同:{0}", logPre.Attachments[0] == logNew.Attachments[0]);
        }
    }

编译并运行,输出结果:

由此可见,由于使用的是浅克隆技术,通过“==”来比较原型对象和克隆对象的内存地址时输出 False,说明这两个对象在内存中分别有不同的地址,因此工作周报对象复制成功。但是比较附件对象的内存地址时输出 True,说明两个附件对象在内存中是同一个对象。

2、深克隆

在深克隆中无论原型对象的成员变量是值类型还是引用类型,都将复制一份都克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。也就是说,深克隆除了对像本身被复制外,对象所包含的所有成员变量也将被复制。

C#中,如果要使用深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个复制品,而元对象仍然存在于内存中。

克隆流程:通过序列化将对象写到一个流中,再从流里将其 读出来,就是实现了深克隆。

要实现序列化的对象,其类必须添加 Serializable 属性标签,不然无法实现序列化。

修改后代码如下:

代码语言:javascript复制
    /// <summary>
    /// 附件类
    /// </summary>
    [Serializable]
    public class Attachment
    {
        public string Name { get; set; }

        public Attachment(string name)
        {
            Name = name;
        }

        public void DownLoad()
        {
            Console.WriteLine("下载文件,文件名为"   Name);
        }
    }

    [Serializable]
    public class WeeklyLog
    {
        public string Name { get; set; }
        public string Date { get; set; }
        public string Content { get; set; }
        public List<Attachment> Attachments { get; set; }

        public WeeklyLog()
        {
            Attachments = new List<Attachment>();
        }

        public object DeepClone()
        {
            MemoryStream ms = new MemoryStream(); // 初始化一个内存流
            BinaryFormatter bf = new BinaryFormatter(); // 以二进制的格式来序列化和反序列化对象
            bf.Serialize(ms, this); // 将档案对象序列化到内存流中
            //设置流的位置,SeekOrigin.Begin表示流的开始,0表示相对于SeekOrigin.Begin的偏移量
            ms.Seek(0, SeekOrigin.Begin); 
            return bf.Deserialize(ms); // 反序列化,实现深克隆
        }
    }

客户端代码如下:

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            V3.WeeklyLog logPre, logNew = null;
            logPre = new V3.WeeklyLog();
            logPre.Attachments.Add(new V3.Attachment("第16周工作周报.txt"));
            try
            {
                logNew = logPre.DeepClone() as V3.WeeklyLog;
            }
            catch (Exception ex)
            {
                Console.WriteLine("克隆失败");
            }

            Console.WriteLine("周报是否相同:{0}", logPre == logNew);
            Console.WriteLine("附件是否相同:{0}", logPre.Attachments[0] == logNew.Attachments[0]);
        }
    }

编译并运行,结果如图:

从结果就可以看出,由于使用了深克隆,所以附件对象也复制了,因此使用“==”比较结果均为 False。深克隆技术实现了原型对象和克隆对象的完全独立,无论对克隆对象做任何修改,都不会影响原型对象,是一种更为理想的克隆方式。

浅克隆:只克隆原型对象和原型对象中的值类型成员变量(变双份),引用类型成员变量两个对象共用深克隆:原型对象、原型对象中的值类型成员变量和原型对象中的引用类型成员变量全部复制一份新的

五、原型管理器的引入和实现

1、原型管理器概念

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

带原型管理器的结构如图:

2、原型管理器的设计和实现

A公司在日常办公中有许多公文需要创建、递交和审批,比如《可行性分析报告》《立项建议书》《软件需求规格说明书》等。为了提高工作效率,在OA系统中为各类公文均创建了模板,用户可以通过这些模板创建新的公文,这些公文模板需要统一进行管理,系统根据用户请求的不同生成不同的新公文。

开发人员使用带原型管理器的原型模式来实现公文管理器的设计,结构如图:

代码如下:

代码语言:javascript复制
    /// <summary>
    /// 抽象公文接口,也可以定义为抽象类
    /// </summary>
    public interface IOfficialDocument : ICloneable
    {
        new IOfficialDocument Clone();//隐藏ICloneable的Clone方法接口
        void Display();
    }
    
    /// <summary>
    /// 可行性分析报告
    /// </summary>
    public class FAR : IOfficialDocument
    {
        public IOfficialDocument Clone()
        {
            return (IOfficialDocument) base.MemberwiseClone();
        }

        public void Display()
        {
            Console.WriteLine("《可行性分析报告》");
        }

        object ICloneable.Clone()
        {
            return this.Clone();
        }
    }
    
    /// <summary>
    /// 软件需求规格说明书
    /// </summary>
    public class SRS : IOfficialDocument
    {
        public IOfficialDocument Clone()
        {
            return (IOfficialDocument) base.MemberwiseClone();
        }

        public void Display()
        {
            Console.WriteLine("《软件需求规格说明书》");
        }

        object ICloneable.Clone()
        {
            return Clone();
        }
    }
    
    /// <summary>
    /// 原型管理器(使用饿汉式单例)
    /// </summary>
    public class PrototypeManager
    {
        private Dictionary<string, IOfficialDocument> odDic = new Dictionary<string, IOfficialDocument>();
        private static PrototypeManager pm = new PrototypeManager();

        private PrototypeManager()
        {
            odDic.Add("far", new FAR());
            odDic.Add("srs", new FAR());
        }

        public static PrototypeManager GetPrototypeManager() => pm;

        /// <summary>
        /// 增加新的公文对象
        /// </summary>
        public void AddOfficialDocument(string key, IOfficialDocument doc)
        {
            odDic.Add(key, doc);
        }

        /// <summary>
        /// 通过浅克隆获取新的公文对象
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public IOfficialDocument GetOfficialDocument(string key)
        {
            return odDic[key].Clone();
        }
    }

客户端代码如下:

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            var pm = PrototypeManager.GetPrototypeManager();
            IOfficialDocument doc1, doc2, doc3, doc4;

            doc1 = pm.GetOfficialDocument("far");
            doc1.Display();

            doc2 = pm.GetOfficialDocument("far");
            doc2.Display();
            Console.WriteLine(doc1 == doc2);

            doc3 = pm.GetOfficialDocument("srs");
            doc3.Display();

            doc4 = pm.GetOfficialDocument("srs");
            doc4.Display();

            Console.WriteLine(doc3 == doc4);
        }
    }

编译并运行输出结果:

原型管理器中使用了一个字典来保存原型对象,客户端可以通过 key 来获取原型对象的克隆对象。另外本例中将原型管理器设计为单例类,并通过饿汉式方式实现,确保系统中只有一个原型管理器,有利于节省系统资源,并可以更好的对原型管理器进行控制。

六、原型模式总结

1、主要优点
  1. 当创建新的对象实例比较复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
  2. 扩展性较好。原型模式中提供了抽象原型类,客户端可以针对抽象原型类编程,而将具体原型类写在配置文件中,增加或减少具体原型类对原有系统不会产生任何影响。
  3. 原型模式提供了简化的创建结构。原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
  4. 可以使用深克隆的方式保存对象的状态。使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用。
2、主要缺点
  1. 需要为每个类提供一个克隆方法,而且该克隆方法位于一个类的内部。当对已有的类进行改造时,需要修改源码,违背了开闭原则。
  2. 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,需要为每一层对象对应的类提供深克隆方法,实现起来比较麻烦。
3、适用场景
  1. 创建新对象的成本比较大时,新对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
  2. 如果系统需要保存对象的状态
  3. 避免使用工厂来创建分层次的对象(多重嵌套),并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到实例会更加方便。

示例代码:

https://github.com/crazyliuxp/DesignPattern.Simples.CSharp

参考资料:

0 人点赞