Kotlin设计模式之委托模式

1,087 阅读9分钟

Kotlin提供了两个本机功能来实现委托模式。第一个是接口委托(例如策略模式)。另一种是属性委托,它专注于类成员/属性(例如延迟加载、observable等)。它们共同提供了一组丰富而简洁的功能。通过本博客,您将了解在什么情况下使用此模式。这些示例向您展示优点,但也会展示已知的问题。

1、组合由于继承

众所周知,在面向对象语言中,继承是关键特性之一。这意味着您可以将一些功能和抽象放入基类中。其他类可以从基类继承因此获得继承的功能。这称为“Is-a”关系。作为教科书的示例,我们可以想象Shape类(基类)和rectangle、circle等为派生类。

过去,这种建模方式被滥用,导致设计不佳。例如,长继承链用于对象添加增量功能。作为一个过于夸张的示例,请考虑一下类设置。我们想要创建“Animal”并实现它们的移动和进食方式。一种简单的方法是如下图所示实现它。很明显,这不会扩展。

image.png

另一方面,下图更倾向于组合而不是继承。该类Animal实现接口IMovingIEating。然而它没有实际的实现代码,它将调用委托给Walking和具体实现MeatEating。正如我们所看到的,这种设计更加灵活,因为可以实现MovingEating而不用修改Animal类。

image.png

委派继承是实现SOLID设计模式中的开放-关闭原则的一种方式。

2、接口委托

如何在Kotlin中创建委托? Kotlin提供了原生功能来实现委托模式,而无需编写任何样板代码。这仅适用于接口(即抽象类)。接口实现IWalking如下代码所示。

// 标准委托
interface IMoving {
    fun move()
}

class Walking: IMoving {
    override fun move() {
        println("Walking")
    }
}

class Animal(private val movable: IMoving): IMoving {
    override fun move() {
        movable.move()
    }
}

fim main() {
    var movable = Walking()
    var animal = Animal(movable)
    animal.move()
}
// 内置委托
interface IMoving {
    fun move()
}

class Walking: IMoving {
    override fun move() {
        println("Walking")
    }
}

class Animal(movable: IMoving): IMoving by movable {

}

fun main() {
    var walking = Walking()
    var animal = Animal(walking)
    animal.move()
}

正如我们所看到的,第一部分代码需要实现接口方法。在正确的版本中,编译器会为您执行此操作。当界面具有多种功能时,效果会更加显著。在派生类类型的超类型的列表中使用by-子句表示可移动的信息将在IMoving对象的内部存储,编译器将生成所有将转发到IMoving的接口方法。

2.1、覆盖接口成员

如果需要重写接口的特定成员函数,只需在派生类中编写该函数并添加关键字override即可。编译器将使用重写方法的新规范。在下面的实例中,我们创建了该方法的新版本move

// By Delegate的重写功能
class Animal(movable: IMoving): IMoving by movable {
    override fun move() {
        println("Something else")
    }
}

fun main() {
    var walking = Walking()
    var animal = Animal(walking)
    animal.move()
}

2.2、多个接口/继承

为了完成上面的例子,我们必须实现所有接口并将所有函数调用委托给成员变量。我们可以通过将与上一节相同的方法应用于所有继承的接口来做到这一点。

// 介个接口委托
interface IMoving {
    fun move()
}

class Walking: IMoving {
    override fun move() {
        println("Walking")
    }
}

interface IEating {
    fun eat()
}

class MeatEater: IEating {
    override fun eat() {
        println("Eat meat")
    }
}

class Animal(movable: IMoving, eating: IEating): IMoving by movable, IEading by eating {
    
}

fun main() {
    var walking = Walking()
    var eating = MeatEater()
    var animal = Animal(walking, eating)
    animal.move()
    animal.eat()
}

2.2.1、相同的函数签名

如果您需要实现多个声明了相同方法的接口,则会出现特殊情况。在这种情况下,委托是不明确的,编译器会给出一个错误,例如“Class 'xxx' must override public open fun doSomething(): Unit defined in Animal because it inherits many implementation of it”。您需要显示实现(或重写)此函数并手动委托它。

image.png

2.3、在运行时替换委托

通常需要在运行时更改委托。这通常用在策略或状态模式中。(目前)Kotlin不支持在使用by关键字进行注入时更改委托。

