zoukankan      html  css  js  c++  java
  • 设计模式的初衷---“委托”有感

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

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

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

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

    这篇文章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)语句,同时使得程序具有更好的可扩展性。

  • 相关阅读:
    Java 项目运用个人看法(简写)
    windows 搭建Solr连接数据库
    总结2016年,计划2017
    如何解决,自己认为特别难的问题?(文摘)
    spring -quartz 定时任务多任务配置
    (转) java Timer 定时每天凌晨1点执行任务
    spring多数据源切换,写入报错的问题
    如何合理和有效的进行数据库设计
    Main方法定点执行线程任务
    莫辜负当下,莫悔恨过去,莫打扰错过的人
  • 原文地址:https://www.cnblogs.com/Sabre/p/12817885.html
Copyright © 2011-2022 走看看