[譯]Kotlin珍品 5 -多繼承以及一些委託實現的有用的小技巧

翻譯說明: 翻譯水平有限,文章內可能會出現不許確甚至錯誤的理解,請多多包涵!歡迎批評和指正.每篇文章會結合本身的理解和例子,但願對你們學習Kotlin起到一點幫助.android

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

原文做者: Uberto Barbini數據庫

前言

今天讓咱們談談我以前最不喜歡的Kotlin特性:by關鍵字.不喜歡它,由於我當時以爲僅僅在極少的使用場景下才會用到它,而且它是複雜和非必要的.
ps:不知道大家看到這句話是否是感同身受.反正我看到原做者寫這句話的時候心裏是以爲本身找到知音了,結果後面就被做者打臉了.express

可是我在後面的探索中有了徹底相反的理解.by關鍵字能和kotlin其餘不少特性結合使用而且越深刻的理解它就愈加現它有用了.
ps:臉疼不疼?不過好歹有了看下去的慾望了編程

首先:by關鍵字在Kotlin中有兩個不一樣的使用場景:bash

  • 接口實現委託.它讓咱們能夠用現用並不一樣的接口實現類去實現這個接口.dom

    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()
    }
    複製代碼

    這裏打印的結果是10ide

  • 屬性委託.它可讓咱們給全部屬性定義一個公用的自定義行爲,好比從一個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") } 複製代碼
相關文章
相關標籤/搜索