[译]Kotlin珍品 5 -多继承以及一些委托实现的有用的小技巧

2,672 阅读7分钟

翻译说明: 翻译水平有限,文章内可能会出现不准确甚至错误的理解,请多多包涵!欢迎批评和指正.每篇文章会结合自己的理解和例子,希望对大家学习Kotlin起到一点帮助.

原文地址: [Kotlin Pearls 5] Multiple Inheritance

原文作者: Uberto Barbini

前言

今天让我们谈谈我之前最不喜欢的Kotlin特性:by关键字.不喜欢它,因为我当时觉得仅仅在极少的使用场景下才会用到它,并且它是复杂和非必要的.
ps:不知道你们看到这句话是不是感同身受.反正我看到原作者写这句话的时候内心是觉得自己找到知音了,结果后面就被作者打脸了.

但是我在后面的探索中有了完全相反的理解.by关键字能和kotlin其他很多特性结合使用并且越深入的理解它就越发现它有用了.
ps:脸疼不疼?不过好歹有了看下去的欲望了

首先:by关键字在Kotlin中有两个不同的使用场景:

  • 接口实现委托.它让我们可以用现用并不同的接口实现类去实现这个接口.

    interface Base {//接口
        fun print()
    }
    
    class BaseImpl(val x: Int) : Base {//委托对象
        override fun print() { print(x) }
    }
    
    class Derived(b: Base) : Base by b//委托者
    
    fun main() {
        val b = BaseImpl(10)
        Derived(b).print()
    }
    

    这里打印的结果是10

  • 属性委托.它可以让我们给所有属性定义一个公用的自定义行为,比如从一个map读取值

这篇稳重仅仅对第一种使用场景展开.我将会在后期的文章中专门讲属性委托,其中会包涵些让人眼前一亮的骚操作.


先看接口实现委托

首先让我们看一个最直接的例子.

记住!只有接口可以被委托.被open abstract 修饰的类并不能被委托出去


所以我们下面定义一个接口和两个接口实现类,一个是我们后面真正要用的,一个是测试目的使用.
    interface ApiClient {
        fun callApi(request: String): String
    }
    //实现类作为测试目的的单例
    object ClientMock: ApiClient {
        override fun callApi(request: String): String = "42"
    }
    //实现类作为真正要用的类
    class HttpApiClient: ApiClient {
        override fun callApi(request: String): String {
            //... 做一些事情
            return "The Answer to $request is 42"
        }
    }

目前为止都是OK吧.好,那我们现在假设有这样一个需求.我们需要另外的类去实现APiClient并且添加新的方法.

这是一个用到by操作符的完美案例.它明确表示了我们想用一个已有的实例去实现一个接口.它可以直接用一个随即创建的实例(HttpApiClient)或者一个object(ClientMock):

    class DoSomething: ApiClient by HttpApiClient(){
        //需要添加的另外方法
    }
    class DoSomethingTest(): ApiClient by ClientMock {
        //需要添加的另外方法
    }

你们可以测试一下,这样写是可以的.不过这样写到底发生了什么呢?

我们可以通过审查DoSomethingClass的Kotlin字节码去探个究竟(Tools|Kotlin|Show Kotlin Bytecode).

Java字节码看不懂没关系,我们可以反编译成我们熟悉的Java代码.

点击Decompile去看Java代码

现在跟我们Kotlin同等的Java就是这个样子了:

    public final class DoSomething implements ApiClient {
       // $FF: synthetic field
       private final HttpApiClient $$delegate_0 = new HttpApiClient();
       @NotNull
       public String callApi(@NotNull String request) {
          Intrinsics.checkParameterIsNotNull(request, "request");
          return this.$$delegate_0.callApi(request);
       }
    }

现在不难发现,编译器把委托的实现者认为是委托者的一个隐藏字段,并且会继续实现委托对象实现的对应接口的方法.

假设你们不知道这个点,在Kotlin中变量,字段和方法不能用美元符号开头,所以在Kotlin代码中不能重写或者轻易的访问?delegate_0字段.

到目前为止的例子中,我们创建新的委托对象,或者引用一个object单例来演示委托实现.其实还有一个更有用的模式就是把委托对象当作构造参数传入,如下:

    class DoSomethingDelegated(client: ApiClient): ApiClient by client {
        //other stuff needed to do something
    }

这种模式十分方便,因为我们可以更灵活的重用我们的类,我们只需要在构造方法里面传入不同的对象就行.


