《Kotlin核心编程》笔记:设计模式
创建型模式
主流的创建型模式有:工厂方法模式、抽象工厂模式、构建者模式
伴生对象增强工厂模式
在有些地方会把工厂模式细分为简单工厂、工厂方法模式以及抽象工厂。
这里主要介绍简单工厂的模式,它的核心作用就是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类与多个子类的时候,我们可以通过这种模式来创建子类对象。
假设现在有一个电脑加工厂,同时生产个人电脑和服务器主机。我们用熟悉的工厂模式设计描述其业务逻辑:
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()
}
}
}
fun main() {
val compter = ComputerFactory().produce(ComputerType.PC)
println(compter.cpu)
}
以上代码通过调用ComputerFactory
类的produce
方法来创建不同的Computer
子类对象,这样我们就把创建实例的逻辑与客户端之间实现解耦,当对象创建的逻辑发生变化时(如构造参数的数量发生变化),该模式只需要修改produce方法内部的代码即可,相比直接创建对象的方式更加利于维护。
用单例代替工厂类
我们已经知道的是,Kotlin 支持用 object
来实现 Java 中的单例模式。所以,我们可以实现一个 ComputerFactory
单例,而不是一个工厂类。
object ComputerFactory { // 用 object 代替 class
fun produce(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
fun main() {
val compter = ComputerFactory.produce(ComputerType.PC)
println(compter.cpu)
}
我们可以通过operator
操作符重载invoke
方法来代替produce
,从而进一步简化表达:
object ComputerFactory {
operator fun invoke(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
fun main() {
val compter = ComputerFactory(ComputerType.PC)
println(compter.cpu)
}
伴生对象创建静态工厂方法
我们是否可以直接通过 Computer()
而不是 ComputerFactory()
来创建一个实例呢?
考虑用静态工厂方法代替构造器。相信你已经想到了 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()
}
}
}
}
class PC(override val cpu: String = "Core") : Computer
class Server(override val cpu: String = "Xeon") : Computer
enum class ComputerType { PC, Server }
fun main() {
val compter = Computer(ComputerType.PC)
println(compter.cpu)
}
在不指定伴生对象名字的情况下,我们可以直接通过 Computer
来调用其伴生对象中的方法。当然,如果你喜欢伴生对象有名字,我们还是可以命名 Computer
的伴生对象,如下用 Factory
来命名:
interface Computer {
val cpu: String
companion object Factory {
operator fun invoke(type: ComputerType): Computer {
return when (type) {
ComputerType.PC -> PC()
ComputerType.Server -> Server()
}
}
}
}
fun main() {
val compter = Computer.Factory(ComputerType.PC)
println(compter.cpu)
}
注意:即便伴生对象是有名字的情况下,在调用时依然可以省略显示指定的名字。
扩展伴生对象方法
假设实际业务中我们是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? = when(cpu) {
"Core" -> ComputerType.PC
"Xeon" -> ComputerType.Server
else -> null
}
调用:
fun main() {
val type = Computer.fromCPU("Core")
println(type)
}
内联函数简化抽象工厂
Kotlin中 的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,我们还可以改进一种更复杂的工厂模式,称为抽象工厂。
工厂模式已经能够很好地处理一个产品等级结构的问题,在上一节中,我们已经用它很好地解决了电脑厂商生产服务器、PC机的问题。进一步思考,当问题上升到多个产品等级结构的时候,比如现在引入了品牌商的概念,我们有好几个不同的电脑品牌,比如 Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护,所以这时候我们就需要引入抽象工厂模式。
抽象工厂模式
为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。
在抽象工厂的定义中,我们也可以把“ 一组相关或相互依赖的对象” 称作 “产品族”,在上述的例子中,我们就提到了3个代表不同电脑品牌的产品族。
下面我们就利用抽象工厂,来实现具体的需求:
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
}
}
}
fun main() {
val dellFactory = AbstractFactory(DellFactory())
val dell = dellFactory.produce()
println(dell)
}
以上代码当你每次创建具体的工厂类时,都需要传入一个具体的工厂对象作为参数进行构造,这个在语法上显然不是很优雅。
下面我们可以用 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()
}
}
}
fun main() {
val dellFactory = AbstractFactory<Dell>()
val dell = dellFactory.produce()
println(dell)
}
- 1)通过将
invoke
方法用inline
定义为内联函数,我们就可以引入reified
关键字,使用具体化参数类型的语法特性; - 2)要具体化的参数类型为
Computer
,在invoke
方法中我们通过判断它的具体类型,来返回对应的工厂类对象。
用具名可选参数而不是构建者模式
在 Java 开发中,你是否写过这样像蛇一样长的构造函数:
// Boolean 类型的参数表示 Robot 是否含有对应固件
Robot robot = new Robot(1, true, true, false, false, false, false, false, false)
刚写完时回头看你还能看懂,一天后你可能已经忘记大半了,再过一个星期你已经不知道这是什么东西了。面对这样的业务场景时,我们惯常的做法是通过 Builder(构建者)模式来解决。
构建者模式
构建者模式主要做的事情就是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性:代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。
一种糟糕的做法就是设计一个在上面你所看到Robot
类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器(telescoping constructor)模式,即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少,但同样存在明显的缺点。因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。
构建者模式可以避免以上的问题,我们用 Kotlin 来实现 Java 中的构建者模式:
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)
}
}
}
fun main() {
val robot = Robot.Builder("007")
.setBattery("R6")
.setHeight(100)
.setWeight(80)
.build()
}
这种链式调用的设计看起来确实优雅了不少,同时对于可选参数的设置也显得比较语义化,它有点近似柯里化语法。此外,构建者模式另外一个好处就是解决了多个可选参数的问题,当我们创建对象实例时,只需要用set
方法对需要的参数进行赋值即可。
然而,构建者模式也存在一些不足:
- 1)如果业务需求的参数很多,代码依然会显得比较冗长;
- 2)你可能会在使用
Builder
的时候忘记在最后调用build
方法; - 3)由于在创建对象的时候,必须先创建它的构造器,因此额外增加了多余的开销,在某些十分注重性能的情况下,可能就存在一定的问题。
事实上,当用 Kotlin 设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时,是这样描述它的:本质上 Builder 模式模拟了具名的可选参数,就像 Ada 和 Python 中的一样。幸运的是,Kotlin 也是这样一门拥有具名可选参数的编程语言。
具名的可选参数
Kotlin 中的函数和构造器都支持这一特性,现在再来回顾下。它主要表现为两点:
- 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
直接声明一个数据类。如你所知,数据类同样支持以上的所有特性。
require 方法对参数进行约束
构建者模式的另外一个作用,就是可以在build
方法中对参数添加约束条件。
举个例子,假设一个机器人的重量必须根据电池的型号决定,那么在未传入电池型号之前,你便不能对weight
属性进行赋值,否则就会抛出异常。
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 支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。
行为型模式
主流的行为型模式有:观察者模式、策略模式、模板方法模式、迭代器模式、责任链模式及状态模式。
Kotlin 中的观察者模式
观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。
简单来说,观察者模式无非做两件事情:
- 订阅者(观察者observer)添加或删除对发布者(被观察者)的状态监听;
- 发布者状态改变时,将事件通知给监听它的所有观察者,然后观察者执行响应逻辑。
Observable
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}.")
}
}
}
fun main() {
val su = StockUpdate()
val sd = StockDisplay()
su.observers.add(sd)
su.setStockChanged(100)
}
在上述例子中,创建了一个可被观察的发布者类StockUpdate
,它维护了一个监听其变化的观察者对象observers
,通过它的add
和remove
方法来进行管理。当StockUpdate
类对象执行setStockChanged
方法之后,那么就会将更新的股价传递给观察者,执行其update
方法来执行响应逻辑。
Delegates.Observable
事实上,Kotlin 的标准库额外引入了可被观察的委托属性,也可以利用它来实现同样的场景。
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)
}
}
}
fun main() {
val su = StockUpdate()
val sd = StockDisplay()
su.listeners.add(sd)
su.price = 100
su.price = 98
}
在该版本中,我们实现了更加具体的需求:当股价上涨或下跌时,打印不同的个性化报价文案。
如果你仔细思考,会发现实现java.util.Observer
接口的类只能覆写update
方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应,我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得臃肿。
换个角度,如果我们把发布者的事件推送看成一个第三方服务,那么它提供的 API 接口只有一个,API 调用者必须承担更多的职能。
显然,使用 Delegates.observable()
的方案更加灵活。它提供了 3 个参数,依次代表委托属性的元数据KProperty
对象、旧值以及新值。
通过额外定义一个StockUpdateListener
接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay
中实现该接口的onRise
和onFall
方法,实现了解耦。
Delegates.Vetoable
有些时候,我们并不希望监控的值可以被随心所欲地修改。Kotlin 的标准库中除了observable
这个委托属性之外,还提供了一个 vetoable
属性,顾名思义,veto
代表的是“ 否决” 的意思,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
。
高阶函数简化策略模式、模板方法模式
遵循开闭原则:策略模式
假设现在有一个表示游泳运动员的抽象类Swimmer
,有一个游泳的方法swim
,表示如下:
class Swimmer {
fun swim() {
println("I am swimming...")
}
}
fun main() {
val shaw = Swimmer()
shaw.swim()
}
由于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
类的方式违背了开放封闭原则。
因此,更好的做法是将游泳这个行为封装成接口,根据不同的场景我们可以调用不同的游泳方法。策略模式就是一种解决这种场景很好的思路。
策略模式定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
本质上,策略模式做的事情就是将不同的行为策略(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(private val strategy: SwimStrategy) {
fun swim() {
strategy.swim()
}
}
fun main() {
// tom会自由泳
val tom = Swimmer(Freestyle())
tom.swim()
// jack会蛙泳
val jack = Swimmer(Breaststroke())
jack.swim()
}
这个方案实现了解耦和复用的目的,且很好实现了在不同场景切换采用不同的策略。然而,该版本的代码量也比之前多了很多。
高阶函数抽象算法
我们用高阶函数的思路来重新思考下策略类,显然将策略封装成一个函数然后作为参数传递给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() {
// tom会自由泳
val tom = Swimmer(::freestyle)
tom.swim()
// jack会蛙泳
val jack = Swimmer(::breaststroke)
jack.swim()
}
可以看到,代码量骤减,且结构也清晰易读。由于策略算法都封装成了一个个函数,我们在初始化Swimmer
类对象时,可以用函数引用的语法传递构造参数。当然,我们也可以把函数用val
声明成Lambda
表达式,那么在传递参数的时候会变得更加简洁直观。
模板方法模式:高阶函数代替继承
另一个可用高阶函数函数改良的设计模式,就是模板方法模式。
某种程度上,模板方法模式和策略模式要解决的问题是相似的,它们都可以分离通用的算法和具体的上下文。然而,如果说策略模式采用的思路是将算法进行委托,那么传统的模板方法模式更多是基于继承的方式实现的。
来看看模板方法模式的定义:
- 定义一个算法中的操作框架,而将一些步骤延迟到子类中,使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。
与策略模式不同,模板方法模式的行为算法具有更清晰的大纲结构,其中完全相同的步骤会在抽象类中实现,可个性化的某些步骤在其子类中进行定义。
举个例子,如果我们去市民事务中心办事时,一般都会有以下几个具体的步骤:
- 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
方法是一个抽象方法。接下来我们再定义具体的子类来继承CivicCenterTask
类,然后对抽象的步骤进行实现。
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")
}
}
调用:
fun main() {
val task = PullSocialSecurity()
task.execute()
val task2 = ApplyForCitizenCard()
task2.execute()
}
在 Kotlin 中我们同样可以用改造策略模式的类似思路,来简化模板方法模式。把抽象的部分使用高阶函数来传递。
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")
}
fun main() {
val task1 = CivicCenterTask()
task1.execute(::pullSocialSecurity)
val task2 = CivicCenterTask()
task2.execute(::applyForCitizenCard)
}
可见,在高阶函数的帮助下,我们可以更加轻松地实现模板方式模式。
运算符重载和迭代器模式
有些时候,我们会定义某些容器类,这些类中包含了大量相同类型的对象。如果你想给这个容器类的对象直接提供迭代的方法,如hasNext
、next
、first
等,那么就可以自定义一个迭代器。然而通常情况下,我们不需要自己再实现一个迭代器,因为Java标准库提供了java.util.Iterator
接口,你可以用容器类实现该接口,然后再实现需要的迭代器方法。
这种设计模式就是迭代器模式,它的核心作用就是将遍历和实现分离开来,在遍历的同时不需要暴露对象的内部表示。
实现Iterator
接口的简单例子:
data class Book(val name:String)
class Bookcase(books: List<Book>): Iterator<Book> {
private val iterator: Iterator<Book> = books.iterator()
override fun hasNext() = this.iterator.hasNext()
override fun next() = this.iterator.next()
}
fun main() {
val bookcase = Bookcase(
listOf(
Book("DiveintoKotlin"),
Book("ThinkinginJava")
)
)
while (bookcase.hasNext()) {
println("Thebooknameis${bookcase.next().name}")
}
}
由于Bookcase
对象拥有与List<Book>
实例相同的迭代器,我们就可以直接调用后者迭代器所有的方法。
当然,我们一般会使用更简洁的遍历打印方式如下:
for (book in bookcase) {
println("The book name is ${book.name}")
}
重载 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 还支持扩展函数,这意味着我们可以给所有的对象都内置一个迭代器。
通过扩展函数重载 iterator 方法
假设现在的Bookcase
是引入的一个类,你并不能修改它的源码,下面我们就演示如何用扩展的语法来给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 中的扩展函数的应用,以及运算符重载功能的强大之处。
用偏函数实现责任链模式
简单来说,责任链模式的目的就是避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
现在我们来举一个更加具体的例子。计算机学院的学生会管理了一个学生会基金,用于各种活动和组织人员工作的开支。当要发生一笔支出时,如果金额在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}.")
}
}
}
fun main() {
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.
在这个例子中,我们声明了GroupLeader
、President
、College
三个类来代表学生会部长、分部长、会长及学院,它们都实现了ApplyHandler
接口。接口包含了一个可空的后继者对象successor
,以及对申请事件的处理方法handleEvent
。当我们把一个申请经费的事件传递给GroupLeader
对象进行处理时,它会根据具体的经费金额来判断是否将申请转交给successor
对象,也就是President
类来处理。以此类推,最终形成了一个责任链的机制。
现在我们再来重新思考下责任链的机理,你会发现整个链条的每个处理环节都有对其输入参数的校验标准,在上述例子中主要是对申请经费事件的金额有要求。当输入参数处于某个责任链环节的有效接收范围之内,该环节才能对其做出正常的处理操作。在编程语言中,我们有一个专门的术语来描述这种情况,这就是“偏函数” 。
实现偏函数类型: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
,然而一些开源库已经实现了这个功能。我们来看看如何定义PartialFunction
类型:
class PartialFunction<in P1, out R>(
private val defineAt : (P1) -> Boolean,
private val f : (P1) -> R
) : (P1) -> R {
override fun invoke(p1 : P1) : R {
if (defineAt(p1)) {
return f(p1)
} else {
throw IllegalArgumentException("Value: ($p1) isn't supported by this function")
}
}
fun isDefinedAt(p1: P1) = defineAt(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
成为一个中缀函数,从而让链式调用的语法变得更加直观。
用 orElse 构建责任链
接下来我们就用设计好的PartialFunction
类及扩展的orElse
方法,来重新实现一下最开始的例子。
val groupLeader = run {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 200 }
val handler: (ApplyEvent) -> Unit = { println(" groupLeader ... ") }
PartialFunction(definetAt, handler)
}
val president = run {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 500 }
val handler: (ApplyEvent) -> Unit = { println(" president ... ") }
PartialFunction(definetAt, handler)
}
val college = run {
val definetAt: (ApplyEvent) -> Boolean = { true }
val handler: (ApplyEvent) -> Unit = { println(" college ... ") }
PartialFunction(definetAt, handler)
}
然后调用如下:
fun main() {
val applyChain = groupLeader orElse president orElse college
applyChain(ApplyEvent(10, "buy a pen"))
applyChain(ApplyEvent(200, "team building"))
applyChain(ApplyEvent(600, "hold a debate match"))
applyChain(ApplyEvent(1200, "annual meeting of the college"))
}
这里用orElse
获得了更好的语法表达。
ADT 实现状态模式
状态模式与策略模式存在某些相似性,它们都可以实现某种算法、业务逻辑的切换。以下是状态模式的定义:
- 状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
状态模式具体表现在:
- 状态决定行为,对象的行为由它内部的状态决定。
- 对象的状态在运行期被改变时,它的行为也会因此而改变。 从表面上看,同一个对象,在不同的运行时刻,行为是不一样的,就像是类被修改了一样。
再次与策略模式做比较,你也会发现两种模式之间的不同:
- 策略模式通过在客户端切换不同的策略实现来改变算法;
- 而在状态模式中,对象通过修改内部的状态来切换不同的行为方法。
来看个饮水机的例子,假设一个饮水机有 3 种工作状态,分别为未启动、制冷模式、制热模式。可以用密封类来封装一个代表不同饮水机状态的 ADT。
class WaterMachine {
var state : WaterMachineState = WaterMachineState.Off(this)
fun turnHeating() {
this.state.turnHeating()
}
fun turnCooling() {
this.state.turnCooling()
}
fun turnOff() {
this.state.turnOff()
}
}
sealed class WaterMachineState(open val machine: WaterMachine) {
class Off(override val machine: WaterMachine): WaterMachineState(machine)
class Heating(override val machine: WaterMachine): WaterMachineState(machine)
class Cooling(override val machine: WaterMachine): WaterMachineState(machine)
fun turnHeating() {
if (this !is Heating) {
machine.state = Heating(machine)
println("turn heating")
} else {
println("The state is already heating mode.")
}
}
fun turnCooling() {
if (this !is Cooling) {
machine.state = Cooling(machine)
println("turn cooling")
} else {
println("The state is already cooling mode.")
}
}
fun turnOff() {
if (this !is Off) {
machine.state = Off(machine)
println("turn off")
} else {
println("The state is already off.")
}
}
}
利用上面的ADT数据结构,来实现这样一个需求:Shaw早上上班的时候会把饮水机调整为制冷模式,他想泡面的时候,就会把饮水机变为制热,所以每次他吃了泡面,下一个喝水的同事就需要再切换回制冷。最后要下班了,Kim就会关闭饮水机的电源。
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 -> machine.turnCooling()
Moment.INSTANCE_NOODLES -> machine.turnHeating()
Moment.AFTER_WORK -> machine.turnOff()
}
}
fun main() {
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
结构型模式
装饰者模式:用接口委托减少样板代码
在 Java 中,当我们要给一个类扩展行为的时候,通常有两种选择:
- 设计一个继承它的子类;
- 使用装饰者模式对该类进行装饰,然后对功能进行扩展。
不是所有场合都适合采用继承的方式来满足类扩展的需求(需遵循“里氏替换原则”),所以很多时候装饰者模式成了我们解决此类问题更好的思路。
装饰者模式:在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。该模式通过创建一个包装对象,来包裹真实的对象。
总结来说,装饰者模式做的是以下几件事情:
- 创建一个装饰类,包含一个需要被装饰类的实例;
- 装饰类重写所有被装饰类的方法;
- 在装饰类中对需要增强的功能进行扩展。
可以发现,装饰者模式很大的优势在于符合 “组合优于继承” 的设计原则,规避了某些场景下继承所带来的问题。然而,它有时候也会显得比较啰唆,因为要重写所有的装饰对象方法,所以可能存在大量的样板代码。
在 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 MacBookUpgrade(val macBook: MacBook) : MacBook by macBook {
override fun getCost() = macBook.getCost() + 219
override fun getDesc() = macBook.getDesc() + ", + 1G Memory"
}
fun main() {
val macBookPro = MacBookPro()
val macBookUpgrade = MacBookUpgrade(macBookPro)
println(macBookUpgrade.getCost())
println(macBookUpgrade.getDesc())
}
如代码所示,我们创建一个代表 MacBook Pro
的类,它实现了MacBook
的接口的3个方法,分别表示它的预算、机型信息,以及生产的年份。
当你觉得原装MacBook
的内存配置不够的时候,需要对其进行一下配置升级,比如再加入 1G 的内存,这时候配置信息和预算方法都会受到影响。
所以通过 Kotlin 的类委托语法, 我们实现了一个MacBookUpgrade
类,该类会把MacBook
接口所有的方法都委托给构造参数对象macbook
。
因此,我们只需通过覆写的语法来重写需要变更的cost
和getDesc
方法。由于生产年份是不会改变的,所以不需重写,MacBookUpgrade
类会自动调用装饰对象的getProdDate
方法。
总的来说,Kotlin 通过类委托的方式减少了装饰者模式中的样板代码,否则在不继承Macbook
类的前提下,我们得创建一个装饰类和被装饰类的公共父抽象类。
通过扩展函数代替装饰者
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 +++")
}
fun main() {
Printer().run {
startDraw { drawLine() }
startDraw { drawDottedLine() }
startDraw { drawStars() }
}
}
还记得之前介绍的run
方法吗?它接收一个lambda
函数为参数,以闭包形式返回,返回值为最后一行的值或者指定的return
的表达式。结合run
的语法,我们就可以比较优雅地实现我们的需求。
总结
设计模式 | Kotlin 中的解决方式 | 备注 |
---|---|---|
工厂方法模式 | 单例 object 类 + invoke 重载伴生对象 companion object + invoke 重载伴生对象扩展方法 | 创建型模式 |
抽象工厂模式 | 内联函数 inline + reified | 创建型模式 |
构建者模式 | 具名可选参数 + require 方法约束 | 创建型模式 |
观察者模式 | Delegates.Observable 委托语法Delegates.Vetoable 委托语法 | 行为型模式 |
策略模式 | 高阶函数(:: 函数引用语法) | 行为型模式 |
模板方法模式 | 高阶函数(:: 函数引用语法) | 行为型模式 |
迭代器模式 | 运算符重载 iterator 扩展函数重载 iterator | 行为型模式 |
责任链模式 | 仿造偏函数 | 行为型模式 |
状态模式 | 利用 ADT(代数数据类型) | 行为型模式 |
装饰者模式 | 接口委托 by 语法扩展函数 | 结构型模式 |
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!