以下代码具有误导性,因为它打印以下内容:

Walking
Walking
Running@xxx
//在运行时更改委托
interface IMoving {
    fun move()
}

class Running: IMoving {
    override fun move() {
        println("Running")
    }
}

class Walking: IMoving {
    override fun move() {
        println("Walking")
    }
}

class Animal(var movable: IMoving): IMoving by movable {
    
}

fun main() {
    var walking = Walking()
    var animal = Animal(walking)
    animal.move()
    
    var running = Running()
    animal.movable = running
    animal.move()
    
    println(animal.movable)
}

3、属性委托

什么是属性委托? Kotlin提供了一些很好的功能来说实现委托属性。它向类属性添加了一些常见功能。您可以在库中创建一些属性(例如Lazy)并使用此功能包装您的类成员。

要了解其工作原理,您需要了解每个成员变量在幕后提供getValuesetValue。属性委托不需要实现接口,但需要为每种类型提供这些函数。正如您所看到的,属性委托是通过对象/函数。

在下面的示例,我们提供了一个委托,该委托在每次访问时都会打印消息。

3.1、只读委托

任何属性委托必须至少为所委托的值类型实现以下函数。这type T取决于被包装的值类型。

// Read requited delegate function
operator fun getValue(example: Any, property: KProperty<<*>): T {
    return // value of T
}

作为示例,我们将使用与Kotlin参考页面略有不同的代码。我们的委托只能用于字符串类型,并且仅返回空字符串。这段代码毫无用处,但它显示了只读属性的用法。

// Example Read only delegate
class Example {
    val p: String by Delegate()
}

class Delegate {
    operator fun getValue(example: Any, property: KProperty<*>): String {
        return ""
    }
}

3.2、读/写委托

为了使用上述委托进行读写实现,我们还必须实现以下函数。请注意,我们有一个字符串值类型。因此我们写了s: String。您需要将其调整为您想要使用的类型。

// Write required delegate function

operator fun setValue(example: Any, property: KProperty<*>, s: String) {

}

我们上面的例子现在可以实现所需的功能了。我们将引入一个类变量cached来保存实际值。

// Example Read/Write delegate
class Example {
    var p: String by Delegate()
}

class Delegate {
    var cached = ""
    
    operator fun getValue(example: Any, property: KProperty<*>): String {
        return cached
    }
    
    operator fun setValue(example: Any, property: KProperty<*>, s: String) {
        cached = s
    }
}

3.3、实现接口作为扩展

另一种方法是将getValue()setValue()函数实现为类的扩展函数Delegate。如果该类不在您的源代码管理中,这很有用。

// Delegated property as extension function
class Example {
    val string: String by Delegate()
}

class Delegate {

}

operator fun Delegate.getValue(example: Any, propety: KProperty<*>): String {
    return ""
}

3.4、通用委托定义

明显的缺点是此代码仅适用于字符串类型的对象。如果我们想将它用于其他类型,我们必须使用泛型类。我们将向您展示如何更改上述示例以使其兼容所有类型。

// Generic class for Delegate
class Example {
    var string: String by Delegate("hello")
    var int: Int by Delegate(3)
}

class Delegate<T>(var cached: T) {
    operator fun getValue(example: Any, property: KProperty<*>): T {
        return cached
    }
    
    operator fun setValue(example: Any, property: KProperty<*>, s: T) {
        cached = s
    }
} 

3.5、匿名对象委托

Kotlin还提供了一种创建匿名对象委托而无需创建新类的方法。这是由于接口ReadOnlyPropertyReadWriteProperty标准库而起作用的。这些接口将为ReadOnlyProperty提供getValue函数。ReadWriteProperty通过添加setValue()函数,扩展了ReadOnlyProperty

在上面的示例,委托属性是通过函数调用创建为匿名对象的。

// Anonymous objects delegates
class Example {
    var string: String by delegate()
}

fun delegate(): ReadWriteProperty<Any?, String> = object: ReadWriteProperty<any?, String> {
    var curValue = ""
    override fun getValue<thisRef: Any?, property: KProperty<*>): String = curValue
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        curValue = value
    }
}

3.6、委托给另一个属性

