拜读经典——大话设计模式(五)——原型模式、模板方法模式、迪米特法则、外观模式

前言

承接前面的内容,继续来学习这本经典武学典籍,这篇会介绍两个设计模式,再补充一个设计法则。

 

 

 

 

 


原型模式

概念与结构

原型模式(Prototype):用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

原型模式其实就是从一个对象再创建另外一个可以定制的对象,而且不需知道任何创建的细节。

基本模板代码

基本模板代码如下:

    abstract class Prototype
    {
        private string id;
        public Prototype(string id) {
            this.id = id;
        }
        public string Id {
            get { return id; }
        }
        //抽象类关键就是有这样的一个Clone方法
        public abstract Prototype Clone();
    }
    class ConcretePrototype1 : Prototype
    {
        public ConcretePrototype1(string id) : base(id) {
        }
        public override Prototype Clone()
        {
            /* MemberwiseClone:创建当前对象的浅表副本。
             * 方法是创建一个新对象,然后将当前对象的
             * 非静态字段复制给该新对象。如果字段是值类型的,
             * 则对该字段执行逐位复制。如果字段是引用类型的,
             * 则复制引用但不复制引用的对象;
             * 因此,原始对象及其副本引用同一对象。
             */
            return (Prototype)this.MemberwiseClone();
        }
    }

主函数:

    class Program
    {
        static void Main(string[] args)
        {
            ConcretePrototype1 p1 = new ConcretePrototype1("I");
            ConcretePrototype1 c1 = (ConcretePrototype1)p1.Clone();
            Console.WriteLine("Cloned:{0}", c1.Id);

            Console.Read();
        }
    }

控制台:

Cloned:I

这样的确可以不用实例化新的对象,直接克隆就行了,但是对于.NET而言,原型抽象类Prototype是用不着的,因为克隆非常常用,所以.NET在System命名空间中提供了ICloneable接口,其中就是唯一的一个方法Clone(),这样我们就只需要实现这个接口就可以完成原型模式了。

 

示例:多份简历

我们来做个例子,现在需要我们来做一个“简历”类,具有可以动态复制的特点:

    class Resume : ICloneable
    {
        private string name;
        private string sex;
        private string age;
        private string timeArea;
        private string company;

        public Resume(string name) {
            this.name = name;
        }
        //设置个人信息
        public void SetPersonalInfo(string sex,string age) {
            this.sex = sex;
            this.age = age;
        }
        //设置工作经历
        public void SetWorkExperience(string timeArea,string company) {
            this.timeArea = timeArea;
            this.company = company;
        }
        //显示
        public void Display() {
            Console.WriteLine("{0} {1} {2}", name, sex, age);
            Console.WriteLine("工作经历:{0} {1}", timeArea, company);
        }
        //实现接口的方法,用来克隆对象
        public object Clone()
        {
            return (object)this.MemberwiseClone();
        }
    }

主类:

    class Program
    {
        static void Main(string[] args)
        {
            Resume a = new Resume("大鸟");
            a.SetPersonalInfo("男", "29");
            a.SetWorkExperience("1999-2004", "XX公司");

            Resume b = (Resume)a.Clone();
            b.SetWorkExperience("1998-2006", "YY企业");

            Resume c = (Resume)a.Clone();
            c.SetPersonalInfo("男", "24");

            a.Display();
            b.Display();
            c.Display();

            Console.Read();
        }
    }

控制台:

大鸟 男 29
工作经历:1999-2004 XX公司
大鸟 男 29
工作经历:1998-2006 YY企业
大鸟 男 24
工作经历:1999-2004 XX公司

这里我们需要明白,MemberwiseClone会将类中的值类型字段按位复制,将类中的引用类型只复制其引用声明。

string在C#中是一种引用类型,但是它具有特殊性,string类型的变量被赋值后,如果再给它赋值,那么会重新开辟空间存放新数据(堆上会为新值分配一个新的string对象),string类型的声明重新指向新空间。(Java中也是这样)

所以虽然string是引用类型,但是我们的“简历”实验里修改克隆对象的字段后,原对象的相应字段没有被修改,因为已经不指向一处了。

一般在初始化的信息不发生变化的情况下,克隆是最好的办法。这既隐藏了对象创建的细节,又对性能是大大的提高。它等于是不用重新初始化对象,而是动态地获得对象运行时的状态。

浅复制与深复制

写过C++的应该猜到了,这就是指“深拷贝”“浅拷贝”。

上例中,我们的字段都是string,所以可以重新赋值完成克隆者细节的区别,但如果是object类型(即一般的类),就会造成“藕断丝连”,即克隆者的某些字段与被克隆者的对应字段指向同一片堆空间。

