设计模式最后一击

来源:互联网 发布:健身大数据 编辑:程序博客网 时间:2024/04/28 13:56

关于这篇文章

        设计模式是每个面向对象开发者必备的知识,是面向对象设计的经验之谈。但是设计模式对于工程人员来讲又是模糊的一门技术,因为他没有严格的数学分析与证明,是实践检验下的合理的经验,对于初学者尤其难理解。笔者也曾经困惑于此,随着工程实践的增加,越来越对设计模式明晰起来,因此想写一篇最终的总结,将我对设计模式的理解简洁的写下,力求一针见血,以后便不再纠结于此,也供其他朋友参考、指正和交流。

设计模式基本准则

        虽然设计模式是实践下的产物,但是在最初设计时也是遵循了一些共性的原则性的东西,笔者认为设计模式明显基于三个准则,或者说总目标:

  • 开放封闭原则:允许新增扩展系统,但拒绝修改系统
  • 隔离复杂和变化:运用各种手段将复杂聚集到一个相对集中的类或者模块,而不是出现在系统的各个地方
  • 可复用性:实际上这也是23种设计模式那本经典著作的标题,虽然没人能设计出能任何情况下任何时候都能复用的模块,但是设计模式力求在复用性上做的更好。

        看到这里也许有人会问,为什么没有那5条面向对象设计原则?什么单一职责、Liskov替换之类的。实际上,所有这些原则在设计模式中都有所体现,但这些原则并不是最核心的东西,只是实现最核心东西的手段。比如,为了实现可复用性这个目标,可能会借鉴单一职责原则和接口隔离原则等。

        实际上,如果我们仔细观察就会发现,那个经典著作在每种模式之后的优缺点讨论就集中体现了上面的三个准则。

设计模式评述

        看过设计模式如果细心的话会发现有些设计模式在类图上是类似的,有的甚至完全相同,不同的仅仅是场景和类的职责。首先探讨一下归纳的三个类似结构。

兄弟依赖型

        在种类型中,有两个具体的类(非抽象)A和B,其中A依赖于B以实现某些功能,A和B可以实现共同的接口,也可以实现各自的接口。


拥有上述类似结构的是Adaptor模式和Proxy模式。

        Adaptor模式的类结构和上图完全相同,A就是适配器(Adaptor),而B是被适配的接口的一个实现(Adaptee)。这个模式很常用,动机是Client常常需要特定的接口IA,而手头现有的接口IB并不是Client所需要的。直接的想法是让B再实现接口IA,把IA中得方法实现一遍。在这种暴力的方案中,适配器A是多余的。下面用上文提到的三个准则分析一下为什么适配器要好于这种简单暴力方案。

        首先,开放封闭原则要求遇到变化时扩展类,而不是修改类。而在简单方案中,我们不得不修改B的代码,实现IA接口。

        其次,好的设计要隔离复杂和变化。在简单方案中,如果另一个客户端Client2需要另一种接口IC,那么B不得不还得实现IC,极端一些如果我有100个客户端,每个都要自己的接口格式呢?这样想见B类已经无法直视了。

        最后一个准则要求模块尽量可复用,那么,实现了100个不同接口,而且接口具体定义的功能又类似的B可以复用么?

        经过分析我想已经高下立判了。最后关于Adaptor模式要说的是很多书中还提及一种基于继承的模式,也就是适配器A不再依赖B,而是继承B。对于这样的形式,我只想说,组合优于继承。

        Proxy模式的类结构更为简单,IA和IB接口实现上是一个,也就是说A和B实现了相同的接口ICommon,A是代理类,B是被代理类。这种模式的主要动机是在被代理的具体类基础上做一些额外的功能或操作,但是对外暴露的接口是不变的。

        在Java中,上述类图描述的代理模式实际上成为静态代理,而Java支持一种更为灵活的动态代理模式。在类图上看,动态代理模式唯一的区别是代理类A并不再直接依赖于被代理类B,通过Java反射机制,A类可以控制被代理的类的方法在任何时候调用,而不必关系自己到底代理了什么类或者什么方法,这样A可以更加专注于自己额外的代理操作,同时也消除了A和B的直接依赖。

        动态代理的威力更加强大,想像一下,如果你100个类或者100个方法要代理,你不得不相应的创建100个静态代理类,而这些静态代理类实现的额外操作有可能是一样的。动态代理广泛用于框架和底层类库中比如spring中各种sql template就是动态代理,例如Spring集成Mybatis时SqlSessionTemplate动态代理了Mybatis中得SqlSession。但是,在简单场景下使用静态代理会显得更加简洁,也没有反射带来的性能损失。当然,因为看着牛逼而在任何情况下都使用动态代理我也没话说。