了解了接口实现委托后,那么委托着中覆写的场景会如何呢?

覆写一个或多个接口的方法就跟我们想的一样.你定义一个新的方法去覆盖上一个方法那么你的类将会忽略被覆写的上一个的方法.

    class DoSomethingOverriden( client: ApiClient): ApiClient by client {
        override fun callApi(request: String): String {
            return "覆写后做点不一样的结果: $request"
        }
    }

"牢记一点!委托实现和通常的覆写以及多态是有不同的!"

1.委托者并不清楚委托对象,所以委托者不能调用委托对象的方法并覆写.
2.委托对象的成员只能访问其自身对接口成员实现
3.在覆写方法的内部,你不能调用`super`,因为委托方式的接口实现类(委托者)并没有继承任何类.
    //接口
    interface Base {
        val message: String
        fun print()
    }
    //委托对象
    class BaseImpl(val x: Int) : Base {
        override val message = "BaseImpl: x = $x"
        override fun print() { println(message) }
        open fun print1(){
            
        }
    }
    
    //委托方式的接口实现类(委托者)
    class Derived(b: Base) : Base by b {
        //覆写message属性
        // 在 b 的 `print` 实现中不会访问到这个属性
        override val message = "Message of Derived"
        override fun print1(){//报错 因为1.委托者并不清楚委托对象,所以委托者不能调用委托对象的方法并覆写
            println("我可以覆写委托对象的方法么")
        }
        
        override fun print(){
            super.print()//报错 3.在覆写方法的内部,你不能调用`super`,因为委托方式的接口实现类并没有继承任何类.
            //....
        }
    }
    
    fun main() {
        val b = BaseImpl(10)//构造委托对象
        val derived = Derived(b)//构造委托方式接口实现类
        derived.print()//2.委托对象的成员只能访问其自身对接口成员的实现
        println(derived.message)//委托者当然可以访问自己的成员
    }
    注释掉引起报错的代码后打印结果:BaseImpl: x = 10
                             Message of Derived

据我所知,目前还没有直接的方式可以在委托者里面的覆写方法里面调用委托对象的方法,不过我们仍然可以用另外的方式实现.我们可以把委托对象作为私有属性并且直接调用委托对象的方法.

    //接口
    interface Base {
        val message: String
        fun print()
    }
    //委托对象
    class BaseImpl(val x: Int) : Base {
        override val message = "BaseImpl: x = $x"
        override fun print() { println(message) }
    }
    
    //委托方式的接口实现类(委托者)
    class Derived(private val b: Base) : Base by b {
        //覆写message属性
        // 在 b 的 `print` 实现中不会访问到这个属性
        override val message = "Message of Derived"
        override fun print(){
            b.print()
            println("覆写后的逻辑")
        }
    }
    
    fun main() {
        val b = BaseImpl(10)//构造委托对象
        val derived = Derived(b)//构造委托方式接口实现类
        derived.print()
        println(derived.message)
    }
    打印结果:BaseImpl: x = 10//委托者在覆写方法内部调用了委托对象的方法
            覆写后的逻辑//委托者的覆写方法成功执行
            Message of Derived

我们现在来看一下如何用委托来实现多继承的

还是举个栗子哈.就说我们有两个接口,分别是Printer(打印机)和Scanner(扫描仪),并分别各有一个实现类.现在我们想创建一个PrinterScanner Class把两个结合在一起.

    data class Image (val name: String)//数据类图片
    
    interface Printer{//打印机接口
        fun turnOn()
        fun isOn(): Boolean
        fun printCopy(image: Image)
    }
    interface Scanner{//扫描仪接口
        fun turnOn()
        fun isOn(): Boolean
        fun scan(name: String): Image
    }
    object laserPrinter: Printer {//打印机接口实现单例-激光打印机
        var working = false
        override fun isOn(): Boolean = working
        override fun turnOn() {
            working = true
        }
        override fun printCopy(image: Image) {
            println("printed ${image.name}")
        }
    }
    object fastScanner: Scanner {//扫描仪接口实现单例-快速扫描仪
        var working = false
        override fun isOn(): Boolean = working
        override fun turnOn() {
            working = true
        }
        override fun scan(name: String): Image = Image(name)
    }
    
    //结合文章前面讲解的,我们只用把ScannerAndPrinter Class当作委托者,通过kotlin的by关键字把两个接口实现类变成委托对象,
    然后通过构造参数传入给委托者访问
    class ScannerAndPrinter(scanner: Scanner, printer: Printer): Scanner by scanner, Printer by printer {
        override fun isOn(): Boolean = (this as Scanner).isOn()//两个单例的方法一样,需要显示的覆写
        override fun turnOn() = (this as Scanner).turnOn()//两个单例的方法一样,需要显示的覆写
        
        fun scanAndPrint(imageName: String) = printCopy(scan(imageName))
    }

