设计模式的初衷---“委托”有感

很多设计模式的初衷,是“尽量少地修改既有代码”,能不动的,就不要去动。但是,如果你发现要实现新的需求,就不得不去修改既有代码,就说明这段代码该优化了 ,使其在再次发生这种变化时,不必再次修改。

例如,对于一个方法来讲,如果每当增加某个类别时,就需要修改这个方法,那么就不太对劲儿,也就是散发出“臭味儿”的时候,就是需要使用设计模式的时候。

很多设计模式,初学者无法理解,是因为还没有遇到那种场景。此时,一个合格“布道者”应当把那个场景描述出来,让初学者真正理解为什么应当使用这种模式,同时,要列举出来如果不使用这种模式,会生产哪些问题。而不是来一句“你以后就懂了”、“用多了就懂了”,这样的回复,没有任何意义。这样回复的人,其实自己没有真明白,似乎萦绕于心,但讲不出来,而能讲出来的才是真正理解。

很多人无法理解“委托”到底是干嘛用的。

这篇文章https://www.cnblogs.com/jimmyzhang/archive/2007/09/23/903360.html,讲得很清楚,但是如果不明白设计的初衷,仍然会觉得多此一举。

目前的理解,使用“委托”或“指针”的原因之一是“尽量少地修改既有代码”,提高封装度,降低耦合度,符合开闭原则(对扩展开放,对修改关闭,也就是可以扩展,但不允许修改)。

根据这篇文章所举的示例,如果不使用委托,那么每次有新的语言类别进行问候时,都要去修改GreetPeople(string name, Language lang)方法,增加if-else或switch的分支。有些人或许认为,这不算什么,工作量又不大。但这就是“坏味道”,这就是系统变得混乱的开始。极端一点,如果我们的系统是给全世界的人用的,全世界有多少种语言呢?据前德意志民主共和国出版的《语言学与语言交际手段指南》一书上说,当今世界已知语言共有5651种,公认的独立语言有4200种,其中100万以上人口使用的有19种。这个if-else或switch应该有多少个分支?

重要的是,这是一个坏的设计,耦合性太高,扩展性很差。想要扩展时,就必须要修改源码,无法独立封装。

下面就用此文作者所使用的例子,从写程序最原始的方法到需要使用委托的发展过程,一一展示。讲得比较细碎,是完整的思路和过程。

请注意,此处的几行代码,都可以理解为数百行代码,意味着有一定的工作量。

第一阶段,写给中国人用的系统。

该系统只有一个功能:问候某人。输出也只有一句话:你好,某某。

这个阶段,理所当然,只有一种语言,就是中国官方语言---中文。

具体动作直接写死在Main方法中。

 

    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("张飞,你好!");
        }
    }

 

第二阶段,系统用得不错,可以在英语地区销售,需要出英文版了。

为了能够表示这是什么版本,需要增加一个变量region。

    class Program
    {
        int region = 0;

        static void Main(string[] args)
        {
            if (region == 0)
                Console.Write("张飞,你好!");
            else
                Console.Write("Jack,hello!");
        }
    }

现在的系统是可以工作的,完美。

过段时间,水平提高了。发现其实用int region = 0这样的变量,不够明显,不能自我解释,每次看到这里时都要看注释或者思考一下它的作用是什么。

而且,还有其它风险,如果有人把region设置成了3会发生什么?

所以,此时应当用枚举。

中文版把region设置为LangType.Chinese,英文版设置为LangType.English,如下:

    enum LangType
    {
        Chinese, English
    }

    class Program
    {
        static LangType region = LangType.Chinese;

        static void Main(string[] args)
        {
            if (region == LangType.Chinese)
                Console.Write("张飞,你好!");
            else
                Console.Write("Jack,hello!");
        }
    }

再优化一下,Main方法中的代码应当尽量简洁,根据职责单一原则,不能把所有代码都写在Main中,该封装的要封装,该抽象的要抽象,自己负责自己的事儿。Main方法是把各个模块组装在一起的总成车间。

    class Program
    {
        static LangType region = LangType.Chinese;

        static void Main(string[] args)
        {
            if (region == LangType.Chinese)
                ChineseGreeting();
            else
                EnglishGreeting();
        }

        public static void ChineseGreeting()
        {
            Console.Write("张飞,你好!");
        }

        public static void EnglishGreeting()
        {
            Console.Write("Jack,hello");
        }
    }

系统可以正常运行。

不过,这个系统目前看起来似乎只能给“张飞”和“Jack”用,其他人怎么办?应当能够根据不同的人显示不同的名字,也能问候关羽、刘备、Hamingway。