层级结构型

        这一类的设计模式最终在对象结构上呈现一定的层级关系,这些模式包括 Composite、Decorator和Interpreter,典型类图如下:

        上面的类图实际上Composite模式的完整描述,一个Composite对象可以组合一个或者多个Component,这些Component既可以是另一个Composite实例,也可以是Leaf实例,如果是Composite实例,那么便形成了树状层级型的嵌套结构。

        在软件开发中,这样的结构随处可见。在java图形界面开发中,有Button、ComboBox这样的控件,对应于上图中得Leaf,而widget、Group这类容器控件对应于Composite,容器控件可以嵌套,包含Leaf控件,从而设计出各种复杂界面元素;此外,Java中的JSON和XML文件处理库在表示相关结构时也需要构建这样的层级嵌套结构。

        Interpreter模式可以认为是Composite模式的一个在语法解析场景下得具体应用,因此我个人认为Interpreter模式可以不必单独列为一个设计模式。在Interpreter模式中,Leaf就是某个语法的终结符,而Composite则是非终结符,非终结符可以嵌套,因此对象之间可以形成一个具有层级关系的语法树。

        至于Decorator模式,它可以被认为是Composite模式加上一定限制下的变种。在类图上,Decorator模式中得Decorator即为Composite,而Leaf类则是被装饰的Target,唯一的限制是Decorator只能组合一个Component接口实例,而在Composite中,Composite可以有多个Component接口实例成员。这最终导致了对象之间结构的不同,Composite模式中对象可以形成嵌套的树状层级结构,而Decorator模式中对象只能形成一个单链表(当然也可以认为是一颗特殊的树)。

        有些开发者也将Decorator模式称为Wrapper模式,是为某个接口增加额外的功能表现。之前提到的Proxy模式也是增加额外操作,那么两者有何区别呢?分析区别的还是要从类图源头找起:Decorator依赖的是抽象的接口,是为抽象的接口提供额外实现,而不关心被装饰的具体类;而Proxy代理的不是抽象接口,而是实实在在的具体类的具体方法。因此,到底在应用中选择Decorator还是Proxy,关键要看你想代理或者包装的是什么了,是抽象类型、接口还是具体类?

