开闭原则和设计模式
在《敏捷软件开发--原则、模式与实践》书中给出了多个OO设计原则,例如单一职责、开闭原则、依赖倒置原则等。在这些原则中,绝大多数都很容易理解,最难理解的恐怕算开闭原则(对扩展开放,对修改关闭),主要原因是它太抽象,是一个比较本质比较基础的设计原则,超出了初学者的理解范围,如果能有一个实际的具体例子来说明就容易理解了。
其实在设计模式中,就有这样的一些模式很好的直接体现了开闭原则。
先暂时岔开一下说说设计模式,设计模式的流行应该是归功和发端于*的《设计模式--可复用面向对象软件的基础》一书的出版,自此设计模式才开始为IT大众所知晓。
对设计模式的学习,不仅要知其然还要知其所以然,也就是不仅要了解常用的设计模式,还要在思想上升华对设计模式的理解,要对设计模式有更高层次的理解。高层次的理解:设计模式是对系统需求变化的一种优雅封装手段。唯一不变的就是变化,变化是客观的,通常是很难避免的,通过这种优雅的封装,也提高了代码的可维护性。
怎么应对变化?其实在日常工作生活中就可以找到应对变化的手段。对各种气体(如气体喷雾剂)和液体(如水),由于它们没有固定的形状,也就是容易变化,所以我们需要用容器(瓶瓶罐罐)将它们封装起来,隔离屏蔽起来,否则任由它们四处蔓延扩散,肯定会把周围的环境污染得一滩糊涂,难以收拾。为了防范洪水猛兽犯人,人们修建各种设施(水库、铁笼子、*、篱笆栅栏)将它们关起来,局限隔离起来,约束起来。
类推到软件中也是这样,软件中的变化类似洪水猛兽,也需要进行封装隔离,如果让变化任意到处蔓延,污染其他的代码,那么受污染的代码就和容易变化的代码产生了较强的耦合,耦合太强太多,就变成意大利面条了,导致有新的变化时,代码难以维护,一旦后面要拥抱变化,那就悲剧了:开发人员要到处查找受污染的代码进行修改,少修改一处就出bug,除了修改原来的代码之外,有时还要增加很多分支逻辑和方法,导致整个代码没有条理和秩序,很凌乱,难以维护。
因此我们应追求低耦合高内聚,减少依赖。对容易变化的代码,就像日常生活中那样,将变化封装隔离起来,对它进行管束&限制,隔离起来,不让变化到处扩散,减少或尽量屏蔽它和其余部分之间的耦合。我们为了封装各种类型各种维度的变化,隔离变化,所以我们在设计中要拆分系统、拆分模块、定义类和接口、定义方法和函数,包括使用设计模式,都是一种封装变化的手段,当然封装变化不是拆分系统、拆分模块的唯一目的。
设计模式的主要目的就是优雅的封装变化,也就是不只是单纯封装变化,减少变化的影响范围,还要优雅封装,也就是提高代码可维护性,让代码有条理有秩序,低耦合,易于扩展。掌握设计模式是开发人员的重要技能之一。
设计模式学起来不太难,一个对设计模式一无所知的的开发人员,把*的设计模式一书认真读两遍基本可掌握。但要注意学习方法,要带着问题去读,对书中的每个设计模式,在读完它要解决的问题时,先停下来,问问自己,碰到这样的问题如何优雅的解决。思考完再继续读下去,再反思下自己为何没想到答案,再对照书中的类图和序列图、代码仔细揣摩体会下该设计模式的精妙。这样才有可能在自己的思想中产生较为深刻的理解,否则就是浮光掠影,没有效果。最后一定要在自己的开发工作中找合适的机会实践一下,不一定都用到,首先是要找到变化点,深入到变化点中去,再对照具体的问题场景,选择对应的模式。我的实践经验是在较多场景中都会用到模板方法(框架中用的更多)、代理模式、策略模式、命令模式、facade、工厂方法等。只有在潜意识中留下设计模式的烙印才算真正掌握了设计模式的精髓,以后一碰到问题,从直觉上就能较快知道应该运用哪些设计模式。
由于变化的多样性,也就是有不同类型和不同维度导致的变化,所有就要有多个不同的设计模式来应对相应的变化。除了*书中所讲的3种类型(创建型、结构性、行为型)的23种设计模式,其实还有很多其它的设计模式,也不只有3种类型,还有其他类型的设计模式,例如分布式系统领域、资源管理领域、应用集成领域这些类型。
回到正题,*书中的哪个设计模式直接体现了开闭原则?说的是直接,很多模式都间接体现了该原则。
开闭原则(扩展开放,对修改关闭)的完整意思是需求变化时要能通过增加代码来实现,这样就扩展了新的功能(对扩展开放),但不要修改先前的模块代码或底层框架核心层代码(对修改关闭),多优雅。显然如果修改核心层代码,很可能会引入风险,还有一种可能是核心层如果是二进制库(第二方或三方dll、jar包),你还无法修改,没源码啊,除非进行反向工程,反编译。这样如果增加功能需要修改核心层,就很难办到了。而遵照开闭原则的设计就不必担忧此问题,因为只需在核心层之外的外围模块(有源码控制权)中增加新代码实现扩展,不需涉及到核心层,核心要保持稳定。
答案是Visitor模式,该模式的类图就不画了。对照该模式的类图,可以发现有两个大的类层次,一个是Visitor层次(Visitor基类&接口和它的众多子类&实现类),另一个是数据元素层次(Element/Node和它的众多子类)。
采用该模式,如果要增加新的算法操作,只需要增加一个外围的Visitor子类即可,先前的核心代码(Visitor基类和Element基类)不用修改也不允许修改,这不正直接体现了开闭原则!
正如*的书中指出,该模式不是万能的,任何模式都有适用范围。如果变化点在数据元素层次(Element/Node层次)该模式就不太管用了。
模板方法、策略模式等也比较好地体现了开闭原则。很多框架也使用这些模式来提供扩展点,通过回调这些扩展点中的方法,以便拥抱具体应用中的变化。
.
题外:对设计模式之间的关系,不想单独再写博客,这里补充下对设计模式关系的理解。我们学知识掌握概念,不仅要了解其内涵/外延、适用场景和不适用的场景,还要注意掌握它和其他知识点、概念之间的区别与联系。学设计模式也是这样,例如学习工厂方法,除了单纯掌握它的内在之外,还要知晓它和其他设计模式之间的区别和联系,它们之间有什么相同、不同、有什么关系联系,相互之间比较有什么优缺点。这里讲解下设计模式之间的关系,如下图,是23种设计模式的关系图。
万事万物都是相互联系,都存在关系,不是孤立存在的。软件设计中的类之间通常也存在关系联系,例如订单和它包含的订单项,业务服务和它依赖的数据访问服务(DAO类),如果画类图对象图,这些关系一般在类图上要有所体现,类图中在有关系的类(对象)之间画线,写上具体的关系名称。于此类似,上图也体现了设计模式之间的关系,例如上图右下方Template Method模板方法模式和Factory Method工厂方法模式有联系,也就是在使用模板方法设计模式时我们通常要一起结合使用工厂方法模式来创建模板方法子类对象。
在使用模板方法设计模式时,定义模板基类,再定义几个模板子类。一般是使用基类对象类型来引用这些子类实例,在运行时调用的是这些子类实例。这些子类实例怎么来的,肯定要创建要new出模板子类对象。大多数情况下这些子类实例一般是单例,如何优雅的封装这种创建中的变化因素(根据某些参数来生成对应的模板子类),这里就用工厂方法模式来创建模板子类,来封装创建时的变化因素。也就是单例模式得到单个工厂实例,工厂实例得到模板实例。
理解了上图,就明白在软件设计中通常是综合使用多种设计模式,它们不一定是互斥关系,例如不同类型的设计模式之间。