旧游无处不堪寻
无寻处,惟有少年心
设计模式-概述

设计模式是软件设计中常见问题的典型解决方案。设计模式与方法或库的使用方式不同,你很难直接在自己的程序中套用某个设计模式。

模式并不是一段特定的代码,而是解决特定问题的一般性概念。

你可以根据模式来实现符合自己程序实际所需的解决方案。

模式的历史


设计模式并不是晦涩的、复杂的概念,事实恰恰相反。模式是面向对象设计中常见问题的典型解决方案。同样的解决方案在各种项目中得到了反复使用,所以最终有人给它们起了名字,并对其进行了详细描述。

1994 年,出版的《设计模式: 可复用面向对象软件的基础》一书,将设计模式的概念应用到程序开发领域中。该书提供了 23 个模式来解决面向对象程序设计中的各种问题。由于书名太长,人们将其简称为 GoF。

为什么以及如何学习设计模式


  1. 设计模式是针对软件设计中常见问题的工具箱,其中的工具就是各种经过实践验证的解决方案。即使你从未遇到过这些问题,了解模式仍然非常有用,因为它能指导你如何使用面向对象的设计原则来解决各种问题
  2. 设计模式定义了一种让你和团队成员能够更高效沟通的通用语言。

关于模式的争议


设计模式自其诞生之初似乎就饱受争议。

  1. 一种针对不完善编程语言的蹩脚解决方案。通常当所选编程语言或技术缺少必要的抽象功能时,人们才需要设计模式。例如,策略模式在绝大部分现代编程语言中可以简单地使用 lambda 来实现
  2. 低效的解决方案。许多人会将这样的统一化认为是某种教条,而不会根据项目的实际情况对其进行调整,要注意,设计模式是问题的解决方案,而不是用解决方案来发现问题
  3. 不当使用。如果你只有一把铁锤,那么任何东西看上去都像是钉子

设计模式分类


模式可以根据其意图或目的可以分为三类:

  1. 创建型模式(Creational Design Patterns): 提供创建对象的机制,增加已有代码的灵活性和可复用性
  2. 结构型模式(Structural Design Patterns): 介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效
  3. 行为型模式(Behavioral Design Patterns): 负责对象间的高效沟通和职责委派

创建型模式

创建型模式有:

  1. 工厂方法 Factory Method
  2. 抽象工厂 Abstract Factory
  3. 生成器 Builder
  4. 原型 Prototype
  5. 单例 Singleton

结构型模式

  1. 适配器 Adapter
  2. 桥接 Bridge
  3. 组合 Composite
  4. 装饰器 Decorator
  5. 外观 Facade
  6. 享元 Flyweight
  7. 代理 Proxy

行为型模式

  1. 责任链 Chain of Responsibility
  2. 命令 Command
  3. 迭代器 Iterator
  4. 中介者 Mediator
  5. 备忘录 Memento
  6. 观察者 Observer
  7. 状态 State
  8. 策略 Strategy
  9. 模板方法 Template Method
  10. 访问者 Visitor

面向对象设计原则


设计原则要比设计模式更重要。我们之后要讲的模式其中有的已经不重要了,实际应用非常少,有些已经被替代了。而设计原则在我们设计过程中是贯穿始终的。所有的设计模式都是根据设计原则进行抽象。设计原则是尺子,用于指导面向对象的设计。

  1. 依赖倒置原则: 高层模块(稳定)不应该依赖于低层模块(变化),二者都应依赖于抽象(稳定)
  2. 开闭原则: 对扩展开放,对修改封闭
  3. 单一职责原则: 一个类应该仅有一个引起他变化的原因
  4. 里氏代换原则: 子类必须能够替换他们的基类
  5. 接口隔离原则: 接口应该小而完备
  6. 优先使用对象组合而不是类继承: 类继承为白箱复用,对象组合为黑箱复用,继承在某种程度破坏了封装,子类父类耦合度高,而组合只要求被组合对象具有良好定义的接口
  7. 封装变化点: 使用封装来创建对象之间的分界层
  8. 针对接口编程而不是针对实现编程: 不要将变量声明为具体类,而应声明为某个接口