浅复制:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。

深复制:把引用对象的变量指向复制过来的新对象,而不是原有的被引用的对象。

 

忽略深浅拷贝下的示例:

    class WorkExperience {
        private string workDate;
        public string WorkDate {
            get { return workDate; }
            set { workDate = value; }
        }
        private string company;
        public string Company {
            get { return company; }
            set { company = value; }
        }
    }
    class Resume : ICloneable
    {
        private string name;
        private string sex;
        private string age;

        private WorkExperience we;

        public Resume(string name) {
            this.name = name;
            we = new WorkExperience();
        }
        //设置个人信息
        public void SetPersonalInfo(string sex,string age) {
            this.sex = sex;
            this.age = age;
        }
        //设置工作经历
        public void SetWorkExperience(string workdate,string company) {
            we.WorkDate = workdate;
            we.Company = company;
        }
        //显示
        public void Display() {
            Console.WriteLine("{0} {1} {2}", name, sex, age);
            Console.WriteLine("工作经历:{0} {1}", we.WorkDate, we.Company);
        }
        //实现接口的方法,用来克隆对象
        public object Clone()
        {
            return (object)this.MemberwiseClone();
        }
    }

主类:

    class Program
    {
        static void Main(string[] args)
        {
            Resume a = new Resume("大鸟");
            a.SetPersonalInfo("男", "29");
            a.SetWorkExperience("1999-2004", "XX公司");

            Resume b = (Resume)a.Clone();
            b.SetWorkExperience("1998-2006", "YY企业");

            Resume c = (Resume)a.Clone();
            c.SetPersonalInfo("男", "24");

            a.Display();
            b.Display();
            c.Display();

            Console.Read();
        }
    }

控制台:

大鸟 男 29
工作经历:1998-2006 YY企业
大鸟 男 29
工作经历:1998-2006 YY企业
大鸟 男 24
工作经历:1998-2006 YY企业

三个类中的WorkExperience对象指向的是同一个实例。

简单的深复制实现

    class WorkExperience :ICloneable {
        private string workDate;
        public string WorkDate {
            get { return workDate; }
            set { workDate = value; }
        }
        private string company;
        public string Company {
            get { return company; }
            set { company = value; }
        }

        public object Clone()
        {
            return (object)this.MemberwiseClone();
        }
    }
    class Resume : ICloneable
    {
        private string name;
        private string sex;
        private string age;

        private WorkExperience we;

        public Resume(string name) {
            this.name = name;
            we = new WorkExperience();
        }
        private Resume(WorkExperience work) {
            this.we = (WorkExperience)work.Clone();
        }
        //设置个人信息
        public void SetPersonalInfo(string sex,string age) {
            this.sex = sex;
            this.age = age;
        }
        //设置工作经历
        public void SetWorkExperience(string workdate,string company) {
            we.WorkDate = workdate;
            we.Company = company;
        }
        //显示
        public void Display() {
            Console.WriteLine("{0} {1} {2}", name, sex, age);
            Console.WriteLine("工作经历:{0} {1}", we.WorkDate, we.Company);
        }
        //实现接口的方法,用来克隆对象
        public object Clone()
        {
            /*调用私有的构造方法,让“工作经历”克隆完成,
             *然后再给这个“简历”对象的相关字段赋值,
             * 最终返回一个深复制的简历对象
             */
            Resume obj = new Resume(this.we);
            obj.name = this.name;
            obj.sex = this.sex;
            obj.age = this.age;
            return obj;
        }
    }

主程序:

    class Program
    {
        static void Main(string[] args)
        {
            Resume a = new Resume("大鸟");
            a.SetPersonalInfo("男", "29");
            a.SetWorkExperience("1999-2004", "XX公司");

            Resume b = (Resume)a.Clone();
            b.SetWorkExperience("1998-2006", "YY企业");

            Resume c = (Resume)a.Clone();
            c.SetPersonalInfo("男", "24");

            a.Display();
            b.Display();
            c.Display();

            Console.Read();
        }
    }

控制台效果:

大鸟 男 29
工作经历:1999-2004 XX公司
大鸟 男 29
工作经历:1998-2006 YY企业
大鸟 男 24
工作经历:1999-2004 XX公司

我们在WorkExperience类中实现了克隆接口,从而让WorkExperience 中的字段都可以被值克隆,而在Resume 类中,我们的Clone方法里不再是将自身浅复制一下就返回出去了,而是我们将利用一个以WorkExperience类变量为参数的私有构造方法创造一个新的Resume对象,然后给这个对象中的属性进行赋值,然后将这个对象返回出去,这就是一个完整的深拷贝对象。

 