普通组合型

        普通组合型在类结构上比较简单,客户类组合一个接口的实现,而接口有若干实现,根据实现的不同,客户类可以灵活的获得不同的行为,实际上这是面向对象多态性的一个 最简单的演示:


        这一类型的设计模式有State模式和Strategy模式,以及Flyweight模式。

        State模式和Strategy模式在类图上完全一致,区别主要是使用场景,State模式将每种状态封装为一个具体类,而Strategy模式将每一种算法实现封装为一个具体类。这样的解释正确,但是有些浅显,如果再进行深入观察和分析,实际上这两种模式在类图结构和类交互细节上并非完全一样。

        在State模式中,每一个状态对象在完成action方法后会返回另一个状态对象,以表明状态之间的转换,也就是说,ConcreteA和ConcreteB对Interface都是有依赖关系的,那本设计模式经典著作中并没有给出这个依赖。此外,在Context对象的request方法中,每次调用接口的action方法后,都要更新Context对象持有的状态对象,以表明自身状态的改变。

        相比之下,Strategy模式则要简单一些,ConcreteA和ConcreteB中得action方法一般不会产生对父接口或者彼此的依赖,仅仅是算法的一种实现。而Context对象中持有的Interface实例一般是在运行之前通过依赖注入设置好的,运行时一般不会改变。

        此外,大家不要被Strategy模式中的策略、算法之类的名词吓到,这个模式在Java语言库中也是随处可见的,比如我们常用的带自定义比较器集合排序Collections.sort,以及Java IO库中的通过过滤器过滤并列出子文件的方法File.list(FilenameFilter filter)和File.listFiles(FileFilter filter),以及Apache Collection库中CollectionUtils类中的一堆静态方法。我个人认为目前流行的函数式编程中将函数作为参数传递的做法本质上就是Strategy模式。

        有些初学者可能会问了,使用这两种模式需要创建很多类,那他们究竟有什么好处呢?就拿State模式来说吧,如果不用这个模式,那么如何实现对象内部不同状态的转换呢?无非就是定义一堆枚举值,然后if-else满天飞,或者switch-case中各种处理函数,直观感觉就很恶心(如果你是C程序员,看着if-else就舒服那我也没办法)。当然,吐槽不能说服别人相信这些模式的优点,还需要大家自己利用第一章阐述的三个准则严格的分析这两种模式的优势何在?

        Flyweight则是这种结构在共享池方面的一种应用,只不过Context变成了Factory,并持有一个Map保存多个Interface实现对象。Factory提供get方法获取指定的实例对象,如果不存在,Factory则初始化合适的Interface实现。

一个接口和一个抽象类

        上面提到的State模式和Strategy模式本质上都只是通过面向对象语言普遍支持的多态性实现,实际上还有更简单的模式。

        定义一个接口方法clone,再让创建一个类实现它,就是prototype模式,结构简单吧。虽然结构简单,但是语义其实并不简单,你需要创建一个和对象完全一样的另一对象,如果对象复杂的话,还需要进行深拷贝和浅拷贝的取舍。Java的Object对象本身就支持这种模式,你只需要重写clone方法,但是就像重写Object的equals方法那样,看着很简单,然而没看过《Effective Java》的话,又有多少人能滴水不漏的正确写出呢?

        定义一个抽象类,定义一些抽象方法和非抽象方法,然后创建一个类继承这个抽象类,实现这些抽象方法,这就是Template模式。看似同样简单的Template模式背后的思想还是很值得咀嚼的,它试图将复杂功能或者流程的实现,简化为若干小功能的实现,从而减轻复杂功能或者流程实现者的难度。

        Template模式在Java语言库中随处可见:如果你想实现一个自己的链表,你会从头实现Java的List<E>接口么?如果这样做,你需要实现包括size、get、add、contains等在内的20多个方法。更简单的方法是直接继承自Java的AbstractList<E>抽象类,这样你只需要实现五个方法size、get、set、add、remove五个方法,而其他方法都可以通过你已经实现的这五个方法默认实现,AbstractList已经为你做好,你只需要继承下来即可;另一个例子来自JavaEE,实际上Servlet本身就是Template模式的应用,想象一下,你是如何处理Get请求和Post请求的?继承一个HttpServlet,然后重写doGet和doPost方法,完全不必关心Get和Post的流程到底是怎样的,你所写的doGet和doPost方法只不过是Servlet容器复杂处理流程中得一个环节而已。

