如何正确地使用设计模式?

有感于@winter 的这个回答“设计模式的荼毒”体现在何处? 故提问。一直也很困惑如何避免过度设计?
关注者
3,761
被浏览
539,578

146 个回答

路过试答一下。

首先离题一下说点虚的

其实GoF的《设计模式》一书,一共有三个层面的内容:

1. 指出编程开发活动中存在模式,提出总结设计模式需要关注的四要素 "名称-问题-解决方案-效果“ ,并给出描述一套模式的格式模板。

2. 提出了面向对象开发中”针对接口编程优于针对实现编程”,”组合优于继承”的总体设计思路

3. 选取了现实开发中基于上述设计思路所形成的23种常见设计模式作为例子详细描述

虽然第3点包括的多个具体的设计模式实例占据了最多的篇幅,但事实上第1,2点才是纲。实际上《设计模式》一书最划时代的意义,在于第1点。在此之后,出现了以设计模式的格式来组织内容的《分析模式》,《企业架构模式》,《企业集成模式》,《xUnit测试模式》,《重构》等等质量颇高的书籍

在书中有一段我认为非常重要但很容易被忽略的话

本书中涉及的设计模式 并不描述新的或未经证实的设计 ,我们只 收录那些在不同系统中多次使用过的成功设计 。这些设计的 绝大部分以往并无文本记录 ,它们或是来源于面向对象设计者圈子里的非正式交流,或是来源于某些成功的面向对象系统的某些部分,但对 设计新手 来说,这些东西是很难学得到的。尽管这些设计 不包括新的思路 ,但我们 用一种新的、便于理解的方式将其展现给读者 ,即:具有统一格式的、已分类编目的若干组设计模式。

这段话的关键是:

1. 书中的模式不是作者的发明创造或独门秘籍,而是早已存在并已经广泛使用的做法,只不过没有被系统地加以记录。换而言之,只要遵循某些原则,这些所谓模式完全可能在无意识的状态下自发出现在产品代码中。

2. 这些模式在各种系统被多次使用。换而言之,你只要接触足够多的代码,必然会大量接触到这些模式的实际应用。只不过在看过《设计模式》一书之前,你可能意识不到这是一个成体系的设计手段。

3. 作者认为《设计模式》这本书的价值在于对设计模式进行了有效的组织和便于理解的描述。换而言之,这本书的写作出发点是”便于理解“,并且是面向”设计新手“的。而不少初学者却恰恰觉得这本书难以理解,这说明,作者已经在保证准确性的前提下,选用了他们所认为最便于理解的描述。比本书描述更为显浅的描述,很可能会牺牲准确性(不准确的描述对于新手来说是显然是害处大于好处)。当然某些人认为是作者表达能力有限,这种事情无法求证,但我倾向于前者。

===================================

现在开始正题,如何正确使用设计模式,准确来说,正确使用GoF的《设计模式》一书中所涉及的23种模式。但我们不妨先考虑一个比较容易回答的问题, 如何避免不正确地使用设计模式。 毕竟不正确地使用还不如不用, 你在避免不正确使用的前提下慢慢用起来,就是正确使用了

使用设计模式最常见走入歧途的做法是:你看了《设计模式》中某个具体模式,觉得似懂非懂,或者甚至根本没看过原书。然后去看了一些举例子打比喻的”再谈“,”妙解“,”大话“之类的东西,觉得豁然开朗。之后在开发时发现某一处非常符合你对这个模式的理解,于是开始使用。这种做法用来练手可以(也就是你明知使用这个模式未必是一个好主意,只是为了尝试在现实场景中去实现一下,练习代码并不进入最终产品代码),用来做真实的设计则往往谬以千里。

原因很简单:设计模式是一种驾驭抽象概念的技术,而描述模式的标准格式里就包括了抽象描述,代码示例和应用场景。如果一个程序员根据这些信息还不能理解一个设计模式的话,说明他首先抽象思维尚不足以驾驭设计模式,其次在理解代码和接触应用场景方面经验不足。简单来说, 还未能达到“设计新手”的入门水平 。在这种状态下勉强去使用设计模式,出问题是在所难免的。

