《Kotlin核心编程》笔记:设计模式

创建型模式

主流的创建型模式有:工厂方法模式、抽象工厂模式、构建者模式

伴生对象增强工厂模式

在有些地方会把工厂模式细分为简单工厂、工厂方法模式以及抽象工厂。

这里主要介绍简单工厂的模式,它的核心作用就是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类与多个子类的时候,我们可以通过这种模式来创建子类对象。

假设现在有一个电脑加工厂,同时生产个人电脑和服务器主机。我们用熟悉的工厂模式设计描述其业务逻辑:

interface Computer {val cpu: String
}class PC(override val cpu: String = "Core") : Computer
class Server(override val cpu: String = "Xeon") : Computerenum 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 代替 classfun 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: Stringcompanion 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") : Computerenum class ComputerType { PC, Server }fun main() {val compter = Computer(ComputerType.PC)println(compter.cpu)
}

在不指定伴生对象名字的情况下,我们可以直接通过 Computer 来调用其伴生对象中的方法。当然,如果你喜欢伴生对象有名字,我们还是可以命名 Computer 的伴生对象,如下用 Factory 来命名:

interface Computer {val cpu: Stringcompanion 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.Serverelse -> null
}

如果指定了伴生对象的名字为Factory,那么就可以如下实现:

fun Computer.Factory.fromCPU(cpu: String): ComputerType? = when(cpu) {"Core" -> ComputerType.PC"Xeon" -> ComputerType.Serverelse -> 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(): Computercompanion 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(): Computercompanion 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? = nullprivate var height: Int? = nullprivate var weight: Int? = nullfun setBattery(battery: String?): Builder {this.battery = batteryreturn this}fun setHeight(height: Int): Builder {this.height = heightreturn this}fun setWeight(weight: Int): Builder {this.weight = weightreturn 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,通过它的addremove方法来进行管理。当StockUpdate类对象执行setStockChanged方法之后,那么就会将更新的股价传递给观察者,执行其update方法来执行响应逻辑。

Delegates.Observable

事实上,Kotlin 的标准库额外引入了可被观察的委托属性,也可以利用它来实现同样的场景。

import kotlin.properties.Delegatesinterface 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 = 100su.price = 98
}

在该版本中,我们实现了更加具体的需求:当股价上涨或下跌时,打印不同的个性化报价文案。

如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应,我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得臃肿。

换个角度,如果我们把发布者的事件推送看成一个第三方服务,那么它提供的 API 接口只有一个,API 调用者必须承担更多的职能。

显然,使用 Delegates.observable() 的方案更加灵活。它提供了 3 个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。

通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中实现该接口的onRiseonFall方法,实现了解耦。

Delegates.Vetoable

有些时候,我们并不希望监控的值可以被随心所欲地修改。Kotlin 的标准库中除了observable这个委托属性之外,还提供了一个 vetoable 属性,顾名思义,veto代表的是“ 否决” 的意思,vetoable提供了一种功能,在被赋新值生效之前提前进行截获,然后判断是否接受它。

例如:

import kotlin.properties.Delegatesvar value: Int by Delegates.vetoable(0) { prop, old, new ->new > 0
}value = 1
println(value)
>>> 1value = -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)
}

可见,在高阶函数的帮助下,我们可以更加轻松地实现模板方式模式。

运算符重载和迭代器模式

有些时候,我们会定义某些容器类,这些类中包含了大量相同类型的对象。如果你想给这个容器类的对象直接提供迭代的方法,如hasNextnextfirst等,那么就可以自定义一个迭代器。然而通常情况下,我们不需要自己再实现一个迭代器,因为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.

在这个例子中,我们声明了GroupLeaderPresidentCollege三个类来代表学生会部长、分部长、会长及学院,它们都实现了ApplyHandler接口。接口包含了一个可空的后继者对象successor,以及对申请事件的处理方法handleEvent。当我们把一个申请经费的事件传递给GroupLeader对象进行处理时,它会根据具体的经费金额来判断是否将申请转交给successor对象,也就是President类来处理。以此类推,最终形成了一个责任链的机制。

现在我们再来重新思考下责任链的机理,你会发现整个链条的每个处理环节都有对其输入参数的校验标准,在上述例子中主要是对申请经费事件的金额有要求。当输入参数处于某个责任链环节的有效接收范围之内,该环节才能对其做出正常的处理操作。在编程语言中,我们有一个专门的术语来描述这种情况,这就是“偏函数” 。

实现偏函数类型:PartialFunction

什么是偏函数?

偏函数是个数学中的概念,指的是定义域X中可能存在某些值在值域Y中没有对应的值。

