Kotlin核心编程 潜入篇:kotlin探索(第9章)
第9章 设计模式
设计模式是一个老生常谈的东西了,它不是一个类、包或类库,而是软件工程中解决特定问题的一种指南。我们通常所说的经典的设计模式,是指软件设计领域的四位大师(GoF)在《设计模式:可复用面向对象软件的基础》中所阐述的23种设计模式。
这些二十几年前就提出来的代码设计的总结,主要采用了类和对象的方式,至今依旧被广泛用于C++、Java等面向对象的语言。然而,Kotlin是一门多范式的语言,在之前的章节中我们已经感受过它如何用函数式的语言特性,在程序设计中会带来更多的可能性。我们已经知道,Kotlin中不需要所谓的“单例模式”,因为它在语言层面就已经支持了这一特性。所以也有人说,设计模式无非只是一些编程语言没有支持的特性罢了。某种程度上看确实如此,然而也未必准确,因为越高级的语法特性伴随而来的设计模式也会更加高级。
因此,本章的主要内容是通过Kotlin的语言特性,来重新思考Java中常见的设计模式,由此我们可以进一步认识Kotlin语言特点,以及了解如何在实际代码设计中运用它们。
需要注意的是,本章内容论述的形式依旧采用了GoF针对常见设计模式的分类方式,即创建型模式、行为型模式、结构型模式。同时,基于Kotlin崭新的语言特性,我们在23种常见的设计模式中,实现或替代了Java中部分典型的设计模式。其中访问者模式已经在第4章进行过详细介绍,在Kotlin中可以利用模式匹配和when表达式对其改良,本章将不会重复提及。
9.1 创建型模式
在程序设计中,我们做得最多得事情之一就是去创建一个对象。创建对象虽然看起来简单,但实际的业务或许十分复杂,这些对象的类可能存在子父类继承关系,或者代表了系统中各种不同的结构和功能。因此,创建怎样的对象,如何且何时创建它们,以及对类和对象的配置,都是实际代码编写中需要考虑的问题。
本节将探讨Kotlin中几种最主流的创建型设计模式:工厂方法模式、抽象工厂模式以及构建者模式。
9.1.1 伴生对象增强工厂模式
工厂模式是我们最熟悉的设计模式之一,在有些地方会把工厂模式细分为简单工厂、工厂方法模式以及抽象工厂。本节我们主要介绍简单工厂的模式,它的核心作用就是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类与多个子类的时候,我们可以通过这种模式来创建子类对象。
假设现在有一个电脑加工厂,同时生产个人电脑和服务器主机。我们用熟悉的工厂模式设计描述其业务逻辑:
interface Computer {
val cpu: String
}
class PC(override val cpu: String = "Core") : Computer
class Server(override val cpu: String = "Xeon") : Computer
enum class ComputerType {
PC, Server
}
class ComputerFactory {
fun produce(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
以上代码通过调用ComputerFactory类的produce方法来创建不同的Computer子类对象,这样我们就把创建实例的逻辑与客户端之间实现解耦,当对象创建的逻辑发生变化时(如构造参数的数量发生变化),该模式只需要修改produce方法内部的代码即可,相比直接创建对象的方式更加利于维护。
现在我们用设计好的类写一段测试的代码:
>>> val comp = ComputerFactory().produce(ComputerType.PC)
>>> println(comp.cpu)
Core
这是我们用Kotlin模仿Java中很标准的工厂模式设计,它改善了程序的可维护性,但创建对象的表达上却显得不够简洁。当我们在不同的地方创建Computer的子类对象时,我们都需要先创建一个ComputerFactory类对象。在3.5.2节中我们了解到Kotlin天生支持了单例,接下来我们就用object关键字以及其相关的特性来进一步简化以上的代码设计。
1.用单例代替工厂类
我们已经知道的是,Kotlin支持用object来实现Java中的单例模式。所以,我们可以实现一个ComputerFactory单例,而不是一个工厂类。
object ComputerFactory { // 用object代替class
fun produce(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
然后,我们就可以如此调用:
ComputerFactory.produce(ComputerType.PC)
此外,由于我们通过传入Computer类型来创建不同的对象,所以这里的produce又显得多余。如果你阅读过第7章,那么就会了解Kotlin还支持运算符重载,因此我们可以通过operator操作符重载invoke方法来代替produce,从而进一步简化表达:
object ComputerFactory {
operator fun invoke(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
依靠Kotlin这一特性,我们再创建一个Computer对象就显得非常简洁,与直接创建一个具体类实例显得没有太大区别:
ComputerFactory(ComputerType.PC)
2.伴生对象创建静态工厂方法
当前的工厂模式实现已经足够优雅,然而也许你依旧觉得不够完美:我们是否可以直接通过Computer()而不是ComputerFactory()来创建一个实例呢?
提到这个问题,也许我们还想到了《Effective Java》一书的第1条指导原则:考虑用静态工厂方法代替构造器。相信你已经想到了Kotlin中的伴生对象,它代替了Java中的static,同时在功能和表达上拥有更强的能力。通过在Computer接口中定义一个伴生对象,我们就能够实现以上的需求,代码如下:
interface Computer {
val cpu: String
companion object {
operator fun invoke(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
}
然后再来测试下:
>>> Computer(ComputerType.PC)
Core
在不指定伴生对象名字的情况下,我们可以直接通过Computer来调用其伴生对象中的方法。当然,如果你觉得还是Factory这个名字好,那么也没有问题,我们可以用Factory来命名Computer的伴生对象,如下:
interface Computer {
val cpu: String
companion object Factory {
operator fun invoke(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
}
调用方法如下:
Computer.Factory(ComputerType.PC)
3.扩展伴生对象方法
依靠伴生对象的特性,我们已经很好地实现了经典的工厂模式。同时,这种方式还有一种优势,它比原有Java中的设计更加强大。假设实际业务中我们是Computer接口的使用者,比如它是工程引入的第三方类库,所有的类的实现细节都得到了很好地隐藏。那么,如果我们希望进一步改造其中的逻辑,Kotlin中伴生对象的方式同样可以依靠其扩展函数的特性,很好地实现这一需求。
比如我们希望给Computer增加一种功能,通过CPU型号来判断电脑类型,那么就可以如下实现:
fun Computer.Companion.fromCPU(cpu: String): ComputerType? = when(cpu) {
"Core" -> ComputerType.PC
"Xeon" -> ComputerType.Server
else -> null
}
如果指定了伴生对象的名字为Factory,那么就可以如下实现:
fun Computer.Factory.fromCPU(cpu: String): ComputerType? = ...
9.1.2 内联函数简化抽象工厂
在第6章中,我们了解到Kotlin中的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,我们还可以改进一种更复杂的工厂模式,称为抽象工厂。
工厂模式已经能够很好地处理一个产品等级结构的问题,在上一节中,我们已经用它很好地解决了电脑厂商生产服务器、PC机的问题。进一步思考,当问题上升到多个产品等级结构的时候,比如现在引入了品牌商的概念,我们有好几个不同的电脑品牌,比如Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护,所以这时候我们就需要引入抽象工厂模式。
抽象工厂模式
为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。
在抽象工厂的定义中,我们也可以把“一组相关或相互依赖的对象”称作“产品族”,在上述的例子中,我们就提到了3个代表不同电脑品牌的产品族。下面我们就利用抽象工厂,来实现具体的需求:
interface Computer
class Dell: Computer
class Asus: Computer
class Acer: Computer
class DellFactory: AbstractFactory() {
override fun produce() = Dell()
}
class AsusFactory: AbstractFactory() {
override fun produce() = Asus()
}
class AcerFactory: AbstractFactory() {
override fun produce() = Acer()
}
abstract class AbstractFactory {
abstract fun produce(): Computer
companion object {
operator fun invoke(factory: AbstractFactory): AbstractFactory {
return factory
}
}
}
可以看出,每个电脑品牌拥有一个代表电脑产品的类,它们都实现了Computer接口。此外每个品牌也还有一个用于生产电脑的AbstractFactory子类,可通过AbstractFactory类的伴生对象中的invoke方法,来构造具体品牌的工厂类对象。
fun main(args: Array<String>) {
val dellFactory = AbstractFactory(DellFactory())
val dell = dellFactory.produce()
println(dell)
}
运行该测试用例,结果如下:
Dell@1f32e575
由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时,都需要传入一个具体的工厂类对象作为参数进行构造,这个在语法上显然不是很优雅。下面我们就来看看,如何用Kotlin中的内联函数来改善这一情况。我们所需要做的,就是去重新实现AbstractFactory类中的invoke方法。
abstract class AbstractFactory {
abstract fun produce(): Computer
companion object {
inline operator fun <reified T : Computer> invoke(): AbstractFactory = when (T::class) {
Dell::class -> DellFactory()
Asus::class -> AsusFactory()
Acer::class -> AcerFactory()
else -> throw IllegalArgumentException()
}
}
}
这下我们的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。我们来分析下这段代码:
1)通过将invoke方法用inline定义为内联函数,我们就可以引入reified关键字,使用具体化参数类型的语法特性;
2)要具体化的参数类型为Computer,在invoke方法中我们通过判断它的具体类型,来返回对应的工厂类对象。
我们来看看如何用上述重写的方法,来改善工厂类的创建语法表达:
fun main(args: Array<String>) {
val dellFactory = AbstractFactory<Dell>()
val dell = dellFactory.produce()
println(dell)
}
现在我们终于可以用类似创建一个泛型类对象的方式,来构建一个抽象工厂具体对象了。不管是工厂方法还是抽象工厂,利用Kotlin的语言特性,我们在一定程度上改进、简化了Java中设计模式的实现。在下一节中,我们将继续讨论Kotlin中的构建者模式,这也是一种非常经典的设计模式。
9.1.3 用具名可选参数而不是构建者模式
在Java开发中,你是否写过这样像蛇一样长的构造函数:
// Boolean 类型的参数表示 Robot 是否含有对应固件
Robot robot = new Robot(1, true, true, false, false, false, false, false, false)
刚写完时回头看你还能看懂,一天后你可能已经忘记大半了,再过一个星期你已经不知道这是什么东西了。面对这样的业务场景时,我们惯常的做法是通过Builder(构建者)模式来解决。
构建者模式
构建者模式与单例模式一样,也是Gof设计模式中的一种。它主要做的事情就是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性:代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。
一种糟糕的做法就是设计一个一开头你所看到Robot类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器(telescoping constructor)模式,即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少,但同样存在明显的缺点。因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。
构建者模式可以避免以上的问题,我们用Kotlin来实现Java中的构建者模式,如下面代码9-1所示。
class Robot private constructor(
val code: String,
val battery: String?,
val height: Int?,
val weight: Int?) {
class Builder(val code: String) {
private var battery: String? = null
private var height: Int? = null
private var weight: Int? = null
fun setBattery(battery: String?): Builder {
this.battery = battery
return this
}
fun setHeight(height: Int): Builder {
this.height = height
return this
}
fun setWeight(weight: Int): Builder {
this.weight = weight
return this
}
fun build(): Robot {
return Robot(code, battery, height, weight)
}
}
}
为了避免代码过于冗长,以上的例子我们只选择了4个属性,其中code(机器人代号)为必需属性,battery(电池)、height(高度)、weight(重量)为可选属性。我们再看看如何用这种方式来声明一个Robot对象:
val robot = Robot.Builder("007")
.setBattery("R6")
.setHeight(100)
.setWeight(80)
.build()
我们来分析下它的具体思路:
• Robot类内部定义了一个嵌套类Builder,由它负责创建Robot对象;
• Robot类的构造函数用private进行修饰,这样可以确保使用者无法直接通过Robot声明实例;
• 通过在Builder类中定义set方法来对可选的属性进行设置;
• 最终调用Builder类中的build方法来返回一个Robot对象。
这种链式调用的设计看起来确实优雅了不少,同时对于可选参数的设置也显得比较语义化,它有点近似2.3.8节中介绍的柯里化语法。此外,构建者模式另外一个好处就是解决了多个可选参数的问题,当我们创建对象实例时,只需要用set方法对需要的参数进行赋值即可。
然而,构建者模式也存在一些不足:
1)如果业务需求的参数很多,代码依然会显得比较冗长;
2)你可能会在使用Builder的时候忘记在最后调用build方法;
3)由于在创建对象的时候,必须先创建它的构造器,因此额外增加了多余的开销,在某些十分注重性能的情况下,可能就存在一定的问题。
事实上,当用Kotlin设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时,是这样子描述它的:本质上builder模式模拟了具名的可选参数,就像Ada和Python中的一样。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。
2.具名的可选参数
Kotlin中的函数和构造器都支持这一特性,我们已经在第3章介绍过相关知识,现在再来回顾下。它主要表现为两点:
1)在具体化一个参数的取值时,可以通过带上它的参数名,而不是它在所有参数中的位置决定;
2)由于参数可以设置默认值,这允许我们只给出部分参数的取值,而不必是所有的参数。
因此,我们可以直接使用Kotlin中原生的语法特性来实现构建者模式的效果。现在重新设计以上的Robot例子:
class Robot(
val code: String,
val battery: String? = null,
val height: Int? = null,
val weight: Int? = null
)
val robot1 = Robot(code = "007")
val robot2 = Robot(code = "007", battery = "R6")
val robot3 = Robot(code = "007", height = 100, weight = 80)
可以发现,相比构建者模式,通过具名的可选参数构造类具有很多优点:
1)代码变得十分简单,这不仅表现在Robot类的结构体代码量,我们在声明Robot对象时的语法也要更加简洁;
2)声明对象时,每个参数名都可以是显式的,并且无须按照顺序书写,非常方便灵活;
3)由于Robot类的每个对象都是val声明的,相较构建者模式者中用var的方案更加安全,这在要求多线程并发安全的业务场景中会显得更有优势。
此外,如果你的类的功能足够简单,更好的思路是用data class直接声明一个数据类。如你所知,数据类同样支持以上的所有特性。
3.require方法对参数进行约束
我们再来看看那构建者模式的另外一个作用,就是可以在build方法中对参数添加约束条件。举个例子,假设一个机器人的重量必须根据电池的型号决定,那么在未传入电池型号之前,你便不能对weight属性进行赋值,否则就会抛出异常。现在我们再来重新改下代码9-1中的build方法实现:
fun build(): Robot {
if (weight != null && battery == null) {
throw IllegalArgumentException("Battery should be determined when setting weight.");
} else {
return Robot(code, battery, height, weight)
}
}
运行下具体的测试用例:
val robot = Robot.Builder("007")
.setWeight(100)
.build()
然后就会发现以下的异常信息:
Exception in thread "main" java.lang.IllegalArgumentException: Battery should be determined when setting weight.
这种在build方法中对参数进行约束的手段,可以让业务变得更加安全。那么,通过具名的可选参数来构造类的方案该如何实现呢?
显然,我们同样可以在Robot类的init方法中增加以上的校验代码。然而在Kotlin中,我们在类或函数中还可以使用require关键字进行函数参数限制,本质上它是一个内联的方法,有点类似于Java中的assert。
class Robot(
val code: String,
val battery: String? = null,
val height: Int? = null,
val weight: Int? = null
) {
init {
require(weight == null || battery != null) {
"Battery should be determined when setting weight."
}
}
}
不难发现,如果我们在创建Robot对象时有不符合require条件的行为,就会导致抛出异常。
>>> val robot = Robot(code="007", weight = 100)
java.lang.IllegalArgumentException: Battery should be determined when setting weight.
可见,Kotlin的require方法可以让我们的参数约束代码在语义上变得更加友好。总的来说,在Kotlin中我们应该尽量避免使用构建者模式,因为Kotlin支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。
9.2 行为型模式
当我们用创建型模式创建出类对象之后,就需要在不同对象之间划分职责、产生交互。那些用来识别对象之间的常用交流模式就是本节要讲述的行为型模式。类似上一节,我们同样会用Kotlin的语法来重新思考几种主流的行为型模式,包括:观察者模式、策略模式、模板方法模式、迭代器模式、责任链模式及状态模式。
9.2.1 Kotlin中的观察者模式
观察者模式是我们接触最多的设计模式之一,尤其是在Android开发中,诸多设计都是基于观察者模式来实现的,如MVC架构、rxJava类库的设计等。同时,我们也肯定逃不了用该模式来管理视图变化的逻辑响应。我们先来看看它的定义:
观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。
简单来说,观察者模式无非做两件事情:
• 订阅者(也称为观察者observer)添加或删除对发布者(也称为观察者publisher)的状态监听;
• 发布者状态改变时,将事件通知给监听它的所有观察者,然后观察者执行响应逻辑。
Java自身的标准库提供了java.util.Observable类和java.util.Observer接口,来帮助实现观察者模式。接下来我们就采用它们来实现一个动态更新股价的例子。
import java.util.*
class StockUpdate: Observable() {
val observers = mutableSetOf<Observer>();
fun setStockChanged(price: Int) {
this.observers.forEach { it.update(this, price) }
}
}
class StockDisplay: Observer {
override fun update(o: Observable, price: Any) {
if (o is StockUpdate) {
println("The latest stock price is ${price}.")
}
}
}
在上述例子中,我们首先创建了一个可被观察的发布者类StockUpdate,它维护了一个监听其变化的观察者对象observers,通过它的add和remove方法来进行管理。当StockUpdate类对象执行setStockChanged方法之后,表明股价已经改变,那么就会将更新的股价传递给观察者,执行其update方法来执行响应逻辑。这些观察者都是StockDisplay的对象,当发现接收到的订阅者类型为StockUpdate时,就会打印最新的股价。
下面我们就来创建一个测试用例:
fun main(args: Array<String>) {
val su = StockUpdate()
val sd = StockDisplay()
su.observers.add(sd)
su.setStockChanged(100)
}
// 运行结果
The latest stock price is 100.
由于Kotlin在语法上相比Java要更简洁,所以如果用Java实现以上的例子会需要更多的代码。然而它们的实现的思路是一样的,都是利用了Java标准库中的类和方法。事实上,Kotlin的标准库额外引入了可被观察的委托属性,也可以利用它来实现同样的场景。
1.Observable
我们先用这一委托属性来改造以上的程序,然后再分析其相关的语法。
import kotlin.properties.Delegates
interface StockUpdateListener {
fun onRise(price: Int)
fun onFall(price: Int)
}
class StockDisplay: StockUpdateListener {
override fun onRise(price: Int) {
println("The latest stock price has risen to ${price}.")
}
override fun onFall(price: Int) {
println("The latest stock price has fell to ${price}.")
}
}
class StockUpdate {
var listeners = mutableSetOf<StockUpdateListener>()
var price: Int by Delegates.observable(0) { _, old, new ->
listeners.forEach {
if (new > old) it.onRise(price) else it.onFall(price)
}
}
}
在该版本中,我们让需求变得更加的具体,当股价上涨或下跌时,我们会打印不同的个性化报价文案。如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应,我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得臃肿。换个角度,如果我们把发布者的事件推送看成一个第三方服务,那么它提供的API接口只有一个,API调用者必须承担更多的职能。
显然,使用Delegates.observable()的方案更加灵活。它提供了3个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中实现该接口的onRise和onFall方法,实现了解耦。
同样,我们来运行下该方案的测试用例:
fun main(args: Array<String>) {
val su = StockUpdate()
val sd = StockDisplay()
su.listeners.add(sd)
su.price = 100
su.price = 98
}
// 运行结果
The latest stock price has risen to 100.
The latest stock price has fell to 98.
2.Vetoable
有些时候,我们并不希望监控的值可以被随心所欲地修改。实际上,你可能希望对某些改值的情况进行否决。Kotlin的标准库中除了提供observable这个委托属性之外,还提供了另外一个属性:vetoable。顾名思义,veto代表的是“否决”的意思,vetoable提供了一种功能,在被赋新值生效之前提前进行截获,然后判断是否接受它。
通过以下的例子你可以更好地了解vetoable的使用:
import kotlin.properties.Delegates
var value: Int by Delegates.vetoable(0) { prop, old, new ->
new > 0
}
>>> value = 1
>>> println(value)
1
>>> value = -1
>>> println(value)
1
我们创建了一个可变的Int对象value,同时用by关键字增加了Delegates.vetoable委托属性。它的初始化值为0,只接收被正整数赋值。所以,当我们试图把value改成-1的时候,打印的结果仍然为旧值1。
9.2.2 高阶函数简化策略模式 模板方法模式
本节我们会同时讨论策略模式、模板方法模式,一方面它们解决的问题比较类似,另一方面这两种设计模式都可以依靠Kotlin中的高阶函数特性进行改良。
1.遵循开闭原则:策略模式
假设现在有一个表示游泳运动员的抽象类Swimmer,有一个游泳的方法swim,表示如下:
class Swimmer {
fun swim() {
println("I am swimming...")
}
}
我们用Swimmer类来创建一个对象shaw:
val shaw = Swimmer()
>>> shaw.swim()
I am swimming...
由于shaw在游泳方面很有天赋,他很快掌握了蛙泳、仰泳、自由泳多种姿势。所以我们必须对Swim方法进行改造,变成代表3种不同游泳姿势的方法。
class Swimmer {
fun breaststroke() {
println("I am breaststroking...")
}
fun backstroke() {
println("I am backstroke...")
}
fun freestyle() {
println("I am freestyling...")
}
}
然而这并不是一个很好的设计。首先,并不是所有的游泳运动员都掌握了这3种游泳姿势,如果每个Swimmer类对象都可以调用所有方法,显得比较危险。其次,后续难免会有新的行为方法加入,通过修改Swimmer类的方式违背了开放封闭原则。
因此,更好的做法是将游泳这个行为封装成接口,根据不同的场景我们可以调用不同的游泳方法。比如shaw计划周末游自由泳,其他时间则游蛙泳。策略模式就是一种解决这种场景很好的思路。
策略模式定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
本质上,策略模式做的事情就是将不同的行为策略(Strategy)进行独立封装,与类在逻辑上解耦。然后根据不同的上下文(Context)切换选择不同的策略,然后用类对象进行调用。下面我们用熟悉的方式重新实现游泳的例子:
interface SwimStrategy {
fun swim()
}
class Breaststroke: SwimStrategy {
override fun swim() {
println("I am breaststroking...")
}
}
class Backstroke: SwimStrategy {
override fun swim() {
println("I am backstroke...")
}
}
class Freestyle: SwimStrategy {
override fun swim() {
println("I am freestyling...")
}
}
class Swimmer(val strategy: SwimStrategy) {
fun swim() {
strategy.swim()
}
}
fun main(args: Array<String>) {
val weekendShaw = Swimmer(Freestyle())
weekendShaw.swim()
val weekdaysShaw = Swimmer(Breaststroke())
weekdaysShaw.swim()
}
// 运行结果
I am freestyling...
I am breaststroking...
这个方案实现了解耦和复用的目的,且很好实现了在不同场景切换采用不同的策略。然而,该版本的代码量也比之前多了很多。下面我们来看看Kotlin如何用高阶函数来简化策略模式。
2.高阶函数抽象算法
我们用高阶函数的思路来重新思考下策略类,显然将策略封装成一个函数然后作为参数传递给Swimmer类会更加的简洁。由于策略类的目的非常明确,仅仅是针对行为算法的一种抽象,所以高阶函数式是一种很好的替代思路。
现在,利用高阶函数我们重新实现这个例子:
fun breaststroke() { println("I am breaststroking...") }
fun backstroke() { println("I am backstroking...") }
fun freestyle() { println("I am freestyling...") }
class Swimmer(val swimming: () -> Unit) {
fun swim() {
swimming()
}
}
fun main(args: Array<String>) {
val weekendShaw = Swimmer(::freestyle)
weekendShaw.swim()
val weekdaysShaw = Swimmer(::breaststroke)
weekdaysShaw.swim()
}
代码量一下子变得非常少,而且结构上也更加容易阅读。由于策略算法都封装成了一个个函数,我们在初始化Swimmer类对象时,可以用函数引用的语法(参见2.3节)传递构造参数。当然,我们也可以把函数用val声明成Lambda表达式,那么在传递参数的时候会变得更加简洁直观。
3.模板方法模式:高阶函数代替继承
另一个可用高阶函数函数改良的设计模式,就是模板方法模式。某种程度上,模板方法模式和策略模式要解决的问题是相似的,它们都可以分离通用的算法和具体的上下文。然而,如果说策略模式采用的思路是将算法进行委托,那么传统的模板方法模式更多是基于继承的方式实现的。现在来看看模板方法模式的定义:
定义一个算法中的操作框架,而将一些步骤延迟到子类中,使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。
与策略模式不同,模板方法模式的行为算法具有更明晰的大纲结构,其中完全相同的步骤会在抽象类中实现,可个性化的某些步骤则在其子类中进行定义。举个例子,如果我们去市民事务中心办事时,一般都会有以下几个具体的步骤:
1)排队取号等待;
2)根据自己的需求办理个性化的业务,如获取社保清单、申请市民卡、办理房产证;
3)对服务人员的态度进行评价。
这是一个典型的适用模板方法模式的场景,办事步骤整体是一个算法大纲,其中步骤1)和步骤3)都是相同的算法,而步骤2)则可以根据实际需求个性化选择。接下来我们就用代码实现一个抽象类,它定义了这个例子的操作框架:
abstract class CivicCenterTask {
fun execute() {
this.lineUp()
this.askForHelp()
this.evaluate()
}
private fun lineUp() {
println("line up to take a number");
}
private fun evaluate() {
println("evaluaten service attitude");
}
abstract fun askForHelp()
}
其中askForHelp方法是一个抽象方法。接下来我们再定义具体的子类来继承CivicCenter-Task类,然后对抽象的步骤进行实现。
class PullSocialSecurity: CivicCenterTask {
override fun askForHelp() {
println("ask for pulling the social security")
}
}
class ApplyForCitizenCard: CivicCenterTask {
override fun askForHelp() {
println("apply for a citizen card")
}
}
然后写个测试用例来创建这些子类,进行调用:
val pss = PullSocialSecurity()
>>> pss.execute()
line up to take a number
ask for pulling the social security
evaluaten service attitude
val afcc = ApplyForCitizenCard()
>>> afcc.execute()
line up to take a number
apply for a citizen card
evaluaten service attitude
不出意料,两者的步骤2)的执行结果是不一样的。
不得不说,模板方式模式的代码复用性已经非常高了,但是我们还是得根据不同的业务场景都定义一个具体的子类。幸运的是,在Kotlin中我们同样可以用改造策略模式的类似思路,来简化模板方法模式。依靠高阶函数,我们可以在只需一个CivicCenterTask类的情况下,代替继承实现相同的效果。
class CivicCenterTask {
fun execute(askForHelp: () -> Unit) {
this.lineUp()
askForHelp()
this.evaluate()
}
private fun lineUp() {
println("line up to take a number");
}
private fun evaluate() {
println("evaluaten service attitude");
}
}
fun pullSocialSecurity() {
println("ask for pulling the social security")
}
fun applyForCitizenCard() {
println("apply for a citizen card")
}
代码量果真又减少了许多。再来看看该方案如何调用逻辑:
val task1 = CivicCenterTask()
>>> task1.execute(::pullSocialSecurity)
line up to take a number
ask for pulling the social security
evaluaten service attitude
val task2 = CivicCenterTask()
>>> task2.execute(::applyForCitizenCard)
line up to take a number
apply for a citizen card
evaluaten service attitude
如你所见,在高阶函数的帮助下,我们可以更加轻松地实现模板方式模式。
9.2.3 运算符重载和迭代器模式
迭代器(iterator)是Java中我们非常熟悉的东西了,数据结构如List和Set都内置了迭代器,我们可以用它提供的方法来顺序地访问一个聚合对象中各个元素。
有些时候,我们会定义某些容器类,这些类中包含了大量相同类型的对象。如果你想给这个容器类的对象直接提供迭代的方法,如hasNext、next、first等,那么就可以自定义一个迭代器。然而通常情况下,我们不需要自己再实现一个迭代器,因为Java标准库提供了java.util.Iterator接口,你可以用容器类实现该接口,然后再实现需要的迭代器方法。
这种设计模式就是迭代器模式,它的核心作用就是将遍历和实现分离开来,在遍历的同时不需要暴露对象的内部表示。迭代器模式非常容易理解,你可能已经非常熟悉。但我们还是举个具体的例子来介绍下这种模式,接着引出Kotlin中相关的语法特性,继而进行改良。
1.方案1:实现Iterator接口
data class Book(val name: String)
class Bookcase(val books: List<Book>): Iterator<Book> {
private val iterator: Iterator<Book>
init {
this.iterator = books.iterator()
}
override fun hasNext() = this.iterator.hasNext()
override fun next() = this.iterator.next()
}
fun main(args: Array<String>) {
val bookcase = Bookcase(
listOf(Book("Dive into Kotlin"), Book("Thinking in Java"))
)
while(bookcase.hasNext()) {
println("The book name is ${bookcase.next().name}")
}
}
// 运行结果
The book name is Dive into Kotlin
The book name is Thinking in Java
由于Bookcase对象拥有与List<Book>实例相同的迭代器,我们就可以直接调用后者迭代器所有的方法。一种更简洁的遍历打印方式如下:
for (book in bookcase) {
println("The book name is ${book.name}")
}
2.方案2:重载iterator方法
我们说过,Kotlin还有更好的解决方案。Kotlin有一个非常强大的语言特性,那就是利用operator关键字内置了很多运算符重载功能。我们就可以通过重载Bookcase类的iterator方法,实现一种语法上更加精简的版本:
data class Book(val name: String)
class Bookcase(val books: List<Book>) {
operator fun iterator(): Iterator<Book> = this.books.iterator()
}
很棒吧?我们用一行代码就实现了以上所有的效果。还没完,由于Kotlin还支持扩展函数,这意味着我们可以给所有的对象都内置一个迭代器。
3.方案3:通过扩展函数
假设现在的Book是引入的一个类,你并不能修改它的源码,下面我们就演示如何用扩展的语法来给Bookcase类对象增加迭代的功能:
data class Book(val name: String)
class Bookcase(val books: List<Book>) {}
operator fun Bookcase.iterator(): Iterator<Book> = books.iterator()
代码依旧非常简洁,假如你想对迭代器的逻辑有更多的控制权,那么也可以通过object表达式来实现:
operator fun Bookcase.iterator(): Iterator<Book> = object : Iterator<Book> {
val iterator = books.iterator()
override fun hasNext() = iterator.hasNext()
override fun next() = iterator.next()
}
总的来说,迭代器模式并不是一种很常用的设计模式,但通过它我们可以进一步了解Kotlin中的扩展函数的应用,以及运算符重载功能的强大之处。
9.2.4 用偏函数实现责任链模式
如果你拥有一定程度的Java开发经验,想必接触过责任链模式。假设我们遇到这样的业务需求场景:希望使得多个对象都有机会处理某种类型的请求。那么可能就需要考虑是否可以采用责任链模式。
典型的例子就是Servlet中的Filter和FilterChain接口,它们就采用了责任链模式。利用责任链模式我们可以在接收到一个Web请求时,先进行各种filter逻辑的操作,filter都处理完之后才执行servlet。在这个例子中,不同的filter代表了不同的职责,最终它们形成了一个责任链。
简单来说,责任链模式的目的就是避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
现在我们来举一个更加具体的例子。计算机学院的学生会管理了一个学生会基金,用于各种活动和组织人员工作的开支。当要发生一笔支出时,如果金额在100元之内,可由各个分部长审批;如果金额超过了100元,那么就需要会长同意;但假使金额较大,达到了500元以上,那么就需要学院的辅导员陈老师批准。此外,学院里还有一个不宣的规定,经费的上限为1000元,如果超出则默认打回申请。
当然我们可以用最简单的if-else来实现经费审批的需求。然而根据开闭原则,我们需要将其中的逻辑进行解耦。下面我们就用面向对象的思路结合责任链模式,来设计一个程序。
data class ApplyEvent(val money: Int, val title: String)
interface ApplyHandler {
val successor: ApplyHandler?
fun handleEvent(event: ApplyEvent)
}
class GroupLeader(override val successor: ApplyHandler?): ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money <= 100 -> println("Group Leader handled application: ${event.title}.")
else -> when(successor) {
is ApplyHandler -> successor.handleEvent(event)
else -> println("Group Leader: This application cannot be handdle.")
}
}
}
}
class President(override val successor: ApplyHandler?): ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money <= 500 -> println("President handled application: ${event.title}.")
else -> when(successor) {
is ApplyHandler -> successor.handleEvent(event)
else -> println("President: This application cannot be handdle.")
}
}
}
}
class College(override val successor: ApplyHandler?): ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money > 1000 -> println("College: This application is refused.")
else -> println("College handled application: ${event.title}.")
}
}
}
在这个例子中,我们声明了GroupLeader、President、College三个类来代表学生会部长、分部长、会长及学院,它们都实现了ApplyHandler接口。接口包含了一个可空的后继者对象successor,以及对申请事件的处理方法handleEvent。
当我们把一个申请经费的事件传递给GroupLeader对象进行处理时,它会根据具体的经费金额来判断是否将申请转交给successor对象,也就是President类来处理。以此类推,最终形成了一个责任链的机制。
fun main(args: Array<String>) {
val college = College(null)
val president = President(college)
val groupLeader = GroupLeader(president)
groupLeader.handleEvent(ApplyEvent(10, "buy a pen"))
groupLeader.handleEvent(ApplyEvent(200, "team building"))
groupLeader.handleEvent(ApplyEvent(600, "hold a debate match"))
groupLeader.handleEvent(ApplyEvent(1200, "annual meeting of the college"))
}
运行结果:
Group Leader handled application: buy a pen.
President handled application: team building.
College handled application: hold a debate match.
College: This application is refused.
现在我们再来重新思考下责任链的机理,你会发现整个链条的每个处理环节都有对其输入参数的校验标准,在上述例子中主要是对申请经费事件的金额有要求。当输入参数处于某个责任链环节的有效接收范围之内,该环节才能对其做出正常的处理操作。在编程语言中,我们有一个专门的术语来描述这种情况,这就是“偏函数”。
1.实现偏函数类型:PartialFunction
我们来看看什么是偏函数?
偏函数
偏函数是个数学中的概念,指的是定义域X中可能存在某些值在值域Y中没有对应的值。
为了方便理解,我们可以把偏函数与普通函数进行比较。在一个普通函数中,我们可以给指定类型的参数传入任意该类型的值,比如(Int)->Unit,可以接收任何Int值。而在一个偏函数中,指定类型的参数并不接收任意该类型的值,比如:
fun mustGreaterThan5(x: Int): Boolean {
if (x > 5) {
return true
} else throw Exception("x must be greator than 5")
}
>>> mustGreaterThan5(6)
true
>>> mustGreaterThan5(1)
java.lang.Exception: x must be greator than 5
at Line57.mustGreaterThan5(Unknown Source) // 必须传入大于5的值
之所以提到偏函数是因为在一些函数式编程语言中,如Scala,有一种PartialFunction类型,我们可以用它来简化责任链模式的实现。由于Kotlin的语言特性足够灵活强大,虽然它的标准库并没有支持PartialFunction,然而一些开源库(如funKTionale)已经实现了这个功能。我们来看看如何定义PartialFunction类型:
class PartialFunction<in P1, out R>(private val definetAt: (P1) -> Boolean, private val f: (P1) -> R) :(P1) -> R {
override fun invoke(p1: P1): R {
if(definetAt(p1)) {
return f(p1)
} else {
throw IllegalArgumentException("Value: ($p1) isn't supported by this function")
}
}
fun isDefinedAt(p1: P1) = definetAt(p1)
}
现在来分析下PartialFunction类的具体作用:
• 声明类对象时需接收两个构造参数,其中definetAt为校验函数,f为处理函数;
• 当PartialFunction类对象执行invoke方法时,definetAt会对输出参数p1进行有效性校验;
• 如果校验结果通过,则执行f函数,同时将p1作为参数传递给它;反之则抛出异常。
想必你已经发现,PartialFunction类可以解决责任链模式中各个环节对于输入的校验及处理逻辑的问题,但是依旧有一个问题需要解决,就是如何将请求在整个链条中进行传递。
接下来我们再利用Kotlin的扩展函数给PartialFunction类增加一个orElse方法。在此之前,我们先注意下这个类中的isDefinedAt方法,它其实并没有什么特殊之处,仅仅只是作为拷贝definetAt的一个内部方法,为了在orElse方法中能够被调用。
infix fun <P1, R> PartialFunction<P1, R>.orElse(that: PartialFunction<P1, R>): PartialFunction<P1, R> {
return PartialFunction({ this.isDefinedAt(it) || that.isDefinedAt(it) }) {
when {
this.isDefinedAt(it) -> this(it)
else -> that(it)
}
}
}
可以看出,在orElse方法中可以传入另一个PartialFunction类对象that,它也就是责任链模式中的后继者。当isDefinedAt方法执行结果为false的时候,那么就调用that对象来继续处理申请。
这里用infix关键字来让orElse成为一个中缀函数,从而让链式调用的语法变得更加直观。
2.用orElse构建责任链
接下来我们就用设计好的PartialFunction类及扩展的orElse方法,来重新实现一下最开始的例子。首先来看看如何用PartialFunction定义groupLeader对象:
data class ApplyEvent(val money: Int, val title: String)
val groupLeader = {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 200 }
val handler: (ApplyEvent) -> Unit = { println("Group Leader handled application: ${it.title}.") }
PartialFunction(definetAt, handler)
}()
这里我们借助了自运行Lambda的语法来构建一个PartialFunction的对象groupLeader。definetAt用于校验申请的经费金额是否在学生会部长可审批的范围之内,handler函数用来处理通过金额校验后的审批操作。
同理,我们用类似的方法再定义剩下的president和college对象:
val president = {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 500 }
val handler: (ApplyEvent) -> Unit = { println("President handled application: ${it.title}.") }
PartialFunction(definetAt, handler)
}()
val college = {
val definetAt: (ApplyEvent) -> Boolean = { true }
val handler: (ApplyEvent) -> Unit = {
when {
it.money > 1000 -> println("College: This application is refused.")
else -> println("College handled application: ${it.title}.")
}
}
PartialFunction(definetAt, handler)
}()
最后,我们再用orElse来构建一个基于责任链模式和PartialFunction类型的中缀表达式applyChain:
val applyChain = groupLeader orElse president orElse college
然后我们再运行一个测试用例:
>>> applyChain(ApplyEvent(600, "hold a debate match"))
College handled application: hold a debate match.
借助PartialFunction类的封装,我们不仅大幅度减少了程序的代码量,而且在构建责任链时,可以用orElse获得更好的语法表达。
9.2.5 ADT实现状态模式
我们在第4章中介绍了什么是ADT(代数数据类型),以及如何用它结合模式匹配来抽象业务。ADT是函数式语言中一种强大的语言特性,这一节我们会继续介绍如何用它来实现状态模式。
状态模式与策略模式存在某些相似性,它们都可以实现某种算法、业务逻辑的切换。
以下是状态模式的定义:
状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
状态模式具体表现在:
• 状态决定行为,对象的行为由它内部的状态决定。
• 对象的状态在运行期被改变时,它的行为也会因此而改变。从表面上看,同一个对象,在不同的运行时刻,行为是不一样的,就像是类被修改了一样。
再次与策略模式做比较,你也会发现两种模式之间的不同:策略模式通过在客户端切换不同的策略实现来改变算法;而在状态模式中,对象通过修改内部的状态来切换不同的行为方法。
现在我们就来看个饮水机的例子,假设一个饮水机有3种工作状态,分别为未启动、制冷模式、制热模式。如果你已经了解了第4章的内容,应该会很自然地联想到,可以用密封类来封装一个代表不同饮水机状态的ADT。
sealed class WaterMachineState(open val machine: WaterMachine) {
fun turnHeating() {
if (this !is Heating) {
println("turn heating")
machine.state = machine.heating
} else {
println("The state is already heating mode.")
}
}
fun turnCooling() {
if (this !is Cooling) {
println("turn cooling")
machine.state = machine.cooling
} else {
println("The state is already cooling mode.")
}
}
fun turnOff() {
if (this !is Off) {
println("turn off")
machine.state = machine.off
} else {
println("The state is already off.")
}
}
}
class Off(override val machine: WaterMachine): WaterMachineState(machine)
class Heating(override val machine: WaterMachine): WaterMachineState(machine)
class Cooling(override val machine: WaterMachine): WaterMachineState(machine)
来分析下这段代码:
1)WaterMachineState是一个密封类,拥有一个构造参数为WaterMachine类对象,我们等下再来定义它;
2)在WaterMachineState类外部我们分别定义了Off、Heating、Cooling来代表饮水机的3种不同的工作状态,它们都继承了WaterMachineState类的machine成员属性及3个状态切换的方法;
3)在每个切换状态的方法中,我们通过改变machine对象的state,来实现切换饮水机状态的目的。
接下来我们再来设计下WaterMachine类:
class WaterMachine {
var state: WaterMachineState
val off = Off(this)
val heating = Heating(this)
val cooling = Cooling(this)
init {
this.state = off
}
fun turnHeating() {
this.state.turnHeating()
}
fun turnCooling() {
this.state.turnCooling()
}
fun turnOff() {
this.state.turnOff()
}
}
WaterMachine类非常简单,它的内部主要包含了以下成员属性和方法:
• 引用可变的WaterMachineState类对象state,用来表示当前饮水机所处的工作状态;
• 分别表示3种不同状态的成员属性,off、heating、cooling,它们也是WaterMachineState类的3种子类对象;它们通过传入this进行构造,从而实现在WaterMachineState状态类内部,改变WaterMachine类的state引用值;当WaterMachine类对象初始化时,state默认为off,即饮水机处于未启动状态;
• 3个直接调用的饮水机操作方法,分别执行对应state对象的3种操作,供客户端调用。
夏天到了,办公室的小伙伴都喜欢喝冷水,早上一来就会把饮水机调整为制冷模式,但Shaw有吃泡面的习惯,他想泡面的时候,就会把饮水机变为制热,所以每次他吃了泡面,下一个喝水的同事就需要再切换回制冷。最后要下班了,Kim就会关闭饮水机的电源。
为了满足以上的需求,我们就可以利用WaterMachine类设计一个waterMachineOps函数:
enum class Moment {
EARLY_MORNING, // 早上上班
DRINKING_WATER, // 日常饮水
INSTANCE_NOODLES, // Shaw 吃泡面
AFTER_WORK // 下班
}
fun waterMachineOps(machine: WaterMachine, moment: Moment) {
when(moment) {
Moment.EARLY_MORNING,
Moment.DRINKING_WATER -> when(machine.state) {
!is Cooling -> machine.turnCooling()
}
Moment.INSTANCE_NOODLES -> when(machine.state) {
!is Heating -> machine.turnHeating()
}
Moment.AFTER_WORK -> when(machine.state) {
!is Off -> machine.turnOff()
}
else -> Unit
}
}
这个方法很好地处理了不同角色在不同需求场景下,应该对饮水机执行的不同操作。此外,正如我们之前在了解密封类和when表达式时所知晓的细节,当用when表达式与处理枚举类时,默认的情况必须用else进行处理。然而,由于密封类在类型安全上的额外设计,我们在处理machine对象的state对象时,则不需要考虑这一细节,在语言表达上要简洁得多。
最后,我们来测试下以上的waterMachineOps方法:
fun main(args: Array<String>) {
val machine = WaterMachine()
waterMachineOps(machine, Moment.DRINKING_WATER)
waterMachineOps(machine, Moment.INSTANCE_NOODLES)
waterMachineOps(machine, Moment.DRINKING_WATER)
waterMachineOps(machine, Moment.AFTER_WORK)
}
执行结果如下:
turn cooling
turn heating
turn cooling
turn off
9.3 结构型模式
最后一种设计模式的大类是结构型模式,在对象被创建之后,对象的组成及对象之间的依赖关系就成了我们关注的焦点,这与程序的可维护性息息相关。在这一节中,我们会重点介绍装饰者模式,与Java中传统的设计方法不同,Kotlin依靠类委托和扩展的语言特性,给开发者提供了更多的选择。
9.3.1 装饰者模式:用类委托减少样板代码
在Java中,当我们要给一个类扩展行为的时候,通常有两种选择:
• 设计一个继承它的子类;
• 使用装饰者模式对该类进行装饰,然后对功能进行扩展。
目前为止,我们已经清楚地明白,不是所有场合都适合采用继承的方式来满足类扩展的需求(第3章讨论过“里氏替换原则”),所以很多时候装饰者模式成了我们解决此类问题更好的思路。
装饰者模式
在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。该模式通过创建一个包装对象,来包裹真实的对象。
总结来说,装饰者模式做的是以下几件事情:
• 创建一个装饰类,包含一个需要被装饰类的实例;
• 装饰类重写所有被装饰类的方法;
• 在装饰类中对需要增强的功能进行扩展。
可以发现,装饰者模式很大的优势在于符合“组合优于继承”的设计原则,规避了某些场景下继承所带来的问题。然而,它有时候也会显得比较啰唆,因为要重写所有的装饰对象方法,所以可能存在大量的样板代码。
在Kotlin中,我们可以让装饰者模式的实现变得更加优雅。猜想你已经想到了它的类委托特性,我们可以利用by关键字,将装饰类的所有方法委托给一个被装饰的类对象,然后只需覆写需要装饰的方法即可。
下面我们来实现一个具体的例子:
interface MacBook {
fun getCost(): Int
fun getDesc(): String
fun getProdDate(): String
}
class MacBookPro: MacBook {
override fun getCost() = 10000
override fun getDesc() = "Macbook Pro"
override fun getProdDate() = "Late 2011"
}
// 装饰类
class ProcessorUpgradeMacbookPro(val macbook: MacBook) : MacBook by macbook {
override fun getCost() = macbook.getCost() + 219
override fun getDesc() = macbook.getDesc() + ", +1G Memory"
}
如代码所示,我们创建一个代表MacBook Pro的类,它实现了MacBook的接口的3个方法,分别表示它的预算、机型信息,以及生产的年份。当你觉得原装MacBook的内存配置不够的时候,希望再加入一条1G的内存,这时候配置信息和预算方法都会受到影响。
所以通过Kotlin的类委托语法,我们实现了一个ProcessorUpgradeMacbookPro类,该类会把MacBook接口所有的方法都委托给构造参数对象macbook。因此,我们只需通过覆写的语法来重写需要变更的cost和getDesc方法。由于生产年份是不会改变的,所以不需重写,ProcessorUpgradeMacbookPro类会自动调用装饰对象的getProdDate方法。
运行一下测试用例:
fun main(args: Array<String>) {
val macBookPro = MacBookPro()
val processorUpgradeMacbookPro = ProcessorUpgradeMacbookPro(macBookPro)
println(processorUpgradeMacbookPro.getCost())
println(processorUpgradeMacbookPro.getDesc())
}
// 运行结果
10219
Macbook Pro, +1G Memory
总的来说,Kotlin通过类委托的方式减少了装饰者模式中的样板代码,否则在不继承Macbook类的前提下,我们得创建一个装饰类和被装饰类的公共父抽象类。接下来,我们再来看看Kotlin中另一种代替装饰类的实现思路。
9.3.2 通过扩展代替装饰者
我们已经在第7章了解到“扩展”这种Kotlin中强大的语言特性,它很灵活的应用就是实现特设多态。特设多态可以针对不同的版本实现完全不同的行为,这与装饰者模式不谋而合,因为后者也是给一个特定对象添加额外行为。
在上一节中,我们已经了解了如何用类委托的语法来简化装饰者模式。实际上,在某些场景中,我们可以利用Kotlin的扩展语法来代替装饰类实现类似的目的。下面就来看一个具体的例子:
class Printer {
fun drawLine() {
println("————————")
}
fun drawDottedLine() {
println("- - - - -")
}
fun drawStars() {
println("********")
}
}
这一次,我们定义了一个Printer绘图类,它有3个画图方法,分别可以绘制实线、虚线及星号线。接下来,我们新增了一个需求,就是希望在每次绘图开始和结束后有一段文字说明,来标记整个绘制的过程。
一种思路是对每个绘图的方法装饰新增的功能,然而这肯定显得冗余,尤其是未来Printer类可能新增其他的绘图方法,这不是一种优雅的设计思路。
现在我们来看看用扩展来代替装饰类,提供更好的解决方案:
fun Printer.startDraw(decorated: Printer.() -> Unit) {
println("+++ start drawing +++")
this.decorated()
println("+++ end drawing +++")
}
你肯定对扩展方法的语法再熟悉不过了,上述代码中我们给Printer类扩展了一个startDraw方法,它包含一个可执行的Printer类方法decorated,当我们调用startDraw时,会在decorated方法执行的前后,分别打印一段表明“绘图开始”和“绘图结束”的文字说明。
那么我们再来看看如何使用这个startDraw方法:
fun main(args: Array<String>) {
Printer().run {
startDraw {
drawLine()
}
startDraw {
drawDottedLine()
}
startDraw {
drawStars()
}
}
}
还记得前面介绍的run方法吗?它接收一个lambda函数为参数,以闭包形式返回,返回值为最后一行的值或者指定的return的表达式。结合run的语法,我们就可以比较优雅地实现我们的需求。
以上测试结果如下:
+++ start drawing +++
————————
+++ end drawing +++
+++ start drawing +++
- - - - -
+++ end drawing +++
+++ start drawing +++
********
+++ end drawing +++
9.4 本章小结
(1)改造工厂模式
Kotlin的object是天生的单例,同时通过伴生对象的语法来创建更加简洁的工厂模式,以及静态工厂方法。此外,由于伴生对象也支持扩展,这使得Kotlin中改造后的工厂模式比Java中的更加灵活强大。
(2)内联函数
简化抽象工厂内联函数的一大奇特之处在于可以获取具体的参数类型,这一特性在实现抽象工厂的场景中大放光彩,我们终于可以用类似创建一个泛型类对象的方式,来构建一个抽象工厂具体对象。
(3)弱化构建者模式的使用
构建者模式的本质在于模拟了具名的可选参数,就像Ada和Python中的一样。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。因此,在用Kotlin设计程序时,我们很少会使用构建者模式,而是直接利用类的原生特性来规避构造参数过长的问题。同时,我们还可以利用类原生的require方法对参数的值进行约束。
(4)用委托属性实现观察者模式
Kotlin的标准库中直接支持了observable这一委托属性,这使其在实现观察者模式的时候相比Java要更加容易和方便。
(5)高阶函数简化设计模式
由于Kotlin支持高阶函数的语法,这给它在实现策略模式及模板方法模式时带来了便利。在策略模式中,我们可以用高阶函数来抽象算法,在模板方法模式中,可以直接利用高阶函数的特性来代替继承实现类似的效果。
(6)重载iterator方法
Kotlin支持运算符重载功能,我们可以利用operator关键词来重载iterator方法,这个巧妙的特性可以替代传统Java中依赖Iterator接口的设计。同时,结合扩展函数的语法,我们可以实现更简洁、更加强大的迭代器模式。
(7)偏函数实现责任链模式
Kotlin的语法使其能够构建一套基于偏函数的责任链模式语法,通过中缀表达式的形式,结合orElse方法,我们可以在调用责任链的时候更加直观优雅。
(8)ADT实现状态模式
我们曾用了一章的篇幅来介绍什么是ADT(代数数据类型),以及如何用它结合模式匹配来抽象业务。ADT是函数式语言中一种强大的语言特性,利用它实现状态模式是一个很好的优势体现。
(9)装饰者模式实现新思路
相比Java,Kotlin在实现装饰者模式上有更多的选择。依靠类委托的语法,我们可以避免大量的样板代码。此外某些场景下,通过扩展来代替装饰类是更好的选择。
更多推荐

所有评论(0)