模板方法模式

概念

当我们要完成在某一细节层次一致的一个过程或一系列步骤,但其个别步骤在更详细的层次上的实现可能不同时,我们通常考虑用模板方法模式来处理。

模板方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

UML图

 

模板代码

AbstractClass抽象类是一抽象模板,定义并实现了一个模板方法,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。

模板代码如下:

  abstract class AbstractClass
    {
        //原始的操作,即将要延迟到子类定义的方法
        public abstract void PrimitiveOperation1();
        public abstract void PrimitiveOperation2();

        public void TemplateMethod() {
            PrimitiveOperation1();
            PrimitiveOperation2();
            Console.WriteLine("");
        }
    }
    class ConcreteClassA : AbstractClass
    {
        public override void PrimitiveOperation1()
        {
            Console.WriteLine("具体类A方法1的实现");
        }
        public override void PrimitiveOperation2()
        {
            Console.WriteLine("具体类A方法2的实现");
        }
    }
    class ConcreteClassB : AbstractClass
    {
        public override void PrimitiveOperation1()
        {
            Console.WriteLine("具体类B方法1的实现");
        }
        public override void PrimitiveOperation2()
        {
            Console.WriteLine("具体类B方法2的实现");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            AbstractClass c;
            c = new ConcreteClassA();
            c.TemplateMethod();

            c = new ConcreteClassB();
            c.TemplateMethod();

            Console.Read();
        }
    }

输出:

具体类A方法1的实现
具体类A方法2的实现

具体类B方法1的实现
具体类B方法2的实现

 

模板方法模式特点

模板方法模式是通过把不变行为搬移到超类,去除子类中的重复代码来体现他的优势。

其实,本质上来说,模板方法模式就是提供了一个很好的代码复用平台。

当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。

这种设计模式其实并不算难,对继承和多态玩的好的人几乎都会在继承体系中多多少少用到他。

 

 

 


迪米特法则

概念

迪米特法则(LoD),也叫最少知识原则,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某个方法的话,可以通过第三者转发这个调用。

一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

释义

迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限。

迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。

 


外观模式

外观模式也叫“门面模式”

概念

外观模式(Facade):为子系统中的一组接口提供一个一致的界面,此模式定义了一个高级接口,这个接口使得这一子系统更加容易使用。

结构图

代码模板

    class SubSystemOne
    {
        public void MethodOne() {
            Console.WriteLine(" 子系统方法一");
        }
    }
    class SubSystemTwo
    {
        public void MethodTwo()
        {
            Console.WriteLine(" 子系统方法二");
        }
    }
    class SubSystemThree
    {
        public void MethodThree()
        {
            Console.WriteLine(" 子系统方法三");
        }
    }
    class SubSystemFour
    {
        public void MethodFour()
        {
            Console.WriteLine(" 子系统方法四");
        }
    }

    //外观类,需要了解所有子系统的方法或属性,以备外部调用
    class Facade
    {
        SubSystemOne one;
        SubSystemTwo two;
        SubSystemThree three;
        SubSystemFour four;

        public Facade(){
            one = new SubSystemOne();
            two = new SubSystemTwo();
            three = new SubSystemThree();
            four = new SubSystemFour();
        }
        public void MethodA() {
            Console.WriteLine("\n方法组A()------");
            one.MethodOne();
            two.MethodTwo();
            four.MethodFour();
        }
        public void MethodB() {
            Console.WriteLine("\n方法组B()------");
            two.MethodTwo();
            three.MethodThree();
        }
    }  
        static void Main(string[] args)
        {
            Facade facade = new Facade();
            facade.MethodA();
            facade.MethodB();

            Console.Read();
        }

输出:

方法组A()------
 子系统方法一
 子系统方法二
 子系统方法四

方法组B()------
 子系统方法二
 子系统方法三

这样看来,外观模式其实很像JavaWeb项目三层架构中Service层和Dao层的关系,一个总的类去统领各式各样小的类,不断组合满足需求,总的类提供外部接口,让别的类调用外部接口。

何时使用外观模式

首先,在设计阶段的初期,应该有意识的将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层(DAO)和业务逻辑层(Service)、业务逻辑层和表示层(Web)的层与层之间建立外观Facade,这样可以将复杂的子系统提供简单的接口,大大降低程序的耦合性。

其次,在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。

第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,可以为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Facade对象交互,Facade与遗留代码交互所有复杂的工作。


 

 

 

 

 

 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

 

发表评论