服务的创建

        创建型模式是单独的一类设计模式,主要关注对象的创建,相对比较容易理解。有些初学设计模式的开发者对其不以为然,因为new不就能搞定一切么?new确实是创建对象功能最强大的方式,在某些语言(Java)中甚至是唯一方式。但是new最大的缺陷是与特定类型紧密绑定,在一个大型系统中,往往类型结构复杂,类型众多,众多的并且在不断演化的特定类型往往成为复杂点,这样new便与这种复杂紧密耦合在一起。第一章的准则要求我们隔离复杂和变化,因此,利用创建型的各种设计模式封装new还是必要的。

        单例模式是重要的设计模式,不仅现实世界中有很多场景是单例的,其他设计模式中也经常需要单例的配合支持。Java对单例模式没有提供直接支持,通常的实现是利用Java的类生命周期创建一个类范围的static成员,因为类在Java加载时时唯一的,因此这个static成员也是唯一的。然后需要做得就是封闭这个static成员的类构造方法,提供static的get方法获取这个单例成员。

        需要注意的是,如果你将实例化static成员变量延迟到get中时,需要考虑多线程下得线程安全问题,简单的可以在static get方法前加同步。如果你是性能控,也可以使用“双重锁定检查惯用法”优化一下。

        单例模式现在得到了新兴语言的直接支持,比如scala提供了伴生对象机制让你可以直接创建一个单例对象。Java虽然间接支持,但是仍然有一个疑问,如果不使用类生命周期和static静态作用域,如何实现单例模式呢?

        Builder模式将一个对象在使用之前的设置封装到一个Builder实例对象中。Builder模式的原始动机是简化一个复杂对象的构造过程,但是我并不完全赞同这个说法,因为有时一个复杂对象在使用前需要设置十几个字段,而用Builder对象封装之后,你仍然需要设置这些字段,工作量并没有减少。

        我认为Builder模式的主要动机是让复杂对象职责变得更加单一,如果一个复杂对象具有十多个set方法,真正的Service方法才两三个,你很难搞明白这个对象到底是干嘛的。因此,把一个Service对象的初始化和设置过程放到Builder对象中,一旦获得Service对象,它将不可变,只是提供业务服务,这样Service对象将会简单很多,其职责也会更加专一。

        给出两个Builder模式的典型应用:一个是Google Guava库中LoadingCache对象的创建;另一个Apache HttpComponents 4.3以上版本中HttpClient对象的创建。

        工厂方法模式和抽象工厂模式都带工厂两字,因此放在一起讨论。工厂方法模式我 们经常用到,但这个模式有些微妙之处,首先看下原始文献如何描述这种模式:


        原作者的动机是希望利用工厂方法模式达到两点目的: 

  • 封装Product实例对象的创建,解除Product使用者对Product具体类型的依赖
  • 将具体Product子类的创建延迟到相应的子工厂类,让各个Product子类共同的操作提到父工厂类的方法中(Creator的AnOperation方法)

        然而,我们通常所见的工厂方法模式并不是上图这种形式,而更多的是将Product子类对象的创建封装到Factory类的静态方法中,而Factory本身没有层级关系。以Java并发库中常用的线程执行器为例,其类图如下:


        作为工厂类的Executors并没有继承的层级结构,而是把各个ExecutorService的实现的对象创建作为自己的静态方法处理,这样,原作者的两条动机只实现了第一条,这实际是原版工厂方法模式的简化版。对于这两种工厂方法模式,显然后者更简洁,但是缺少某些优点,但是如果你不需要这些优点时,那么选择简洁的方案则更加明智,所谓存在即合理。

        抽象工厂模式和原版的工厂方法模式很像,Product和Factory都有继承层级结构, 如下图:


        抽象工厂和原版的工厂方法最大的区别要从Product分析起。在抽象工厂模式中,Product有多个相对独立的层级体系,像上图的AbstractProductA和AbstractProductB,而且最为关键的一点是不同产品体系中的具体产品有某种关系,比如ProductA2和ProductB2可能有某种关系,比如这两个产品都是Simple的或者这两个产品都来自同一厂商。这样,为了更好的体现这一关系,我们将这两个产品的获取方法封装到同一个抽象工厂类实现中,即ConcreteFactory1。

        那么问题来了,如果AbstractProductA和AbstractProductB的各个子类完全没有关系,那么该怎么办?还能使用抽象工厂模式呢?这时再使用抽象工厂模式把两个无关的Product实例创建硬塞进同一个工厂实例中显然是不合适的,这种情况的解决方法简单而暴力:为AbstractProductA和AbstractProductB分别使用工厂方法模式搞定!