所以,需要在每种语言的方法的参数列表中增加一个“name”参数,调用的时候传入使用者的姓名,这样就可以给更多的人使用了。

重要的是,只要在调用处传入使用者的姓名即可,不需要像此前的代码一样,每次都要去修改相应的方法体。

enum LangType
    {
        Chinese, English
    }

    class Program
    {
        static LangType region = LangType.English;

        static void Main(string[] args)
        {
            if (region == LangType.Chinese)
                ChineseGreeting("关羽");
            else
                EnglishGreeting("Hamingway");

            Console.Read();
        }

        public static void ChineseGreeting(string name)
        {
            Console.Write(name + ",你好!");
        }

        public static void EnglishGreeting(string name)
        {
            Console.Write(name + ",hello");
        }
    }

现在,在main方法中,仍然会有长长的条件判断语句,要么是if-else,要么是switch,不够简洁,总成车间中应当都是零部件的成品,不应当有生产各个零部件的机器和原料。应当把条件判断语句独立成一个单独的方法,就叫Greeting吧,同时,name和LangType也相应提到Greeting方法中。

class Program
    {
        static LangType region = LangType.English;

        static void Main(string[] args)
        {
            Greeting("Hamingway", LangType.English);
        }

        static void Greeting(string name, LangType langType)
        {
            if (region == LangType.Chinese)
                ChineseGreeting(name);
            else
                EnglishGreeting(name);

            Console.Read();
        }

        public static void ChineseGreeting(string name)
        {
            Console.Write(name + ",你好!");
        }

        public static void EnglishGreeting(string name)
        {
            Console.Write(name + ",hello");
        }
    }

这下清爽了。

一般来讲,多数程序,可能到此为止。如果再有新的语言加入,就增加if-else或switch分支,每次都要修改源码。

有些人可能认为,修改源码并不费多大事,加个分支也没有多大工作量,这不就是我们码农的工作么。

每当我们有这种想法的时候,就再深入思考一下,如果是团队合作,如果是公司间合作,如果我们是组件生产者,如果我们是API开发者,会怎样?

如果我们是公司内部的团队合作,我们是负责写Greeting方法的团队,其它团队是Greeting方法的使用者。团队间以dll作为产品。我们肯定不希望每次增加一种语言的时候,就要改一次源码,然后重新发布一个dll给别人,这也太low了。

团队间合作还好,至少还是一个公司的,可以互相迁就一点。公司间合作就比较严肃了,如果发现每次增加一种语言的时候就要等着我们修改方法,重新发布dll发给他们,这耽误的时间算谁的责任呢?

如果我们是组件或插件开发者,当用户发现无法支持他们的语言时,他们往往会用脚投票。

所以,“每次增加语言时就要修改源码”,这种适应能力肯定还不够完善。

我们要争取在调用的时候,就决定了向谁、用哪种语言问候,且不需要改动源码。

我们来分析一下,Greeting("Hamingway", LangType.English)方法,可能会导致修改源码的是哪部分,name参数肯定不是,给它传什么都可以,只要是字符串,它不会导致修改源码。所以会导致修改源码的是语言的变化,每当增加语言的时候,一定会导致源码的修改。所以需要改进的是传入语言的方式。

怎么改呢?

设想一下,如果能有一个方法,能像参数一样传给Greeting,并对传入的name参数所代表的人进行问候,而这个方法本身,就决定了用哪种语言进行问候,这个问题就迎刃而解了,不需要修改源码。像这样Greeting(string name, **** GeneralGreeting),此处的GeneralGreeting是一个新的方法,表示“通用的问候方法”。

那么,能实现吗?答案是肯定的。

根据我们现有的知识,方法的参数,都是“类型”或自定义的“类”,要么是系统自带的int、string、bool等数据类型,要么是我们自己定义的类。那么,如果我想让参数类型是个方法,怎么弄?

这就是“委托”的用武之处。把那个****位置定义成“委托”。

委托的本质,就是类,所以它可以用来定义变量,只是它的变量比较特殊,是“方法”。我们对此可能感有点别扭,变量不都是名词嘛?怎么成了一个动作?但是对计算机来讲,都是地址,都是代码块,没有太大区别。本文不用指针来解释委托。把某“方法变量”定义成“委托”,它就可以代表一类动作。

我们继续推导,如果这个“委托”GeneralGreeting能够接收不同的方法,那么它应该也能接收那些方法的参数列表,这个“委托”也应当与那些方法具有相同的参数列表,否则无法处理那些参数。至此,一个委托的定义就呼之欲出了。

delegate void GeneralGreet(string name);

这样就定义了一个委托,时刻记住,它就是类,定义的时候也是可以与类class平行的,可以用来定义变量。

