• C#设计模式-原型模式(Prototype Pattern)


    引言

    在软件开发过程中,我们习惯使用new来创建对象。但是当我们创建一个实例的过程很昂贵或者很复杂,并且需要创建多个这样的类的实例时。如果仍然用new操作符去创建这样的类的实例,会导致内存中多分配一个一样的类实例对象,增加创建类的复杂度和消耗更多的内存空间。
    如果采用简单工厂模式来创建这样的系统。随着产品类增加,子类数量不断增加,会增加额外系统复杂程度,为此我们不得不引入原型模式了。

    概念

    原型模式(Prototype Pattern)是一种创建型设计模式, 使你能够复制对象, 甚至是复杂对象, 而又无需使代码依赖它们所属的类。
    通过复制一个已经存在的实例来创建一个新的实例,而且不需知道任何创建的细节。被复制的实例被称为原型,这个原型是可定制的。
    所有的原型类都必须有一个通用的接口, 使得即使在对象所属的具体类未知的情况下也能复制对象。 原型对象可以生成自身的完整副本, 因为相同类的对象可以相互访问对方的私有成员变量。

    结构图

    原型模式下主要角色:

    • 原型(Prototype):声明一个克隆自身的接口,该角色一般有抽象类(Prototype)、接口(ICloneable)两种实现方式。
    • 具体原型类(ConcretePrototype):实现原型(抽象类或接口)的 Clone() 方法,它是可被复制的对象。
    • 访问类(Client):使用具体原型类中的 Clone() 方法来复制新的对象。

    实现

    假如有一个测试用例模板,项目A正在使用,公司又引进一个项目B,项目B的测试用例模板自己重新写一套肯定非常麻烦,那么可以使用项目A的用例模板,拿来改改就可以使用了。省却了许多时间。

    使用浅拷贝实现

    浅拷贝:将原来对象中的所有字段逐个复制到一个新对象,如果字段是值类型,则简单地复制一个副本到新对象,改变新对象的值类型字段不会影响原对象;如果字段是引用类型,则复制的是引用,改变目标对象中引用类型字段的值将会影响原对象。例如, 如果一个对象有一个指向引用类型(如测试用例的名称)的字段, 并且我们对该对象做了一个浅复制, 那麽两个对象将引用同一个引用(即同一个测试用例名称)。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace Prototype
    {
        class Program
        {
            static void Main(string[] args)
            {
                TestCase projectALoginCase = new TestCase
                {
                    Id = 1001,
                    ProjectName = "A项目",
                    CreatTime = new DateTime(2020, 11, 19),
                };
                projectALoginCase.SetTestCaseContent("登录测试", "", "打开登录页面并且登录", "登录成功");
    
    
                TestCase projectBLoginCase = (TestCase)projectALoginCase.Clone();
                projectBLoginCase.ProjectName = "B项目";
    
                projectALoginCase.Show();
                projectBLoginCase.Show();
                Console.Read();
            }
        }
    
        /// <summary>
        /// 实现了 ICloneable 接口
        /// </summary>
        public class TestCase : ICloneable
        {
            public TestCase()
            {
                mTestCaseContent = new TestCaseContent();
            }
    
            private int id;
            private string projectName;
            private DateTime creatTime;
            private TestCaseContent mTestCaseContent;
    
            public int Id
            {
                get { return id; }
                set { id = value; }
            }
    
            public string ProjectName
            {
                get { return projectName; }
                set { projectName = value; }
            }
    
            public DateTime CreatTime
            {
                get { return creatTime; }
                set { creatTime = value; }
            }
    
            
            public void Show()
            {
                Console.WriteLine($"Id:	{this.Id}");
                Console.WriteLine($"ProjectName:	{this.ProjectName}");
                Console.WriteLine($"CreatTime:	{this.CreatTime}");
                if (this.TestCaseContent != null)
                {
                    this.TestCaseContent.show();
                }
                Console.WriteLine("=================================================");
    
            }
    
            /// <summary>
            /// 关联一个引用类型
            /// </summary>
            public TestCaseContent TestCaseContent
            {
                get { return mTestCaseContent; }
            }
            public void SetTestCaseContent(string Name, string Level, string Step, string ExpectedResults)
            {
                this.mTestCaseContent.Name = Name;
                this.mTestCaseContent.Level = Level;
                this.mTestCaseContent.Step = Step;
                this.mTestCaseContent.ExpectedResults = ExpectedResults;
            }
    
            public object Clone()
            {
                // 浅拷贝对象的方法
                return this.MemberwiseClone();
            }
        }
    
        /// <summary>
        /// 测试用例内容类
        /// </summary>
        public class TestCaseContent
        {
            public string Name { get; set; }
            public string Level { get; set; }
            public string Step { get; set; }
            public string ExpectedResults { get; set; }
    
            public void show() {
                Console.WriteLine($"Name:	{this.Name}");
                Console.WriteLine($"Level:	{this.Level}");
                Console.WriteLine($"Step:	{this.Step}");
                Console.WriteLine($"ExpectedResults:	{this.ExpectedResults}");
            }
        }
    }

    运行后结果

    Id:    1001
    ProjectName:    A项目
    CreatTime:    11/19/2020 12:00:00 AM
    Name:    登录测试
    Level:    高
    Step:    打开登录页面并且登录
    ExpectedResults:    登录成功
    =================================================
    Id:    1001
    ProjectName:    B项目
    CreatTime:    11/19/2020 12:00:00 AM
    Name:    登录测试
    Level:    高
    Step:    打开登录页面并且登录
    ExpectedResults:    登录成功
    =================================================

    如果我们将拷贝后的项目B的测试用例的值进行重新设置,如下代码:

    static void Main(string[] args)
            {
                TestCase projectALoginCase = new TestCase
                {
                    Id = 1001,
                    ProjectName = "A项目",
                    CreatTime = new DateTime(2020, 11, 19),
                };
                projectALoginCase.SetTestCaseContent("登录测试", "", "打开登录页面并且登录", "登录成功");
    
    
                TestCase projectBLoginCase = (TestCase)projectALoginCase.Clone();
                projectBLoginCase.ProjectName = "B项目";
                projectBLoginCase.SetTestCaseContent("B项目登录测试", "级别高", "打开登录页面并且登录", "登录成功");
    
                projectALoginCase.Show();
                projectBLoginCase.Show();
                Console.Read();
            }

    再次运行结果如下:

    Id:    1001
    ProjectName:    A项目
    CreatTime:    11/19/2020 12:00:00 AM
    Name:    B项目登录测试
    Level:    级别高
    Step:    打开登录页面并且登录
    ExpectedResults:    登录成功
    =================================================
    Id:    1001
    ProjectName:    B项目
    CreatTime:    11/19/2020 12:00:00 AM
    Name:    B项目登录测试
    Level:    级别高
    Step:    打开登录页面并且登录
    ExpectedResults:    登录成功
    =================================================

    可以看的,通过浅拷贝后实际复制的是引用,改变目标对象中引用类型字段的值将会影响原对象。对于上面的实例显然是不可取的。修改B项目的测试用例影响到了A项目,肯定是有问题的。
    接下来介绍使用深拷贝进行实现。

    使用深拷贝实现

    深拷贝:与浅复制不同之处在于对引用类型的处理,深复制将新对象中引用类型字段指向复制过的新对象,改变新对象中引用的任何对象,不会影响到原来的对象中对应字段的内容。例如,如果一个对象有一个指向引用类型(如测试用例的名称)的字段,并且对该对象做了一个深复制的话,将创建一个新的对象(即新的测试用例名称)。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace Prototype
    {
        class Program
        {
            static void Main(string[] args)
            {
                TestCase projectALoginCase = new TestCase
                {
                    Id = 1001,
                    ProjectName = "A项目",
                    CreatTime = new DateTime(2020, 11, 19),
                };
                projectALoginCase.SetTestCaseContent("登录测试", "", "打开登录页面并且登录", "登录成功");
    
    
                TestCase projectBLoginCase = (TestCase)projectALoginCase.Clone();
                projectBLoginCase.ProjectName = "B项目";
                projectBLoginCase.SetTestCaseContent("B项目登录测试", "级别高", "打开登录页面并且登录", "登录成功");
    
                projectALoginCase.Show();
                projectBLoginCase.Show();
                Console.Read();
            }
        }
    
        /// <summary>
        /// 实现了 ICloneable 接口
        /// </summary>
        public class TestCase : ICloneable
        {
            public TestCase()
            {
                mTestCaseContent = new TestCaseContent();
            }
    
            /// <summary>
            /// 使用私有构造函数对引用类型进行复制
            /// </summary>
            /// <param name="testCaseContent"></param>
            private TestCase(TestCaseContent testCaseContent)
            {
                this.mTestCaseContent = (TestCaseContent)testCaseContent.Clone();
            }
    
            private int id;
            private string projectName;
            private DateTime creatTime;
            private TestCaseContent mTestCaseContent;
    
            public int Id
            {
                get { return id; }
                set { id = value; }
            }
    
            public string ProjectName
            {
                get { return projectName; }
                set { projectName = value; }
            }
    
            public DateTime CreatTime
            {
                get { return creatTime; }
                set { creatTime = value; }
            }
    
            
            public void Show()
            {
                Console.WriteLine($"Id:	{this.Id}");
                Console.WriteLine($"ProjectName:	{this.ProjectName}");
                Console.WriteLine($"CreatTime:	{this.CreatTime}");
                if (this.mTestCaseContent != null)
                {
                    this.mTestCaseContent.show();
                }
                Console.WriteLine("=================================================");
    
            }
    
            /// <summary>
            /// 设置测试用例详细内容
            /// </summary>
            /// <param name="Name"></param>
            /// <param name="Level"></param>
            /// <param name="Step"></param>
            /// <param name="ExpectedResults"></param>
            public void SetTestCaseContent(string Name, string Level, string Step, string ExpectedResults)
            {
                this.mTestCaseContent.Name = Name;
                this.mTestCaseContent.Level = Level;
                this.mTestCaseContent.Step = Step;
                this.mTestCaseContent.ExpectedResults = ExpectedResults;
            }
    
            public object Clone()
            {
                // 创建一个全新的测试用例内容
                TestCase newTestCase = new TestCase(this.mTestCaseContent);
    
                newTestCase.Id = this.Id;
                newTestCase.ProjectName = this.ProjectName;
                newTestCase.CreatTime = this.CreatTime;
    
                return newTestCase;
            }
        }
    
        /// <summary>
        /// 测试用例内容类
        /// </summary>
        public class TestCaseContent:ICloneable
        {
            public string Name { get; set; }
            public string Level { get; set; }
            public string Step { get; set; }
            public string ExpectedResults { get; set; }
    
            public object Clone()
            {
                // 浅拷贝
                return this.MemberwiseClone();
            }
    
            public void show() {
                Console.WriteLine($"Name:	{this.Name}");
                Console.WriteLine($"Level:	{this.Level}");
                Console.WriteLine($"Step:	{this.Step}");
                Console.WriteLine($"ExpectedResults:	{this.ExpectedResults}");
            }
        }
    }

    运行后结果:

    Id:    1001
    ProjectName:    A项目
    CreatTime:    11/19/2020 12:00:00 AM
    Name:    登录测试
    Level:    高
    Step:    打开登录页面并且登录
    ExpectedResults:    登录成功
    =================================================
    Id:    1001
    ProjectName:    B项目
    CreatTime:    11/19/2020 12:00:00 AM
    Name:    B项目登录测试
    Level:    级别高
    Step:    打开登录页面并且登录
    ExpectedResults:    登录成功
    =================================================

    从结果中可以看出,通过拷贝后A项目的测试用例还是A项目的,B项目的测试用例是B项目的。创建非常方便。

    应用场景

    原型模式通常适用于以下场景:

    • 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
    • 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
    • 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
    • 在实际项目中,原型模式很少单独出现,一般是和工厂模式一起出现,通过Clone方法创建一个对象,然后由工厂方法提供给调用者。

    优缺点

    优点:

    • 原型模式向客户隐藏了创建新实例的复杂性
    • 原型模式允许动态增加或较少产品类。
    • 原型模式简化了实例的创建结构,工厂方法模式需要有一个与产品类等级结构相同的等级结构,而原型模式不需要这样。
    • 产品类不需要事先确定产品的等级结构,因为原型模式适用于任何的等级结构

    缺点:

    • 每个类必须配备一个克隆方法。
    • 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
  • 相关阅读:
    宋宝华: Linux内核编程广泛使用的前向声明(Forward Declaration)
    朱辉(茶水): Linux Kernel iowait 时间的代码原理
    宋宝华:Linux设备驱动框架里的设计模式之——模板方法(Template Method)
    宋宝华:论程序员的时代焦虑与焦虑的缓解
    邵国际: C 语言对象化设计实例 —— 命令解析器
    让天堂的归天堂,让尘土的归尘土——谈Linux的总线、设备、驱动模型
    宋宝华:关于Ftrace的一个完整案例
    php采集页面指定标签里面的内容
    js调用video的播放时长
    php解析url并得到url中的参数
  • 原文地址:https://www.cnblogs.com/tynam/p/14005357.html
Copyright © 2020-2023  润新知