接口标准化是产业强盛的标志(秦的统一、活字印刷、汽车制造)。

各个原则(SOLID)之间也有着千丝万缕的联系,下面我们依次利用代码说明。

单一职责原则(S)

class Order
{
amount() {}
pay() {
// get money
// payment interface
}
}

let order = new Order()
let paymentInfo = order.pay()

一开始,我们有一个订单类,内部有支付方法,当我们新增一个订阅类、会员类时,如果也需要支付方法,那么我们就需要拷贝相同的代码。

class Subscription
{
amount() {}
pay() {
// get money
// payment interface
}
}

let order = new Subscription()
let paymentInfo = order.pay()

class Membership
{
amount() {}
pay() {
// get money
// payment interface
}
}

let order = new Membership()
let paymentInfo = order.pay()

那么我们应抽离单独的支付类,保证类职责单一。

class Order
{
amount() {}
}

class Payment
{
pay(payable) {}
}

let payment = new Payment()
let order = new Order()
let paymentInfo = payment.pay(order)

开闭原则(O)

class Storage
{
putFile(content, type) {
if (type == 'local') {
// ...
} else if (type == 'oss') {
// ...
}
}
}

如果这种写法,每次新增加存储类型,那么就需要重新测试全部代码。
我们可以利用继承扩展或构造扩展来遵守开闭原则。

class Storage
{
protected storageDriver
driver(driver) {
this.storageDriver = driver
}

putFile(content) {
this.storageDriver.putFile(content)
}
}

class FileDriver
{
putFile(content) {
// ...
}
}

let storage = new Storage()
let fileDriver = new FileDriver()
storage.driver(fileDriver).putFile("content")

里氏代换原则(L)

父类可以完全替换子类,也就是继承来的方法不允许重写,只允许在子类新增方法。

class BaseMath
{
sum(num1, num2) {
return num1 + num2
}
}

class IntegerMath extends BaseMath
{
sum(num1, num2) {
return parseInt(num1 + num2)
}
}

(new BaseMath()).sum(0.1, 0.1) // 0.2
(new IntegerMath()).sum(0.1, 0.1) // 0

以上情况违反里氏代换原则,我们使用如下方式遵循。

class BaseMath
{
sum(num1, num2) {
return num1 + num2
}
}

class IntegerMath extends BaseMath
{
intSum(num1, num2) {
return parseInt(num1 + num2)
}
}

(new BaseMath()).sum(0.1, 0.1) // 0.2
(new IntegerMath()).sum(0.1, 0.1) // 0.2
(new IntegerMath()).intSum(0.1, 0.1) // 0

接口隔离原则(I)

接口隔离原则要求单个接口的方法数尽可能少。

interface FileInterface
{
write(content)
getExtends()
getFileName()
getFileSize()
}

class Log
{
info(info, writer: FileInterface) {
writer.write(info)
}
}

class LogFile implements FileInterface
{
write(content) {
// ...
}
getExtends() { return null }
getFileName() { return null }
getFileSize() { return null }
}

以上情况违反里氏代换原则,因为接口力度不够小,我们使用如下方式遵循。

interface Writeable
{
write(content)
}

class Log
{
info(info, writer: Writeable) {
writer.write(info)
}
}

class LogFile implements Writeable
{
write(content) {
// ...
}
}

依赖倒置原则(D)

依赖倒置是指:

  1. 不要在一个类中使用 new 创建一个对象
  2. 注入时应使用接口而不是具体类
class Mail
{
send() {}
}

class Order
{
complete() {
(new Mail).send()
}
}

以上方式,邮件与订单耦合在一起,无法测试,我们使用如下方式注入:

class Mail
{
send() {}
}

class Order
{
complete(mail: Mail) {
mail.send()
}
}

以上方式还是有问题,邮件与订单耦合在一起,无法替换,我们使用接口方式注入:

interface Mailable
{
send() {}
}

class Order
{
complete(mail: Mailable) {
mail.send()
}
}