因而得出第一点: 如果你已经看过某个设计模式的描述,要正确使用它的最基本前提是,你能完全看懂GoF《设计模式》中对它的描述。 在此之前,只看,不用。看是指看该模式的原文描述或者具体代码。特别地,不建议通过一些类比具体生活例子的方式来理解设计模式。设计模式的写作格式是经过验证,能有效地描述一个模式而又不失准确性的手段。如果你无法理解,看实际生产代码的应用场景是唯一真正有效的方法(因为设计模式本身就是从具体代码中总结出来的)。用类比的方法降低设计模式的抽象性来帮助了解没有实质的意义——即使你觉得自己懂了,你的抽象思维和开发经验还未达到能正确使用这个模式的水平。

正如前面所言, 只要你对面向对象一些基本原则有充分的理解,你甚至可能在没看过《设计模式》之前就开始使用某种模式了 ,如果你已经达到这种程度自然能无压力看懂描述。退一步假如你还没达到这种程度, 既然《设计模式》中的模式非常常见,你只要有心多看代码,在现有代码中必然能接触到。通过实际应用的代码与书中的描述互相印证,要理解亦不难。 再退一步,假如你接触的代码就一直没遇到 某个模式,你也一直无法自发理解某个模式,那么这个模式就对你没用,你没必要一定要找机会用。

避免不正确使用的第二点是,避免过度设计。这里说的过度设计 本质上就是你为可能发生的变动支付了过多的复杂度代价。 其实过度设计和设计模式没有必然的关系,只要你认定一个地方会变动,你就会考虑是否应该增加复杂度来换取灵活性。 设计模式只不过针对某些具体场景提供了一些效率较高的以复杂度换灵活性的手段而已。 避免过度设计的关键是,你能正确评估未雨绸缪所引入的复杂度,相对于发生变动的可能性和破坏力,是否值得。

正确评估变动的可能性和破坏力,只能依靠行业经验,属于资历问题。如果你对当前场景没有足够的经验进行评估,最好的办法就是假定它不会频繁变化,只采用普通的高内聚低耦合策略,而不需要增加额外的复杂度来提供灵活性。等到确认出现变化时,再进行重构。

而对设计模式的认识可能会影响对复杂度的估计,不少设计模式的初学者很容易错误估计实现某个设计模式所带来的复杂度,认为灵活性随手可得。又或者下意识地寻找练手机会而忽略掉复杂性的代价。在假定程序员对某个设计模式已经充分理解的前提下,我觉得评估复杂度时至少应该考虑到这些因素:

1. 需要额外引入哪些新的概念。要明白一段代码涉及的概念越多,就越难理解。

2. 设计模式自身的实现复杂度

3. 一旦引入后,为了保持设计所带来的灵活性,后续开发需要注意的地方。是否能通过代码层面的限制来保证灵活性不会被破坏。

4. 团队中其他人对这个设计模式的理解程度

5. 对排错调试,代码静态分析可能造成的影响 (例如Observer模式和Visitor模式往往会打乱静态分析,难以通过阅读代码确定执行状态)

如果能够大致准确地评估上述要素然后作出决定,我觉得即使变动最终没有发生,也算是一个合格的设计决策。真正的难点在于评估变动,这只能靠经验。还有就是每次做出设计决策后最好能跟踪总结,为下次决策积累经验。

关于设计模式的使用暂时想到这些。

========================

既然题目中提到了“设计模式的荼毒”,这里也说说我认为《设计模式》一书中最大一处问题:一句看上去正确,但被后来一些读物误解并放大,在实际执行中造成最多问题的话:

命名一个新的模式增加了我们的设计词汇。设计模式允许我们在较高的抽象层次上进行设计。基于一个模式词汇表,我们自己以及同事之间就可以讨论模式并在编写文档时使用它们。模式名可以帮助我们思考,便于我们与其他人交流设计思想及设计结果。

