前面的话
说起设计模式,首先想到的就是类,虽然 Javascript 是一个弱类型动态语言,类也是通过模拟实现,但这不影响在开发过程中应用设计和模式。之前阅读过一些关于设计模式的书籍和文章,很好的指导了自己在日常项目中的编码思路。
同时学无止境,随着 ECMAScript 标准的不断发展,诸如 Class、Proxy、Promise等的新特性不断加入,外加 Typescript 的应用,终于可以相对完整的实现这些设计模式,所以是时候再进行一次设计模式的回顾学习了。
设计原则
设计模式,如其名,先有设计,再有模式。
模式是对一些常用设计的总结和提炼,设计思想又对模式的形成具有指导意义。授之以鱼不如授之以渔,掌握设计原则才能更好的理解模式。
S.O.L.I.D 设计原则是 Uncle Bob 最早提出的,了解过设计模式的朋友应该耳熟能详:
- S – Single Responsibility Principle(单一职责原则 SRP)
- O – Open-Closed Principle(开放封闭原则 OCP)
- L – Liskov Substitution Principle(里氏替换原则 LSP)
- I – Interface Segregation Principle(接口分离原则 ISP)
- D – Dependency Inversion Principle(依赖倒置原则 DIP)
下面谈谈我对以上这些设计原则的理解:
S 单一职责原则 SRP
A class should have only one reason to change
类发生更改的原因应该只有一个
这个原则和 UNIX 设计哲学里面的「Do one thing and do it well 让每个程序只做好一件事」很像。
让一个程序、类只做好一件事,当一个程序、类内部的功能过于繁复时,进行设计拆分独立成多个,并让每个部分保持独立性,只做好这部分既定的目标。
O 开放封闭原则 OCP
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
软件实体(类,模块,方法等等)应当对扩展开放,对修改关闭。
对扩展进行开放表示应该在不破坏现有代码的情况下向添加新功能。对修改进行封闭表示不应该对现有功能进行重大更改,而这意味着大量的重构和回归测试。
应当在不修改的前提下扩展。项目中新增的需求进行增量扩展新代码,而非破坏性的修改已有代码。
L 里氏替换原则 LSP
Subtypes must be substitutable for their base types.
派生类型必须可以替换它的基类型。
基类替换成子类后程序正常运行,想要做到这点需要做到子类在继承时不会修改任何关于基类抽象的方法和状态变量。推荐在子类中调用父类的公有方法来获取一些内部状态变量,而不是直接使用它。
就比如这个经典的问题:父类鸟
,拥有一个 fly
方法,而鸵鸟
去继承了鸟
,但是鸵鸟
不会飞,就修改了 fly
方法。这时如果一个原来调用父类鸟
可以正常运行的程序替换成鸵鸟
就可能因为fly
方法的修改出现问题。
能出现修改父类原有抽象方法才能实现需求目标的子类,更应该审视这个继承关系是否正确,更多时候使用依赖、组合来替代继承关系。
I 接口分离原则 ISP
Clients should not be forced to depend on methods they do not use.
不应该强迫客户依赖于它们不用的方法。
接口分离原则的思想和单一职责原则有点相似,接口分离是面向接口抽象层,单一职责是面向实现层。
避免单一接口内部过于复杂,需要拆分时就要及时拆分,而不应该要求类强制实现不需要的方法。
接口这个概念虽然在 Javascript 中不存在,但是在 Typescript 中有着广泛的使用。
D 依赖倒置原则 DIP
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
高层模块不应该依赖于低层模块,二者都应该依赖于抽象
B. Abstractions should not depend upon details. Details should depend upon abstractions.
抽象不应该依赖于细节,细节应该依赖于抽象
定义原文已经比较白话了,就是要面向接口编程,依赖抽象而不是具体实现。
依赖倒置基于这个事实:相比于实现细节的多变性,抽象的内容要稳定的多。
举个例子:
一个高层类Person
具有一个read
方法,其依赖于低层类Book
,Book
中拥有一个方法getContent
来获取文字;
正常情况下Person
实例化后,调用read
传入实例化的Book
,在执行getContent
正常阅读出文字内容;
而此时来了一个新的低层类Newspaper
,因为Person
在定义时已经规定依赖传入Book
类型,就没法读取Newspaper
;如果想读就要修改Person
的依赖;
解决方法,抽象出一个可读物接口Interface Reader
,让Class Book
和class Newspaper
都实现它,同时class Person
也依赖这个接口,就做到了解耦。
设计模式的分类
在《设计模式:可复用面向对象软件的基础》一书中定义了23种设计模式,同时根据其设计意图又可以分为三个大类:创建型 、结构型、行为型。
创建型模式 Creational Pattern
创建型模式提供创建对象的机制,抽象了实例化的过程,将对象的创建和创建的过程细节进行了分离。使结构更加清晰,并符合单一职责原则 SRP 的设计思路。
其包含如下类型:
- 单例模式(Singleton)
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
- 生成器模式(Builder)
- 原型模式(Prototype)
结构型模式 Structural Pattern
结构型模式是介绍如何将类或对象结合在一起,组合成功能更强大的结构,从而实现一定的设计目标。类、对象之间怎样设计继承、依赖、组合关系直接影响到整体程序的健壮性、可维护性,结构型模式应用也相当广泛。
其包含如下类型:
- 适配器模式(Adapter)
- 装饰器模式(Decorator)
- 代理模式(Proxy)
- 外观模式(Facade)
- 桥接模式(Bridge)
- 组合模式(Composite)
- 享元模式(Flyweight)
行为型模式 Behavioral Pattern
行为型模式负责对象之间的沟通和职责划分,其除了关注结构以外,更关注他们之间的通信机制。通过行为型模式可以更加清晰的划分类、对象之间的职责,展现实例对象之间的作用交互。
其包含如下类型:
- 职责链模式(Chain of Responsibility)
- 命令模式(Command)
- 备忘录模式(Memento)
- 迭代器模式(Iterator)
- 中介者模式(Mediator)
- 观察者模式(Observer)
- 状态模式(State)
- 策略模式(Strategy)
- 模板方法模式(Template Method)
- 访问者模式(Visitor)
- 解释器模式(Interpreter)
部分常用设计模式
一些设计模式在设计初衷面向的是 Java ,所以接下来的文章中我会结合 Typescript 回顾其中部分常用的设计模式。
- 创建型:工厂模式、单例模式
- 结构型:适配器模式、装饰模式、代理模式、外观模式
- 行为模式:观察者、迭代器、状态、备忘录
参考
- 深入理解JavaScript系列 - 汤姆大叔
- S.O.L.I.D The first 5 principles of Object Oriented Design with JavaScript
- Java设计模式:23种设计模式全面解析