职责提炼

        单一职责是实现第一章的三个目标的重要手段,职责的单一使得每个类的粒度变小,更加容易复用;一个类一个职责也使得不同的变化和复杂性不会互相影响;单一职责也使得修改的代价变得更低。围绕单一职责,原作者专门设计了几个设计模式,以Bridge、Iterator、职责链、visitor模式说明。

        按照官方的说法,Bridge模式用来分离接口和实现。接口和实现本来就是紧密耦合 的,但是当接口出现了一个子接口时,情况就变糟糕了,就像下图这样:


        本来RoleA有两个实现,RoleA1和RoleA2,现在又出现了一个子接口RoleB,导致实现RoleB的时候,RoleB继承下来的method方法的两个实现也要重新写一遍,就出现了这四个恶心的实现类。

        说到这儿一个问题马上就出现:在Java语言库中这样的结构比比皆是,比如接口Collection有子接口List,List底下还有个抽象模板AbstractList,而最底层具体的List实现又有很多,那么为什么Java集合库看起来那么优美,完全没有上面的问题呢?

        要回答这个问题,就得回到单一职责这个主题上了。导致上面类图中得问题的根源在于RoleA和RoleB在业务上的继承关系并没有那么紧密,或者说是两种没啥关系的角色。我们知道继承是耦合性最强的一种方式,当RoleB和RoleA的关系没那么亲密的时候,硬要继承就会出问题,这时RoleB“很不情愿”的继承了RoleA中和自己没啥关系的method接口方法。然后在子类实现中,悲剧就发生了,method和operate方法完全需要各自的实现,然后最后又不得不组合起来,就形成了四个恶心的实现类。实际上,这四个实现类的职责也都不单一。

        在反观Java集合库,List和Collection就是紧密的“is-a”关系,List从Collection接口继承的接口方法连同自己的方法形成一个密不可分的整体,具体的实现类完全感知不到哪些方法属于自己的爷爷Collection,当然也就不会出现上图的情况了。

        既然问题出在RoleA和RoleB上,那么解决办法还得从这两个接口找起。既然RoleA和RoleB有关系,但是还没有亲密到那种程度,那么就用稍微弱一点的组合代替继承,即RoleB组合RoleA,而不是继承RoleA。这样一来,RoleB的实现类就不需要实现method方法了,因为RoleB根本就没有method方法,RoleA和RoleB的实现类只需要实现各自的接口方法即可,类图如下:

 

        这个类图是不是很像设计模式原书中的Bridge模式类图?原书中的RoleB就是所谓的Abstract抽象,而RoleA是所谓的Implementation实现,原书的Bridge模式是将类体系中的抽象和实现分离,使其各自变化。我认为Bridge模式是将继承体系中不同职责分离,使其各自变化,这才是Bridge模式更一般更本质的表述。

        职责链模式在数据流处理场景下的典型模式。在职责链中的每个对象都持有下一个对象引用,处理完数据后将数据送入下一个对象进行处理。这是很简单明了的职责分离,一个对象负责一个职责,职责之间有先后顺序,职责对象形成链状结构。

        还记得前文中描述过的一种可能具有链式对象结构的设计模式么?对,就是装饰模式,装饰模式中的各个装饰器对象也具有链状结构,区别是装饰器模式不是针对数据处理,而是每个装饰器对象是对下一个装饰器对象的功能增强或者修改。

        JavaEE中包括职责链模式的典型应用,即Filter。你可以在web.xml中配置多个Filter对象,并指定先后关系,每个Filter对象在处理完request后调用chain.doNext,让下一个Filter对象处理,知道request到达Servlet实例。

        Iterator模式就不必多解释了,Java集合类库诸如List、Set、Map等接口都支持该模式。这个模式容易理解是因为它应用场景单一,职责明确,就是将集合的迭代操作分离出来,定义成一个Iterator接口,并规定好接口中各个方法的调用规范,比如iterator.next和iterator.remove的调用前后必须调用iterator.hasNext方法。也许有人会问,不用Iterator我也可以迭代List啊,说到这,就必须提提我认为迭代器最重要的两点存在的意义了:

  • 提供一致的方法为各种集合迭代:你当然可以用for循环迭代List,但是如何用for循环迭代Set和Map呢?
  • 允许在迭代中对集合进行破坏性操作:所谓破坏性操作一般就是删除元素,你也可以在for循环中判断并删除当前index索引处的List元素,但是你得好好计算一下迭代变量i和删除后List元素长度关系,小心索引下标越界或者遗漏元素喔。

        然后谈一下号称设计模式中最难理解的Visitor模式。首先从高层审视一下这个模式,前两种模式分别分离对数据的不同处理和迭代这两种职责,而Visitor模式分离了什么职责呢?Visitor分离了对一个复杂对象的“不靠谱”的修改。

        面向对象范型首先讲究的就是封装性,即把数据和对数据的操作封装在一起,而Visitor模式却要将对一个复杂对象的修改操作拆出来并封装到另一个Visitor对象中,这岂不是破坏了面向对象的封装性?没错,Visitor模式确实破坏了封装性,这是设计模式著作中明确指出的Visitor模式缺点,但是这个破坏却不得不做,原因就是上段中写出的修改的“不靠谱”。

        为了解释“不靠谱”,假设你从数据库中获取了很多数据,构建了一个StockModel的对象表示股票行情,这个对象结构足够复杂,包含了很多股票属性和层级结构,你打算将这个对象提供给展示层的JSP展示。这时,操作的“不靠谱”体现在如下两点:

  • 操作很特殊:有一天,产品经理突然让你把StockModel中的某个字段临时调整一下,由于种种原因,你无法修改数据库中的数据,只好通过代码完成。
  • 操作增加频繁:一旦你的StockModel被产品经理盯上,你对它的特殊处理便没完没了。

        这种不靠谱的操作确实和StockModel紧密相关,但是你如果把这些方法真的作为StockModel的方法,那么问题就来了。虽然这些操作和StockModel相关,但是都是些只有产品经理才懂的特殊处理,你把这些方法当成StockModel的公开方法看起来很怪异。另一方面,特殊处理频繁增加让StockModel的方法和代码越来越多,一个拥有上千行代码甚至上万行代码的糟糕类分分钟就诞生了。

        解决方法就是Visitor模式:定义一个Visitor接口,接口方法就一个visit(StockModel model),每有一个特殊处理就创建一个Visitor实现,将特殊处理过程写到visit方法中,达到分离特殊处理职责的目的,这样来一个新的特殊处理无需修改StockModel,而是创建新的Visitor实现,完美符合开放-封闭原则,思想和方法就是这么简单。

        那么号称最难理解的Visitor模式到底难在哪儿呢?先看一下设计模式中Visitor的标 准类图:


        这个图和我的例子的主要区别是,在我的例子中,被访问的对象就一个即StockModel,而图中有两个ConcreteElementA和ConcreteElementB,并实现了共同的接口Element,因此在Visitor中需要针对这两个类分别提供接口以进行特殊处理,这很好理解。最难理解的是Element及其实现中定义accept方法,这个方法接受Visitor参数并调用相应方法修改自身。这个方法也困扰了我很久,为什么Element及其实现要反过来依赖Visitor呢?我的例子中StockModel没有依赖Visitor也没有什么不妥嘛?

        不理解accept方法存在的意义,我们不妨把accept方法去掉,看看有什么后果。去掉之后,上图下半部分的ObjectStructure、Element及其实现便不再依赖Visitor。这里有一个重要的细节:Client依赖ObjectStructure,而ObjectStructure依赖于Element接口,而不知道自己持有的对象是Element的哪个实现的实例。如果你看出这个细节,那么问题马上就能看出来了:当Client或者ObjectStructure将Element对象扔给Visitor对象进行处理的时候到底选择Visitor哪个方法执行呢?Client和ObjectStructure根本就不知道Element对象属于ConcreteElementA还是ConcreteElementB啊!

        这下你知道accept方法的作用了吧。在我的例子中,StockModel是唯一的复杂对象,没有兄弟类,因此Visitor只有针对唯一一个接口,针对此类对象的访问,accept方法当然也就没有存在的必要。因此设计模式没必要照本宣科,抓住精髓结合自己的场景随机应变才是王道,在被访问对象只有一个类时,accept该省就省,能去除一个方向的依赖,这样类结构还能变得清晰一些呢。