为了方便理解,我们可以把偏函数与普通函数进行比较。在一个普通函数中,我们可以给指定类型的参数传入任意该类型的值,比如(Int) -> Unit,可以接收任何Int值。而在一个偏函数中,输入类型的参数值不一定被接收,比如:

fun mustGreaterThan5(x: Int): Boolean {if (x > 5) return trueelse throw Exception("x must be greator than 5")
}mustGreaterThan5(6)
>>> truemustGreaterThan5(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 collegeapplyChain(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(): Intfun getDesc(): Stringfun getProdDate(): String
}
class MacBookPro: MacBook {override fun getCost() = 10000override fun getDesc() = "Macbook Pro"override fun getProdDate() = "Late 2011"
}
// 装饰类
class MacBookUpgrade(val macBook: MacBook) : MacBook by macBook {override fun getCost() = macBook.getCost() + 219override 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

因此,我们只需通过覆写的语法来重写需要变更的costgetDesc方法。由于生产年份是不会改变的,所以不需重写,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 语法
扩展函数
结构型模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/232825.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++初阶-反向迭代器的模拟实现

反向迭代器的模拟实现 一、反向迭代器的定义二、反向迭代器的功能2.1 operator2.2 operator- -2.3 operator*2.4 operator&#xff01; 三、list反向迭代器模拟实现完整代码3.1 list.h3.2 iterator.h3.3 test.cpp 一、反向迭代器的定义 我们反向迭代器的思路是复用正向迭代器的…

STM32F407-14.3.12-01使用断路功能

使用断路功能 使用断路功能时&#xff0c;根据其它控制位&#xff08;TIMx_BDTR 寄存器中的 MOE⑨、OSSI⑪ 和 OSSR⑩ 位以及 TIMx_CR2 寄存器中的 OISx⑰ 和 OISxN⑱ 位&#xff09;修改输出使能信号和无效电平。任何情况下&#xff0c;OCx③ 和 OCxN④ 输出都不能同时置为有效…

LD2450-24G人体移动跟踪轨迹雷达模块

文章目录 前言一、LD2450简介特点引脚定义 二、使用步骤上位机使用方法通信协议协议格式数据输出协议 雷达命令配置方式串口解析示例 前言 运动目标跟踪是指在区域内实时跟踪运动目标所在的位置&#xff0c;实现对区域内运动目标测距、测角和测速。LD2450是海凌科24G毫米波雷达…

WFrest 库:快速、高效的基于workflow的C++异步 Web 框架

在这篇博客中&#xff0c;我将介绍 WFrest 库&#xff0c;一个基于 C Workflow 企业级程序引擎的异步 Web 框架。WFrest 库能够帮助开发者快速搭建 HTTP 服务器&#xff0c;实现高效的 Web 应用开发。 一、WFrest 库的背景 WFrest 库是一个由[作者/团队]开发的开源项目&#…

基于paddlepaddle的FPS最远点采样

什么是FPS最远点采样&#xff1f; 最远点采样&#xff08;Farthest Point Sampling&#xff0c;FPS&#xff09;是一种常用的采样算法&#xff0c;主要用于点云数据&#xff08;如激光雷达点云数据、分子坐标等&#xff09;的采样。 为了方便解释&#xff0c;定义一下待采样点…

深入解析线程安全的Hashtable实现

目录 引言 1. Hashtable简介 2. Hashtable线程安全实现原理 2.1. 锁机制 2.2. 分段锁 2.3. CAS操作 3. 线程安全策略 3.1. 同步方法 3.2. 分段锁优化 3.3. 乐观锁和CAS 4. 性能优化 4.1. 负载均衡 4.2. 惰性加载 5. 注意事项 5.1. 死锁和性能问题 5.2. 内存开销…

嵌入式软件测试(黑盒测试)---三年嵌入式软件测试的理解

文章内容为本人这三年来在嵌入式软件测试&#xff08;黑盒&#xff09;上的一些积累吧&#xff0c;说起来也挺快的&#xff0c;毕业三年的时间就这样过去了&#xff0c;在两家公司工作过&#xff08;现在这家是第二家&#xff09;&#xff0c;这几年的测试项目基本都是围绕着嵌…

深入探索Zookeeper的ZAB协议:分布式系统的核心解析

引言 自我进入软件开发领域以来&#xff0c;我一直对分布式系统充满着浓厚的兴趣。在这个领域中&#xff0c;Zookeeper无疑是一个备受关注的重要组件。作为一名资深的Java工程师&#xff0c;我有幸深入探索过Zookeeper的许多方面&#xff0c;其中最让我着迷的部分莫过于其核心机…

第十三章 枚举类型和泛型

枚举类型可以取代以往的常用的定义方式&#xff0c;即将常量封装在类或者接口中&#xff0c;此外它还提供了安全检查功能。枚举类型本质上还剋以类的形式存在。泛型的出现不仅可以让程序员少写一些代码&#xff0c;更重要的是它可以解决类型安全问题。泛型提供了编译时的安全检…

redolog有什么用,是怎么工作的

redolog其实就是想干一件事&#xff1a;当一个事务commit了&#xff0c;那肯定是在内存中改了&#xff0c;但是在磁盘里未必。可能刚提交事务就宕机了&#xff0c;还没来得及写磁盘&#xff08;并且也不会立刻写的&#xff0c;会隔一段时间才刷&#xff09;。redolog就是要保证…

关于设计师的自我评价(合集)

设计师的自我评价篇一 本人接受过正规的美术教育&#xff0c;具有较好的美术功底及艺术素养&#xff0c;能够根据公司的需要进行设计制作&#xff0c;熟练掌握多种电脑制作软件&#xff0c;能够高效率地完成工作。本人性格开朗、思维活跃、极富创造力&#xff0c;易于沟通&…

软件测试必会:cookie、session和token的区别

今天就来说说session、cookie、token这三者之间的关系&#xff01;最近这仨玩意搞得头有点大&#x1f923; 01 为什么会有它们三个 我们都知道 HTTP 协议是无状态的&#xff0c;所谓的无状态就是客户端每次想要与服务端通信&#xff0c;都必须重新与服务端链接&#xff0c;意…

奇怪的资源分享

说明一下 最近找了宝宝巴士的资源&#xff0c;下了半天结果发现要解压密码&#xff0c;还甩出付费二维码&#xff0c;气坏我了。要我付钱怎么可能&#xff0c;打死我都不会付钱的。于是我找了另外的资源。这里分享一下这个资源。 宝宝巴士视频版 链接 宝宝巴士压缩版 链接 …

Selenium Wire - 扩展 Selenium 能够检查浏览器发出的请求和响应

使用 Selenium 进行自动化操作时&#xff0c;会存在很多的特殊场景&#xff0c;比如会修改请求参数、响应参数等。 本篇将介绍一款 Selenium 的扩展&#xff0c;即能够检查浏览器发出的请求和响应 - Selenium Wire。 简介 Selenium Wire 扩展了 Selenium 的 Python 绑定&…

24--泛型与Collections工具类

1、泛型 1.1 泛型概述 在前面学习集合时&#xff0c;我们都知道集合中是可以存放任意对象的&#xff0c;只要把对象存储集合后&#xff0c;那么这时他们都会被提升成Object类型。当我们在取出每一个对象&#xff0c;并且进行相应的操作&#xff0c;这时必须采用类型转换。 p…

聊聊15年进入中专计算机的道路

仍记得笔者是参加2015年杭州市中考&#xff0c;优质高中的录取分数线是454分&#xff0c;而我439分&#xff0c;父亲想让我读个民办普通高中。而我将这个志愿排在了计算机专业之后。我成功进入了一所计算机中专。命运之轮就这样悄悄转动。 1、为什么当初选择计算机行业 中考没…

Halcon深度学习相关术语介绍

1、深度学习术语表一 序号 术语 解释 1 Adam Adam (adaptive moment estimation)是一种基于一阶梯度的随机目标函数优化算法&#xff0c;用于计算单独的自适应学习率。在深度学习方法中&#xff0c;该算法可用于最小化损失函数。 2 anchor 它们作为固定的参考边界框&am…

C语言第五十四弹---模拟使用strstr函数

使用C语言模拟使用strstr函数 定义&#xff1a;strstr 是一个 C 标准库函数&#xff0c;用于在一个字符串中查找另一个字符串的第一次出现位置。strstr 函数的声明如下&#xff1a; char* strstr(const char* haystack, const char* needle);它接受两个参数&#xff1a;haysta…

Sectigo DV多域名证书能保护几个域名

多域名SSL证书不限制受保护的域名的类型&#xff0c;可以时多个主域名或者子域名&#xff0c;多域名SSL证书都可以同时保护&#xff0c;比较灵活。但是&#xff0c;多域名https证书并不是免费无限制保护域名数量&#xff0c;一把的多域名SSL证书默认保护3-5个域名记录&#xff…

云原生之深入解析强大的镜像构建工具Earthly

一、Earthly 简介 Earthly 是一个更加高级的 Docker 镜像构建工具&#xff0c;Earthly 通过自己定义的 Earthfile 来代替传统的 Dockerfile 完成镜像构建&#xff1b;Earthfile 就如同 Earthly 官方所描述: Makefile Dockerfile Earthfile在使用 Earthly 进行构建镜像时目前…