它的结构,与ChineseGreeting(string name)和EnglishGreeting(string name)样的,GeneralGreet就是给它们准备的。

那么我们前面的方法就可以改造成:

Greeting(string name, GeneralGreet generalGreet )

此处是一个关键,一定要想清楚。

完整的定义如下:

        public static void Greeting(string name, GeneralGreet generalGreet)
        {
            generalGreet(name);

            Console.Read();
        }

怎么用?

调用方只要给generalGreet传入以任何名字命名的,带有一个字符串参数的方法,即可。此处是ChineseGreeting(string name)和EnglishGreeting(string name)两个方法。

 

delegate void GeneralGreet(string name);

    class Program
    {
        static void Main(string[] args)
        {
            Greeting("张飞", ChineseGreeting);
        }

        public static void Greeting(string name, GeneralGreet generalGreet)
        {
            generalGreet(name);

            Console.Read();
        }

        public static void ChineseGreeting(string name)
        {
            Console.Write(name + ",你好!");
        }

        public static void EnglishGreeting(string name)
        {
            Console.Write(name + ",hello");
        }
    }

 

其中的ChineseGreeting(string name)和EnglishGreeting(string name)两个方法,可以放置到其它专用的语言类中,或者干脆就是“与我无关”,它可能是调用方关心的内容,作为基础服务提供方,我只设置一个“带有一个字符串参数的委托”,至于要用这个委托做什么,是调用方的事,可以是问候,也可以是鞭打。

第三阶段,业务发展更大了,需要支持更多的语言。

好了,现在调用方要加一个西班牙语的问候,只需在他们的语言类中增加一个SpanishGreeting(string name),然后像其它语言一样调用就可以了:

 

   delegate void GeneralGreet(string name);

    class Program
    {
        static void Main(string[] args)
        {
            Greeting("张飞", ChineseGreeting);
            Greeting("Don Quijote", SpanishGreeting);

            Console.Read();
        }

        public static void Greeting(string name, GeneralGreet generalGreet)
        {
            generalGreet(name);
        }

=========================================================================================
        public static void ChineseGreeting(string name)
        {
            Console.Write(name + ",你好!");
        }

        public static void EnglishGreeting(string name)
        {
            Console.Write(name + ",hello");
        }

        public static void SpanishGreeting(string name)
        {
            Console.Write(name + ",Hola");
        }
    }

横线以上,可以放在一个类中,横线以下,可以放在另一个类中,清清爽爽。

 

 delegate void GeneralGreet(string name);

    class Program
    {
        static void Main(string[] args)
        {
            Greeting("张飞", ChineseGreeting);
            Greeting("Don Quijote", SpanishGreeting);
            Greeting("督邮", whip);

            Console.Read();
        }

        public static void Greeting(string name, GeneralGreet generalGreet)
        {
            generalGreet(name);
        }


        public static void ChineseGreeting(string name)
        {
            Console.Write(name + ",你好!");
        }

        public static void EnglishGreeting(string name)
        {
            Console.Write(name + ",hello");
        }

        public static void SpanishGreeting(string name)
        {
            Console.Write(name + ",Hola");
        }

        public static void whip(string name)
        {
            Console.Write(name + ", 挨了刘备一顿鞭子!");
        }
    }

 

 

 

以下的解释,也不那容易理解,还是需要一定程度的前期输入和理解。也不是特别形象。

在中文语境中,委托和代理,是密不可分的。委托,是一个动词,代理是一个名词。A委托B这个代理去做一件事。也就是中介。中介的价值在于,专业的人做专业的事。房产中介存在的意义在于,我想买房,但我不知道哪里有好房,而中介知道。那么中介是怎么知道的呢?因为有人要卖房的时候也会找房产中介。

同样的场景替换至本例,作为基础服务提供方(Greeting),我提供问候服务,甲方付我钱,让我去问候一个人(调用)。做这件事有两种方法,一种是我亲自去,或者我公司的人亲自去,这就是使用delegate之前的场景,全部自己解决,每次有新的语言加入,就要让公司内部的人学一种新的语言(修改源码),这太累了。随着业务的扩大,中文和英文的我公司自己还能应付,现在有葡萄牙客户也需要提供服务了,这已经超出我们的业务范围,所以,最好的办法是专业的人做专业的事。有些语言我们不懂,但有人懂,去委托一家代理就可以了。客户只要告诉我们的代理需要用什么语言进行问候(PortgualGreeting)就可以了。

 “委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。

 

posted @ 2020-05-02 13:47  火军刀  阅读(508)  评论(0编辑  收藏  举报