隔离复杂解耦通信

        上一节介绍了设计模式在明晰面向对象各个部分职责方面的贡献。本节介绍设计模式在隔离复杂和变化,尤其是通信调用关系方面的努力。涉及的模式有Facade、Mediator和Observer。

        Facade模式几乎是第三方类库实现时必用的模式。一般第三方类库会包含很多功能,这些功能通过方法调用完成,这些方法也会分散在各个类、接口中,如果不提供一个统一的接口,用户的使用难度将大大增加,如果再没有完善的文档,你的库是没有人会用的。Facade模式就是提供类库的统一入口,隔离在多个入口对用户的复杂性。它的具体形式可以很灵活,可以是一个Factory、一个接口、一个单例、甚至是一个类+一堆静态方法(比如fastjson库的入口JSON类)。在类库中出现的***Client、***ServiceFactory、***Factory、***Facade之类的都可以看做是门面入口。

        需要澄清的一点是Facade模式仅仅用于统一入口,并不意味着用户自始至终都只和类库的Facade对象打交道,因为一个类库大话动辄成百上千个功能,把这些功能接口全放在一个对象里用户会舒服么?以使用者的角度,让调用者在Facade类中轻松获得各类功能的接口实例或者所需的对象才是比较好的应用方案。

        按照设计模式著作的原始描述,Mediator模式用于隔离有相互通信或者调用的两个模块,因为如果模块接口过多,或者接口不稳定,模块间的相互调用会变得很混乱,甚至出现危险的调用死锁(三个模块循环调),引入Mediator,调用和通信将变成一个方向,即依赖Mediator,调用和通信将变得可控。

        尽管如此,Mediator的缺点也不容忽视:Mediator实际上把通信的复杂隔离并承担在了自己头上,Mediator本身将变得很复杂,它的设计和维护成本也是不容回避的。我的建议是利用约定优于配置的原则尽量避免使用此模式,尽量避免模块间的相互通信,而是让通信变成一个方向,变得有序,必要的时候也可以引入消息队列等中间件或者使用下文将提到的观察者模式让这种通信变得有序。你需要意识到模块之间无序的相互通信本身就是不太好的设计,应该重新规划和设计,而不是引入一个Mediator了事。

        本文最后讨论一下MVC的基础观察者模式。观察者模式实际上就是发布-订阅模型,发布者Model将数据发往订阅者view,而无需关心订阅者有多少个,以及订阅者的具体类型,而订阅者需要依赖发布者,因为要将自己注册到发布者中。

        观察者模式隔离的是整个观察者,因为观察者是复杂的和易变的,经常会有新的视图、新的观察者出现、或者展示上进行各种调整;而数据模型的结构是相对稳定的。观察者模式由此也展示了一个重要的设计原则:易变的要依赖稳定的,而不是相反。因此,我们可以联想到并不是所有的Model-View场景都合适用观察者模式,如果Model的结构和接口、以及返回的数据结构老是变化,视图反而相对稳定,可以想象一下还能否用观察者模式或者MVC模型。

        至于这个模式的应用就不多说了,就算自己从头实现也不难。Java语言通过其标准库对观察者模式提供直接支持,继承一下Observable,实现一下Observer就妥妥的搞定了。

结语

        虽然本文的标题是设计模式最后一击,但是仅仅意味着对传统的设计模式总结告一段落,并不意味着从此不再关注任何模式和设计。相反,随着新技术新思维的涌现,一定会产生新的模式适应这种变化,比如分布式计算领域、并行编程领域、性能方面、函数式编程方面等。甚至随着自己经验的增加,自己也可以尝试总结新的模式。总之一句话,要活到老,学到老。

        本文中没有提及的Command模式和Memento模式等在GUI软件开发中较为常见,笔者在自己的Web领域鲜有涉及,因此不敢造次。还望各位个中行家里手多多补充,权当笔者在此抛砖引玉。

1 0
原创粉丝点击