从一个属性委托给另一个属性可能是提供向后兼容性的一个非常有用的技巧。想象一下,在类的1.0版本中,您有一些公共成员函数。但在他们的一生中,名字发生了变化。为了不破坏客户端,我们可以将旧的成员函数标记为deprecated(通过注解)并将其调用转发到新的实现。

// Delegate to other member
class Example {
    var newName: String = ""
    
    @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
    var oldName: String by this::newName
}

fun main() {
    val example = Example()
    example.oldName = "hello"
    
    println(example.newName)
}

我们的经验,在某些情况下,此代码无法编译,并出现错误代码:

Type 'KMutableProperty0' has no method 'getValue(MyClass, KProperty<*>)' and thus it cannot serve as a delegate.

知道有一个开放的错误(LINK),我们希望它能尽快修复!

4、委托示例和用例

Kotlin为常见问题提供了一些预先实现的解决方案。请访问KotlinLang页面查看完整列表。对于当前的实现,我们将向您展示几个示例。

4.1、通过observable

可以通过使用可观察委托来获得基本功能。当值改变时它提供回调。这样做得好处是您可以访问新的和以前的值。要使用可观察委托,您必须指定初始值。

// by observable
class Book {
    var content: Strign by obserable("") { property, oldValue, newValue ->
        println("Book changed")
    }
}

fun main() {
    val book = Book()
    book.content = "New content"
}

4.2、通过vetoable

vetoable有点类似观察者委托。然而,回调函数可用于撤销修改。请注意,回调必须返回布尔值。如果成功,则为true;如果失败,则为false。在我们的书籍示例中,我们检查书籍的信内容是否为null或为空。在这种情况下,它被认为是错误的,我们想要vetoable该更改。

// vetoable
class Book {
    var content: String by vetoable("") { property, oldValue, newValue ->
        !newValue.isNullOrEmpty()
    }
}

fun main() {
    val book = Book()
    book.content = "New content"
    println(book.content)
    
    book.content = ""
    println(book.content)
}

4.3、通过Lazy

Kotlin提供了变量延迟计算的现有实现。一个常见的用例是需要花费大量时间来初始化但在开始时不会直接使用的成员变脸。例如,考虑一个Book类,它获取一个表示整个文本的字符串。书籍类拥有另一个对书籍进行昂贵的后处理的类。开发人员决定让这个示例变得Lazy,因为它并不是总被调用。

Lazy属性附加到analyzer类。运行代码时,它首先会打印出书籍已创建,然后analyzer成员变量已实例化。

// by lazy
class Book(private val rawText: String) {
    private val analyser: Analyser by lazy { Analyser() }
    
    fun analyse() {
        analyser.doSomething()
    }
}

class Analyser {
    init {
        println("Init analyser class")
    }
    
    fun doSomething() {
        print("DoSomething")
    }
}

fun main() {
    val rawText = "MyBook"
    val book = Book(rawText)
    println("Book is created")
    book.analyse()
}

4.4、通过Not Null

返回具有非null值的读/写属性的属性委托,该属性不是在对象构造期间而是在稍后的时间初始化。由于它是在稍后时间点创建的,因此与延迟初始化有关。然而,一个很大的缺点是没有本地方法可以知道该值是否已初始化。它需要一些样板代码才能保存。我们的十本里将类似于以下代码。请注意,Lazy要好得多。

// By Delegates.NotNull
class Analyser {
    init {
        println("Init analyser class")
    }
    
    fun doSomething() {
        println("DoSomething")
    }
}

class Book(private val rawText: String) {
    var analyser: Analyser by Delegates.notNull()
    
    fun analyse() {
        analyser = Analyser()
        analyser.doSomething()
    }
}

fun main() {
    val rawText = "MyBook"
    val book = Book(rawText)
    println("Book is created")
    book.analyse()
}

4.5、Logging

您可以使用委托对应用程序日志记录库的访问。在带有惰性的函数中,这是为每个需要的类提供对记录器的访问的最惯用的方法。

// Idiomatic logging access
fun <R: Any> R.logger(): Lazy<Logger> {
    return lazy {
        Logger.getLogger(unwrapCompainionClass(this.javaClass).name)
    }
}

class Something {
    val LOG by logger()
    
    fun foo() {
        LOG.info("Hello from Something")
    }
}