设计模式
设计模式(Design Pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛等人(Erich Gamma,Richard Helm,Ralph Johnson 和John Vlissides 这四人提出的。也被称为:Gang of Four,GOF,四人帮)在1990 年代从建筑设计领域引入到计算机科学的。 设计模式并不能直接用于完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。
以上描述摘自维基百科。设计模式出现于1990 年代,那时主要的编程语言是Java 和C++ 这样的面向对象编程语言。随着计算机语言的发展,越来越多的现代语言的出现和流行,已经改变了设计模式出现时的计算机语言背景。虽然,现在Java 和C++ 依然流行,但Python、Scala、Groovy、Swift 等现代语言使用者越来越多。函数式编程语言(Lisp、Scheme、Haskell 等)也越来越来受到重视。程序员的视野大大拓展了,手中可使用的工具也更丰富了。
所以现在有一部分人质疑设计模式已经与时代发展脱节,认为设计模式是过时的产物。设计模式解决问题的方式,增加了不必要的复杂度。在一些现代语言中,设计模式解决的问题可以更好地被其他方式处理,有些问题甚至可能根本不存在。
这种质疑一定程度上是正确的。因为,我们现在拥有了更高级的语言工具。语言的选择是很重要的,因为语言的选择会影响我们看问题的方式。在使用过程式语言时,你可能为了方便读取数据,而“制造”某种类似于类(class)的数据结构,你可能会称这种设计模式为“封装“。但在面向对象编程语言中,你不会认为这是一个需要设计模式来解决的问题,因为语言本身已经为你解决了。在使用面向对象语言时,为了使操作可以在对象间传递,我们使用一个被称为命令模式(Command)的设计模式。但是,在拥有高阶函数的编程语言中,你可能没有意识到这是个问题。因为,你很自然地就将函数(即操作)作为参数传入到另一个函数中,而这就是函数式编程语言中高阶函数的概念。
从这点来看,随着我们所使用的编程语言的演化,我们遇到的问题也确实一直在改变。GOF 提出的23 种设计模式也许部分过时了。但我们遇到的问题并不会消失,设计模式的概念将一直存在:提炼普遍存在的问题,提出解决方案。这其实是一个抽象过程。现在已有的编程语言都存在表达的局限,即对某类问题抽象层次过低。所以,我们在使用任何编程语言时,都还是会遇到一些普遍存在的却没有被语言本身很好解决的问题。这时,我们就会使用到“设计模式”,即人们总结出来的解决方案:遇到问题A,用方案A;遇到问题B,用方案B。只不过问题会一直变化。现在我们不可以再使用那23 个模式来解决问题了,但是我们仍然需要总结出其它模式来解决新的问题。这种情况一直会持续到我们拥有“完美语言”的那一天。但现在看起来,这一天还没有到来的迹象。
Swift 中的设计模式
Swift 是一门多范式编程语言。除了支持面向对象编程范式之外,还支持函数式编程范式,和泛型编程。这使得 Swift 可以使用函数式编程的某些优秀工具解决我们在面向对象编程中遇到的困难。作为一门崭新的语言(2014 年发布),Swift 也同时吸收了很多其他语言的优秀特性。这使得与 GOF 首次提出 23 个设计模式时相比,我们现在看待 Swift 中设计模式相关问题的看法肯定已大大不同了。毕竟 GOF 提出设计模式主要是针对 Java 和 C++ 这种相对而言更传统的编程语言。在 Swift 中,一些模式已经被语言特性所吸收,你在使用 Swift 甚至察觉不出这类问题的存在;一些问题仍然存在,我们仍然需要某种设计模式,但实现起来会更为简便;当然,仍然会存在一些问题,Swift 也没有解决。
另外一个有趣的相反的问题是:Swift 是否引起了一些新问题,需要一些新的设计模式来解决呢?相对于一些实验性编程语言(如 Lisp,Haskell)而言,作为一门工业语言,Swift 的语言设计其实是保守的,吸收的大都是其他语言成熟的优秀特性。本质上,Swift 仍然主要还是一门面向对象编程语言。函数式编程,泛型编程等新的语言特性的引入,都只是一种改进,还谈不上是革命性的变化。这使得 Swift 虽然显得保守,但也风险可控,伴随剧烈革命而引入新的问题的可能性被降低了。当然,最终结论仍然需要大规模的业界应用实践之后才能得出。
这篇文章主要会涉及 Swift 改善的那一部分。具体而言,是指 Swift 消除了哪些设计模式,使哪些设计模式的实现简化了。
函数式编程
Swift 支持函数式编程范式。程序员可以使用 Swift 写出函数式风格的代码。函数式编程是一种以数学函数为程序语言建模的核心的编程范式。在函数式编程中,函数是核心概念,是“头等公民”,函数被赋予了更多职责,拥有更多灵活的使用方式。这一章可以看到使用函数式编程范式,可以消除一些面向对象编程中使用到的设计模式。
高阶函数
高阶函数,指可以将其他函数作为参数或者返回结果的函数。由于高阶函数,我们发现 GOF 设计模式中命令模式(Command)在 Swift 中消失了。
命令模式使用对象封装一系列操作(命令),使得操作可以重复使用,也易于在对象间传递。由于Swift 仍然主要是一门面向对象编程语言,我们仍然可以使用Swift 实现一个经典的命令模式。实现命令模式的目的只是和之后使用高阶函数的方案对比:
// Receiver class Light { func turnOn() { println("The light is on") } func turnOff() { println("The light is off") } } // Abstract Command protocol Command { func execute() } // Concrete Command class FlipUpCommand: Command { private let light: Light init(light: Light) { self.light = light } func execute() { light.turnOn() } } class FlipDownCommand: Command { private let light: Light init(light: Light) { self.light = light } func execute() { light.turnOff() } }
以上例程中,灯(Light)是命令(Command)的操作对象(Receiver)。我们定义了命令的协议,同时我们实现两个具体的命令操作:FlipUpCommand 和 FlipDownCommand。它们分别使灯亮,和使灯灭。
// Invoker class LightSwitch { var queue: [Command] = [] func addCommand(command: Command) { queue.append(command) } func execute() { for command in queue { command.execute() } } } // Client class Client { static func pressSwitch() { let lamp = Light() let flipUpCommand = FlipUpCommand(light: lamp) let flipDownCommand = FlipDownCommand(light: lamp) let switcher = LightSwitch() switcher.addCommand(flipUpCommand) switcher.addCommand(flipDownCommand) switcher.addCommand(flipUpCommand) switcher.addCommand(flipDownCommand) switcher.execute() } } // Use Client.pressSwitch()
这段则代码显示了如何使用命令模式。
在函数式编程中,由于存在高阶函数。我们可以直接将一个函数作为参数传给另外一个函数。所以,使用类包裹函数在对象间传递这件事情就显得多余了。以下代码显示如何使用高阶函数达到命令模式相同的效果:
// Invoker class LightSwitchFP { {1} var queue: Array<(Light) -> ()> = [] {1} func addCommand(command: (Light) -> ()) { queue.append(command) } {1} func execute(light: Light) { for command in queue { command(light) } } } {1} // Client class ClientFP { {1} static func pressSwitch() { let lamp = Light() let flipUp = { (light: Light) -> () in light.turnOn() } let flipDown = { (light: Light) -> () in light.turnOff() } {1} let lightSwitchFP = LightSwitchFP() lightSwitchFP.addCommand(flipUp) lightSwitchFP.addCommand(flipDown) lightSwitchFP.addCommand(flipUp) lightSwitchFP.addCommand(flipDown) {1} lightSwitchFP.execute(lamp) } } {1} // Use ClientFP.pressSwitch() {1} {1}
使用高阶函数的版本中,负责集中调度命令的 LightSwitchFP 类有一个接受命令的函数 addCommand。由于 Swift 支持高阶函数,这个函数无需接受一个携带命令函数的 Command 对象,而是直接接受表示命令的函数。这样更为直接自然。所以,命令模式在 Swift 这样拥有高阶函数的编程语言中,就显得多余了。
一等函数
一等函数,进一步扩展了函数的使用范围,使得函数成为语言中的“头等公民”。这意味函数可在任何其他语言构件(比如变量)出现的地方出现。可以说,一等函数是更严格的高阶函数。
策略模式(Strategy)定义了一系列算法,将每个算法封装起来,并且使它们之间可以互相替换。此模式让算法的变化独立于使用算法的客户。
下面,我们使用Swift 讨论一下一等函数对策略模式的影响。我们先用Swift 实现传统的策略模式:
protocol Strategy { func compute(first: Int, second: Int) -> Int } class Add: Strategy { func compute(first: Int, second: Int) -> Int { return first + second } } class Multiply: Strategy { func compute(first: Int, second: Int) -> Int { return first * second } } class Context { let strategy: Strategy init(strategy: Strategy) { self.strategy = strategy } func use(first: Int, second: Int) { strategy.compute(first, second: second) } } let context = Context(strategy: Add()) context.use(1, second: 2)
类似于命令模式,策略模式中的策略对象主要用于封装操作(函数),不同的是策略模式中的策略对象封装的是不同的算法。这些算法实现了相同的接口,在这个例子中,接口是用 Strategy 协议表示的。我们使用两个实现了 Strategy 协议的具体类:Add
和Multiply
分别封装两个简单的算法。Context 对象,用于对算法进行配置选择,它有一个 Strategy 类型的实例变量:strategy。通过配置 Context 的 strategy 具体类型,可以使用不同的算法。
然后我们再看看如果存在一等函数,策略模式是否可以得到化简:
let add = { (first: Int, second: Int) -> Int in return first + second } let multiply = { (first: Int, second: Int) -> Int in return first * second } class ContextFP { let strategy: (Int, Int) -> Int init(strategy: (Int, Int) -> Int) { self.strategy = strategy } {1} func use(first: Int, second: Int) { strategy(first, second) } } {1} let fpContext = FPContext(strategy: multiply) fpContext.use(1, second:2) {1} {1}
由于 Swift 的函数都是一等函数,使得我们可以把函数作为参数传给另外一个函数。这无需在使用对象来封装算法。函数可以成为封装算法的载体,这样更为直接自然。例子中,ContextFP 的构造器的传参就是函数类型。给予构造器代表不同算法的函数,就配置了不同的算法。
函数也可以作为类的实例变量。这样在类中,直接维护代表算法的函数也成为可能。从类型声明可以看出,ContextFP 中的实例变量 strategy 就是一个函数。
一等函数的概念使得函数获得了更高的地位,使得函数的灵活性大大增加。在很多场景下直接使用函数会是更直接自然的选择。面向对象编程范式,赋予了对象更高的地位。但是,如果给予函数“正常”一些的地位,可以简化不少问题。设计模式中的不少模式存在都是由于函数的使用限制,需要使用在使用类包裹函数。类似的例子还有模版方法模式(Template method)。
柯里化函数
柯里化函数(Curried Function),是指接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,该函数返回一个接受余下参数的新函数。这个名词来源于逻辑学家:Haskell Curring。编程语言 Haskell 也取自这位逻辑学家的名字。
抽象工厂模式(Abstract Factory)提供了一种方式,可以将一组具有同一主题的单独的工厂封装起来。在正常使用中,客户端程序需要创建抽象工厂的具体实现,然后使用抽象工厂作为接口来创建这一主题的具体对象。客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来。
函数柯里化可以用于制造一系列相关的函数。这和抽象工厂模式的目的类似。仍然如前例,先使用Swift 实现经典的抽象工厂模式。首先,我们给出一个产品族,包括两个牌子,两种设备类型的四个组合:
// Abstract Product protocol Product { var brand: String { get } var name: String { get } } protocol Phone: Product { } protocol Pad: Product { } // Concrete Product class ApplePhone: Phone { var brand: String { return "Apple" } var name: String init(name: String) { self.name = name } } class SamsungPhone: Phone { var brand: String { return "Samsung" } var name: String init(name: String) { self.name = name } } class ApplePad: Pad { var brand: String { return "Apple" } var name: String init(name: String) { self.name = name } } class SamsungPad: Pad { var brand: String { return "Samsung" } var name: String init(name: String) { self.name = name } }
然后,我们需要抽象工厂模式,来为创建这些同一主题的产品提供易于使用,方便扩展的方法:
// Abstract Factory protocol AbstractFactory { func createPhone() -> Phone func createPad() -> Pad } // Concrete Factory class AppleFactory: AbstractFactory { func createPhone() -> Phone { return ApplePhone(name: "iPhone 6S") } func createPad() -> Pad { return ApplePad(name: "iPad Air 2") } } class SamsungFactory: AbstractFactory { func createPhone() -> Phone { return SamsungPhone(name: "Note 5") } func createPad() -> Pad { return SamsungPad(name: "Note") } }
再看看如何使用抽象工厂:
let appleFactory: AbstractFactory = AppleFactory() appleFactory.createPad() let samsungFactory: AbstractFactory = SamsungFactory() samsungFactory.createPhone()
对于这个抽象工厂实现来说,虽然加入一个新的设备类型会有很大问题,需要改变抽象工厂的接口,但如果只增加品牌就很好办了。只需在实现一个具体品牌的工厂即可。
然后,我们使用柯里化函数来实现一个工厂方法。这个函数接受创建产品所需要各种参数,返回具体的产品。但我们实际上不会使用这个工厂方法直接创建产品。而是使用柯里化,让这个工厂方法充当一个函数工厂,创建一批可以作为产品工厂的方法。然后,我们可以使用这批产品工厂方法创建具体的产品。
func createProductWithType(type: String)(brand: String)(name: String) -> Product? { switch (type, brand) { case ("Phone", "Apple"): return ApplePhone(name: name) case ("Pad", "Apple"): return ApplePad(name: name) case ("Phone", "Samsung"): return SamsungPhone(name: name) case ("Pad", "Samsung"): return SamsungPad(name: name) default: return nil } } let createApplePhone = createProductWithType("Phone")(brand: "Apple") let createSamsungPhone = createProductWithType("Phone")(brand: "Samsung") let createApplePad = createProductWithType("Pad")(brand: "Apple") let createSamsungPad = createProductWithType("Pad")(brand: "Samsung") // Use let applePhone = createApplePhone(name: "iPhone 6S") println(applePhone)
与抽象工厂模式相比,使用柯里化函数充当函数工厂,我们可以更轻松地基于一些条件创建一系列工厂方法。
Swift 的语言特性
扩展
扩展(Extension)扩展是一种向已有的类,枚举或者结构体添加新功能的方法。扩展和 Objective-C 中的类别(Category)类似,但是与 Objective-C 中的分类不同的是,Swift 中的扩展没有名字。
适配器模式(Adapter)将一个类的接口转接成用户所期待的。适配器使得因接口不兼容而不能在一起工作的类工作在一起。通常适配器有两种实现方法:一种使用继承,创建新类,并继承原有类,同时实现需要兼容的接口;另一种使用组合,创建新类,维护原有类的实例对象,并实现需要兼容的接口。无论哪种方法我们都需要创建一个新的被称为适配器的类。
扩展使得我们多了一种为类增添方法的手段。适配器模式的实质是使得一个已经存在的类,适配另外一个接口。实现接口其实就是实现接口所定义的方法。如果拥有扩展这种工具,就无需适配器模式这种笨重的做法。
下面先试着完成适配器的经典实现。首先,我们写一套配合完美的圆木桩和圆洞。如果圆木桩的直径小于圆洞的直径,毫无疑问,我们可以把圆木桩打入圆洞。
protocol Circularity { var radius: Double { get } } class RoundPeg: Circularity { let radius: Double init(radius: Double) { self.radius = radius } } class RoundHole { let radius: Double init(radius: Double) { self.radius = radius } func pegFits(peg: Circularity) -> Bool { return peg.radius <= radius } }
现在我们的工地出现了一根方木桩。我们仍然需要测试这个方木桩是否可以打入圆洞。由于木桩是否能被打入圆洞的测试要求木桩实现了 Circularity,这时我们就需要一个适配器,将 SquarePeg 适配为符合 Circularity 接口的类。适配的结果就是下面的 SquarePegAdaptor,它实现了 Circularity 接口。这样,我们可以判断它是否能打入圆洞。
class SquarePeg { let width: Double init(width: Double) { self.width = width } } class SquarePegAdaptor: Circularity { private let peg: SquarePeg var radius: Double { get { return sqrt(pow(peg.width/2, 2) * 2) } } init(peg: SquarePeg) { self.peg = peg } }
检测结果:
// Test if the square peg is fit with the hole let hole = RoundHole(radius: 5.0) for i in 5...10 { let squarePeg = SquarePeg(width: Double(i)) let peg: Circularity = SquarePegAdaptor(peg: squarePeg) let fit = hole.pegFits(peg) println("width:\(i), fit:\(fit)") }
然后,我们看看使用扩展是否能简化适配器模式:
// Use extension extension SquarePeg: Circularity { var radius: Double { get { return sqrt(pow(width/2, 2) * 2) } } }
使用扩展,我们无需像适配器模式那样,创建新类充当适配器。而只需要扩展原有类,为原有类添加一个方法,同时就实现了 Circularity 接口。在使用时,也更简单了,我们只需要直接使用原有类即可:
for i in 5...10 { let peg = SquarePeg(width: Double(i)) let fit = hole.pegFits(peg) println("width:\(i), fit:\(fit)") }
由于 Swift 提供了扩展(Extension)这一新的语言特性,使得处理接口转换这类原来需要适配器模式解决的问题时,更为简单了。
运算符重载
解释器模式(Interpreter)定义语言的文法,并建立一个解释器来解释语言中的句子。本质上说,解释器模式是一种语言扩展工具。如果你使用的语言不适合解决某类问题,可以使用解释器模式构建一些新的语法,甚至新的语言用于更为方便地解决特定领域的问题。这是在语言不提供语言级别的对自身扩展时的一种替代。
一些语言会禁止对语言本身进行扩展,例如Java。这可能是因为Java 的设计者认为,扩展语言需要专业的技能和良好的语法设计,这通常不是普通程序员可以胜任的。扩展语言这种特性常常会带来一些容易令人混淆的问题,很多描述这类技术的文档,也通常会告诫读者,“如果你不清楚自己在做什么事情,就最好不要使用这个功能”。但另一方面,语言禁止对本身的扩展也就限制了其使用范围,所以,Java 语言中才会出现解释器模式这种解决扩展语言自身功能的方案。当然不可避免的,这种解决方案会比有语言级别的支持复杂晦涩。
下面我们会讨论一种扩展语言语法的语言特性:运算符重载。看看有了运算符重载,解释器模式是否能得到简化。运算符重载是多态的一种。运算符(比如+,= 或==)被当作多态函数,他们的行为随着其参数类型的不同而不同。这意味着我们可以赋予一个运算符新的含义,使用在新的数据类型上,这就是一种扩展语言语法的特性。
我们仍然分别用解释器模式和运算符重载来解决同一个问题。两个例程的比较可以清晰地说明差别。问题的要求是实现复数的加法和乘法。先用解释器模式实现:
import Foundation protocol Expression { func interprete(variables:Dictionary<String, Expression>) -> ComplexNumber } struct ComplexNumber: Expression { let real: Double let imaginary: Double init(real: Double, imaginary: Double) { self.real = real self.imaginary = imaginary } func interprete(variables:Dictionary<String, Expression>) -> ComplexNumber { return self } } struct Plus: Expression { let leftOperand: Expression let rightOperand: Expression init(left: Expression, right: Expression) { self.leftOperand = left self.rightOperand = right } func interprete(variables:Dictionary<String, Expression>) -> ComplexNumber { let left = leftOperand.interprete(variables) let right = rightOperand.interprete(variables) return ComplexNumber(real: left.real + right.real, imaginary: left.imaginary + right.imaginary) } } struct Multiply: Expression { let leftOperand: Expression let rightOperand: Expression init(left: Expression, right: Expression) { self.leftOperand = left self.rightOperand = right } func interprete(variables:Dictionary<String, Expression>) -> ComplexNumber { let left = leftOperand.interprete(variables) let right = rightOperand.interprete(variables) let resultReal = left.real * right.real - left.imaginary * right.imaginary let resultImaginary = left.real * right.imaginary + left.imaginary * right.real return ComplexNumber(real: resultReal, imaginary: resultImaginary) } } struct Variable: Expression { let name: String init(name: String) { self.name = name } func interprete(variables: Dictionary<String, Expression>) -> ComplexNumber { if let variable = variables[name] { return variable.interprete(variables) } return ComplexNumber(real: 0, imaginary: 0) } }
然后,我们实现解释器。为了使实现更简单,问题更聚焦,我们只实现后缀表达式形式的复数加法和乘法。这是因为对于后缀表达式,我们只需借助栈(Stack)对表达式进行一次遍历,就可以计算出结果,这大大降低了算法复杂度。而对中缀表达式的解释,我们通常也会先将它转化为后缀表达式,再做解释,计算出结果。鉴于这个例子里主要是展示解释器模式,而不是讲解解释器实现,所以,我们仅实现最简单的后缀加法和乘法。
struct Stack<M> { var items = Array<M>() mutating func push(item: M) { items.append(item) } mutating func pop() -> M { return items.removeLast() } } struct Evaluator: Expression { let syntaxTree: Expression init(expression: String) { var expressionStack = Stack<Expression>() let tokens = expression.componentsSeparatedByString(" ") for token in tokens { if token == "+" { let subExpression = Plus(left: expressionStack.pop(), right: expressionStack.pop()) expressionStack.push(subExpression) } else if token == "*" { let subExpression = Multiply(left: expressionStack.pop(), right: expressionStack.pop()) expressionStack.push(subExpression) } else { expressionStack.push(Variable(name: token)) } } syntaxTree = expressionStack.pop() } func interprete(variables: Dictionary<String, Expression>) -> ComplexNumber { return syntaxTree.interprete(variables) } }
最后,看看如何使用解释器模式:
let expression = "w x *" let sentence = Evaluator(expression: expression) var variables = Dictionary<String, Expression>() variables["w"] = ComplexNumber(real: 1, imaginary: 2) variables["x"] = ComplexNumber(real: 2, imaginary: 4) sentence.interprete(variables)
由于 Swift 支持运算符重载,我们可以更简单地使用语言已经提供的特性完成上面例程中相同功能。Swift 支持以运算符作为函数名。所以,对于 Swift 中已经实现的运算符,我们只需要重载两个函数,使运算符可以应用于新的类型:复数类型,即可。
func + (left: ComplexNumber, right: ComplexNumber) -> ComplexNumber { let resultReal = left.real + right.real let resultImaginary = left.imaginary + right.imaginary return ComplexNumber(real: resultReal, imaginary: resultImaginary) } func * (left: ComplexNumber, right: ComplexNumber) -> ComplexNumber { let resultReal = left.real * right.real - left.imaginary * right.imaginary let resultImaginary = left.real * right.imaginary + left.imaginary * right.real return ComplexNumber(real: resultReal, imaginary: resultImaginary) }
完成运算符重载之后,我们就可以像将两个整数相加一样,使用加法运算符将两个复数相加:
let first = ComplexNumber(real: 1, imaginary: 2) let second = ComplexNumber(real: 2, imaginary: 4) first + second first * second
可以看到运算符重载使得扩展运算符的使用范围变得更容易了。这也很容易理解,解释器模式其实就是对语言扩展自身语法这一功能的缺失而设计的设计模式。再次设计很有时候需要绕过很多障碍,很容易比语言的原生支持更为复杂繁琐。
总结
这篇短文中,我们回顾了设计模式的概念,并讨论了在现代编程语言背景下,设计模式相关问题的一些变化。然后使用 Swift 语言演示了 5 个设计模式和新的语言特性之间的关系。这 5 个设计模式和语言特性分别是:高阶函数和命令模式,一等函数和策略模式,柯里化函数和抽象工厂模式,扩展和适配器模式,运算符重载和解释器模式。
这 5 个例子展示了设计模式在现代编程语言中的变化:要么被语言特性更好的代替了;要么变得更为简单了。
参考文档
- Blog: airspeedvelocity
- IBM developerWorks
- Design Pattern Implementaion
- Apple’s Document “Swift Programming Language”
感谢丁晓昀对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。
评论