toBeTheLight.github.io 荒原

阅读:《图解设计模式》读后总结--如何学习设计模式

2019-08-02
toBeTheLight

阅读《图解设计模式》的所思所想。 感觉这本书缺了不少东西,但是还是有收获的。

困惑

作者试图从另一个角度阐述设计模式,所以对 23 种具体设计模式进行了重新分类,但整本书读下来比较困惑,在于几点:

  1. 分类标准不统一,有实现思路、实现内容、模式目的等标准,甚至还有“适应设计模式”这种分类,颇有些无从分类的“自暴自弃”的味道。同时在这种分类方式下,还存在一个问题,即某设计模式的实现是会用到另一个设计模式的,甚至其些设计模式的书中实现类图会基本相同,但是却属于不同分类,带来了新的困惑,好像要强迫你在学习一个设计模式时,要忘掉其他设计模式的存在。
  2. 缺乏对具体设计模式适用场景的充分阐述,知何却不知为何。
  3. 作为入门书,未对更低层的原则进行科普,即使知道了各具体模式可以达成哪些具体目的,却无法融汇到统一的思想出口。
  4. ?

但是总觉得还有一个抓不到的原因,那么再深入探究一下,到底是什么令我产生困惑呢?这就需要了解设计模式的起源。

根源

设计模式(design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案

设计模式是问题的方案。

设计模式是经验的总结。

首先,正确的学习方式应该是带着问题找答案。如果答案被直接摆在面前却不知问题,不论是谁都会产生“这是啥?!”的困惑。但更重要的是,经验是具有普适性的,具体的设计模式其实体现的是具体的思想方式,这种思想方式与语言无关,同时在单一语言中也一定有多种实现形式,那么此时就进入到了抽象和具象的冲突。若无顿悟的天分,接收到思想的抽象概念描述时,会有一种脑子懂了,却无从下手的感觉,同时若以有限的应用示例来描述,又无法完全体会到思想的方方面面。

所以设计模式的学习应该是快速的阅读书籍,在对模式有轮廓性认识后,带着问题,不断实践练习的一个过程,要在实践中得出自己的体会,将从书中得到的融到自己的骨子里。这也是造成前面讲的困惑的根本原因了,实践不够呀。

这其中还有另一个教训,我曾经陷入了为什么能用这种设计模式而不能用另一种设计模式的思维旋涡,一样,只靠想,不依托实践,这些问题是解决不了的。所以不要把时间浪费在纠结的思考中。

也是因此,后续内容不会是面面俱到的长篇累牍,只会对设计模式的脉络做基于目的的简要阐述。

目的与手段

维护一个软件的长期良性发展是究极目标,即提高可维护性,降低维护成本。可以从抽象登记分为 4 个层次。

  1. 目标:维护性。
  2. 标准:扩展性、重用性、高内聚、低耦合。
  3. 原则:7 大基本原则。
  4. 模式:23 + N 种设计模式。

应该通过提升扩展性、重用性等达到高内聚、低耦合的特性。

在这个过程中应遵循 7 大原则,同时这些原则又是设计模式的基础,是设计模式为何如此设计的依据。

而模式则是更具体的思想范式,设计模式不仅仅局限于 23 种,跟随技术水平的发展,也伴生出了新的问题,也就总结出了针对新问题的模式。

7 大基本原则

设计模式往往是基于类,接口来讲的,而 JS 并非基于类的语言,支持度不够,同时我们又不应该将模式的思想拘泥于类中,所以可以将下述原则的应用个体,如类、接口,放到函数或模块等其他维度上体会。

  • 单一职责原则:
    • 单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。
    • 我们不必要拘泥于类,该原则的根本目的是控制职责所在的个体复杂度。只需要明白单一个体只需要做好一件事,个体越简单则可读性越好,职责划分越明确,则改动发生时,越不会影响其他个体。
    • 比如这种职责拆分可以发生在函数粒度,也可以发生在函数的聚合层面(类或者更外层的函数),职责和个体理想状态下应该是一对一的。
    • 这个原则要求我们能清晰的认识到代码逻辑中的多重职责,从而才能进行划分。
  • 接口隔离原则:
    • 客户端不应该被迫依赖于它不使用的方法。一个类对另一个类的依赖应该建立在最小的接口上。
    • 即对于依赖者,被依赖者应该只提供他关心的功能。当体现在接口上时,就是接口隔离原则,将有冗余的接口拆分。
    • 可以避免由于依赖者的增多导致接口膨胀,影响到其他的依赖者。
    • 相对于单一职责原则可以理解为单一职责原则是对内做最少承诺,而接口隔离原则是对外做最少的承诺。
  • 依赖倒置原则:
    • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
    • 即面向接口编程。我们只需要对低层进行接口定义,高层只需要关注有哪些接口并进行调用,低层实现时只要实现了这些接口,那么传给高层的实例发生变化时,高层就不需要修改。降低了耦合度。
  • 开闭原则:
    • 软件实体应当对扩展开放,对修改关闭。
    • 在软件修改时,尽量通过扩展而实现而不是通过修改来实现,避免对现有逻辑的影响。
  • 合成复用原则:
    • 在复用时,要尽量先使用组合(实例化是就存在)或者聚合(通过 API 调用添加为成员变量)等关联关系来实现,其次才考虑使用继承关系来实现。
    • 继承强耦合,组合聚合是弱耦合。
  • 里氏替换原则:
    • 继承必须确保超类所拥有的性质在子类中仍然成立。即子类可以扩展父类的功能,但不能改变父类原有的功能。
  • 迪米特法则:
    • 只与你的直接朋友交谈,不跟“陌生人”说话,又叫最少知识原则。即一个类对自己依赖的类知道的越少越好。
    • 耦合是无法完全避免的。
    • 被依赖的类不论多复杂,都应该将细节封装在内部,对外暴露 API。
    • 应该避免类中出现非直接的朋友关系(直接朋友关系:成员变量、参数、返回值)的依赖。

可以看出这些原则都是为了个体间的低耦合而努力。

模式的一句话描述

  1. 迭代器模式:为了在不暴露数据的内部结构的前提下对外提供可替换的迭代方式。此模式隐藏内部细节,且可替换迭代方式。这种思路可推广至迭代以外的其他能力。

  2. 适配器模式:为了使不兼容的接口协同工作,将现有接口包装为需要的接口。在处理代码边界即第三方依赖时也可使用,能在第三方依赖被换掉时降低改动成本。

  3. 模板方法模式:在流程结构确定,而步骤的具体实现不定或有差异时使用。即定义好模板,但将具体处理的实现交给子类,扩展新的能力时只需实现新的子类。

  4. 工厂方法模式:创建接口不变的情况下,由用户决定什么哪个实例时,使用。产品和工厂一一对应,扩展新的产品时需要增加新的产品类和对应的工厂类。

  5. 单例模式:单一类只允许生成一个实例时使用。

  6. 原型模式:避免较高的实例化成本时使用。通过复制生成实例,核心在于复制现有实例,避免重走实例化的过程。

  7. 建造者模式:最终产出物的组成部分相同,但需要组装过程可替换时使用。通过组装生成复杂实例,并将组装过程抽离至独立的类,核心在于侧重组装,那么实现不同的组装过程类就能产出不同的产出物。

  8. 抽象工厂模式:与工厂方法模式类似,当工厂类产出多个产品时可以使用抽象工厂模式。注意区别是是工厂产出一个还是多个产品。

  9. 桥接模式:在对外提供的功能接口内有多个维度的变化时使用,将类的对外接口和实现分为独立的两个类,对外接口通过在内部组合使用实现类来完成具体实现,可减少维度引起的类数量的爆炸增长。

  10. 策略模式:当我们完成任务的策略需要可被替换时使用。将通过策略完成任务的过程拆分为调用和实现,实现部分提供成系统的方法簇,即具体策略,以参数形式传给调用部分,从而实现策略可替换。

  11. 组合模式:当需要提供给用户的是多个对象,且对象间是部分-整体的层次结构,且不希望用户关心对象间差异,只要一套访问接口时使用。通过多个子类实现相同接口实现。

  12. 装饰器模式:给一个对象追加更多功能,且不改变提供给用户的接口时使用。

  13. 访问者模式:在不改变数据结构的前提下可以添加对数据结构的新的操作时使用。通过将数据结构与操作分离的方式实现。

  14. 责任链模式:个体需要被多个对象处理,但处理对象间有没有耦合关系时,为了避免增加系统复杂度时使用。通过将多个处理对象组成一条责任链,然后将待处理目标沿着这条链传递进行处理实现。Koa 中间件机制就是一种实践。

  15. 门面模式:简化用户对复杂系统中子系统的联系,对外提供简单易用的接口时使用。通过包装更高层的类,由它调度子系统实现。

  16. 中介者模式:简化复杂系统中子系统之间的联系,将交互封装一个中介对象,降低子系统对象间的耦合。可以体会下与门面模式的不同。

  17. 观察者模式:一个对象改变时需要导致其他对象也改变,且不关心其他对象具体是谁时可以使用。通过观察对象管理监听他的所有观察者,并在发生变化时通知所有观察者实现。

  18. 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态时使用。

  19. 状态模式:对象与外部互动导致状态变化,从而行为不同时使用。通过将不同状态下的行为封装为不同的类,允许在状态改变时通过切换状态类改变行为实现。可以理解为策略模式的一种特殊应用。是一种内置了多种“策略”,根据状态变化切换“策略”的模式。

  20. 享元模式:有大量的实例需要公用或重复使用时使用。我们可以把这些实例当做享元,并管理起来。可以当做同时使用了多个单例模式的类,因为存在管理能力,所以会受外部因素影响,在返回前修改单例的状态。

  21. 代理模式:在我们不想让用户直接使用对象的情况下使用,如加以访问控制。很简单,实现方式就是加一层代理来间接引用对象。

  22. 命令模式:需要使命令发起者和执行者不可见,甚至需要对命令加以管理时使用。此模式通过将命令封装为一个类,将命令执行者作为命令的依赖,分离命令调用者和命令实现者,同时由于命令实例的存在又可以对命令加以管理。

  23. 解释器模式:将发生频率足够高的问题的各个实例表述为一个简单语言的句子,并构建一个解释器解释语言中的句子。通过对定义语句语法节点,并针对每类语法节点声明类,对语句节点遍历解析实现。

我们可以从上述描述中看到重复的几个关键词:拆分、不关心、不破坏,还是在为了个体间的低耦合而努力。

而且要注意的是,模式并非完美,有些模式实现时甚至会增加内部耦合,增加系统复杂度,所以要关注目的,关注目的,关注目的,关注是否降低了所关注的可能变化的点的耦合度。

结语

最后以三个问题结束这篇文章。

学什么?

我们学设计模式,是为了学习如何合理的组织我们的代码,如何解耦,如何真正的达到对修改封闭对扩展开放的效果,而不是去背诵那些类的继承模式,然后自己记不住,回过头来就骂设计模式把你的代码搞复杂了,要反设计模式。

如何用?

为了合理的利用设计模式,我们应该明白一个概念,叫做扩展点。扩展点不是天生就有的,而是设计出来的。我们设计一个软件的架构的时候,我们也要同时设计一下哪些地方以后可以改,哪些地方以后不能改。

如何用的好?

“我亦无他,惟手熟尔。”

参考


Content