这句对“模式名称”要素的描述的话,在很多后续书籍或文章中被引申为: 设计模式的一个重要作用是为团队提供了一套方便的交流手段 。看上去非常正确,例如,我可以对同事说,这里需要一个Adapter;或者在代码中直接命名XXXApapter,同事们一看就知道这是什么了。交流变得非常方便——前提是,我们都看过《设计模式》并清楚关于这个设计模式的描述。使用设计模式进行交流的结果就是: 了解某个设计模式的人跟不了解这个设计模式的人根本无法交流。

而交流在团队中是一种非常基础,不可或缺的东西,进一步的结果就是, 了解某个设计模式的人认为不了解这个设计模式的人达不到基础水平。 而按照前文的分析,设计模式只不过是对已有开发手段的总结,完全有可能出现某个人的能力已经足够自发使用设计模式,只不过因为没认真看过《设计模式》这本书,而被认为达不到基础水平。 这造成了很多有一定编程能力的开发者对设计模式十分反感

再一步引申的结果是,因为设计模式变成了一种鉴别是否具有基础水平的手段,那么为了让自己看起来有基础以上水平,就必须要表现得懂设计模式——即使看不懂《设计模式》原文。这就给许多“大话”,“再谈”读物带来了市场,进而造就了一大批不是从实际开发或阅读代码中理解设计模式,在实际应用中错漏百出的初学者。

设计模式应用检验手段一:


大部分设计模式,是让你在在面向对象的基础上尽量消除继承的手段。所以,如果你用了一些设计模式,减少了继承,那你八成用对了。如果你用了一大堆设计模式,然而继承却越来越频繁,那你100%用错了。

之所以说大部分,是因为个别设计模式(比如享元模式)是为了解决特殊场景特殊问题而生的。


设计模式应用检验手段二:


一个设计合理的系统,因为解耦充分,各个模块独立性强,所以单元测试应该是比较容易写的。如果你用了一大堆设计模式,却发现给你写的类编写单元测试用例非常困难,那你一定是用错了。


设计模式应用检验手段三:


多态的本质是运行期动态决定程序的分支走向,也就是“更好的if”,而设计模式,至少是《设计模式》那本书中提到的那些模式,基本上是基于多态的。所以如果你合理的利用设计模式,你设计出的代码应该有较少的if,如果你的代码越使用设计模式if越多,或者更直观地说,缩进越多,你一定犯了错误。


设计模式应用检验手段四:


有些模式是很容易用错的,比如visitor模式,其实是为了解决java不支持double dispatch而存在的,然而其逻辑很晦涩。所以当你还在怀疑自己是否用对了设计模式的时候,你不应该使用这样的模式。


设计模式应用检验手段五:


类继承总的来说只有1.5种正确的打开方式:

第一种叫做模板方法(template method),是设计模式之一。这种模式说的是基类在一个抽象的层面实现了公有方法a,a依赖私有方法b,而b的实现是子类的工作。这种模式下,子类实现的虚方法b是不应该被外界调用的。当然有一种极端情况,b即使a本身(比如react中的render),此时模板方法蜕化为普通的多态。

第二种叫做mixin,类a通过继承一个基类b,获得某种相对独立的能力。然而这其实是一种不太好的设计,尤其是在需要mixin多种能力的时候,更合理的方式其实是在a内部创建b的实例。只是在比较简单的场景中,我们可以用继承糊弄一下,所以只能算0.5种。

除此之外,使用类继承基本上可以认为是错误的设计。

由此我们可以推论,在es6流行导致运行时mixin写法在前端不流行之后,前端代码中出现了一些新的设计错误。


设计模式应用检验手段六:


设计模式是一回事,设计模式的实现是另一回事,这一点很重要。比如著名的观察者模式(observer),在java中有大量应用。然而我们常见的观察者模式样板代码之所以长那个样,是因为java中缺乏事件订阅系统。在c++/c#以及js中实现观察者模式,就没必要那样写。因为c++有std:function,c#有委托,而js函数干脆就是对象。