好了!这就完全按照我们的要求实现了通过by实现了多继承了.大家也可以自己敲一下在demo里面跑一下帮助理解加深印象.

后话

ps:下面这段话建议大家自行查找复合复用的中文解释,鄙人水平有限翻译起来不准确.以免误导他人

在面相对象编程中复合复用原则:类应该通过它们的实例或者其他实现了预期功能的类去实现多态行为和代码复用,而不是继承基类或者父类.这个原则是经常在面向对象编程中提到的,比如在有些影响深远的书籍(Design Patterns)中提到.

在面向对象编程中并且甚至在函数式编程中,以上的原则都是正确的.除此之外,在kotlin类中默认是不加open修饰符的,所以继承并不被采纳.

Kotlin的by操作符是简单易用并且也许容易被我们低估和忽视.相比于之前的一些复用方式,它让代理变得"看不见"并且会促进你们在一个更好的设计上面去思考和学习.

这里还有最后一个例子.一个关于从数据库读取区域内对象的迷你库.这个库没有通过继承而是利用委托把业务和逻辑分离的很好.边看例子中注释边去理解(建议从下往上看).

    typealias Row = Map<String, Any> //a db row is expressed as a Map field->value
    interface DbConnection {//数据库接口
        //abstraction on the db connection/transaction etc
        fun executeQuery(sql: String): List<Row>
    }
    interface SqlRunner<out T> {//执行sql语句接口 
                                //out is because we can return T or subtypes
                                //(kotlin泛型知识,这里不做过多解释,先理解成接口内
                                要返回这个T类型才在前面加了out就行,后期会专门为泛型写一个专栏)
        // interface to execute sql statement and return domain objects
        fun builder(row: Row): T
        fun executeQuery(sql: String): List<Row>
    }
    //declared outside SqlRunner interface to avoid overriding and multiple implementations
    //在SqlRunner接口外申明可以避免覆写和多实现的麻烦.这里用泛型拓展函数来申明了两个方法
    fun <T> SqlRunner<T>.fetchSingle(sql: String): T = builder( executeQuery(sql).first() )
    fun <T> SqlRunner<T>.fetchMulti(sql: String): List<T> = executeQuery(sql).map { builder(it) }
    //a real example would be: class JdbcDbConnection(dbConnString: String): DbConnection
    class FakeDbConnection(): DbConnection{//(伪代码)一个数据库接口的实现类
        //trivial example but in reality manage connection pool, transactions etc and translate from JDBC
        override fun executeQuery(sql: String): List<Row> {
            return listOf( mapOf("id" to 5, "name" to "Joe") )
        }
    }
    //now how to use all this for retrieve Users
    interface UserPersistence{//持久化用户接口
        //interface needed by the domain
        fun fetchUser(userId: Int): User
        fun fetchAll(): List<User>
    }
    class UserPersistenceBySql(dbConn: DbConnection): UserPersistence, SqlRunner<User> by UserSql(dbConn) {
        //translate domain in sql statements but still abstract from db connection,transactions etc.
        //实现了SqlRunner<T>接口,
        override fun fetchUser(userId: Int): User {
            return fetchSingle("select * from users where id = $userId")
        }
        override fun fetchAll(): List<User> {
            return fetchMulti("select * from users")
        }
    }
    class UserSql( dbConn: DbConnection) : SqlRunner<User>, DbConnection by dbConn {
        // implementation for User
        override fun builder(row: Row): User = User(
            id = row["id"] as Int,
            name = row["name"] as String)
        // note that we don't need to implement executeQuery because is already in DbConnection
    }
    
    //创建单例的时候构造了FakeDbConnection,接着构造了UserPersistenceBySql,接着构造了UserSql
    object UserRepository: UserPersistence by UserPersistenceBySql(FakeDbConnection())
    //example of use
    fun main() {
        val joe = UserRepository.fetchUser(5)//委托者UserRepository调用委托对象UserPersistenceBySql的方法
        println("fetched user $joe")
    }