Translation note: the translation level is limited, the article may appear inaccurate or even wrong understanding, please forgive me! Criticism and correction are welcome. Each article will combine its own understanding and examples, hoping to help you learn Kotlin.

[Kotlin Pearls 5] Multiple Inheritance

Uberto Barbini

preface

Today let’s talk about one of my least favorite Kotlin features: the by keyword. I didn’t like it because I thought it was only used in rare situations, and it was complicated and unnecessary. Anyway, WHEN I saw the original author write this sentence, I felt that I had found a friend, and then I was slapped in the face by the author.

However, I came to understand the exact opposite in my later explorations. The by keyword can be used in combination with many of Kotlin’s other features and the more I understand it, the more I find it useful. But at least I have the desire to watch

First: The by keyword is used in two different scenarios in Kotlin:

  • Interface implementation delegation. this allows us to implement the interface using existing and different interface implementation classes.

    Interface Base {// interface funprint} class BaseImpl(val x: Int) : Base {// Delegate object override funprint() { print(x)}} class Derived(b: Base) : Base by b// delegate funmain() {
        val b = BaseImpl(10)
        Derived(b).print()
    }
    Copy the code

    The result printed here is 10

  • Property delegate, which allows us to define a common custom behavior for all properties, such as reading values from a map

This article focuses only on the first use scenario. I’ll focus on property delegates in a later article, which will include some pretty impressive manipulation.


First look at the interface implementation delegate

First, let’s look at the most straightforward example.

Remember! Only interfaces can be delegated. Classes decorated by Open Abstract cannot be delegated


Interface ApiClient {funCallAPI (Request: String): String} ApiClient { override fun callApi(request: String): String ="42"Override fun callApi(request: String): String {//... Do somethingreturn "The Answer to $request is 42"}}Copy the code

So far so OK. Ok, so let’s say we have a requirement. We need another class to implement APiClient and add new methods.

This is a perfect example of using the BY operator. It explicitly states that we want to implement an interface using an existing instance. It can be created directly with an instance (HttpApiClient) or an object(ClientMock):

    class DoSomething: ApiClient by HttpApiClientClass DoSomethingTest(): ApiClient by ClientMock {Copy the code

And you can test it out. It works. But what happens when you write this?

We can review the Kotlin DoSomethingClass Bytecode to know (the Tools | Kotlin | Show Kotlin the Bytecode).

It doesn’t matter if you can’t read Java bytecode, we can decompile it into Java code that we are familiar with.

Now our Kotlin equivalent of Java looks like this:

    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); }}Copy the code

Now it is not hard to see that the compiler considers the implementer of the delegate to be a hidden field of the delegate and continues to implement the methods of the corresponding interface implemented by the delegate object.

Assuming you don’t know this point, in Kotlin variables, fields and methods can’t start with a dollar sign, so they can’t be overridden or easily accessed in Kotlin code, right? Delegate_0 fields.

In the examples so far, we have created a new delegate object or referenced an Object singleton to demonstrate delegate implementation. A more useful pattern is to pass in a delegate object as a construct parameter, as follows:

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

This pattern is convenient because we can reuse our classes more flexibly by passing in different objects in the constructor.


Now that you know the interface implementation delegate, what about the scenario where the delegate overwrites?

Overwriting one or more interfaces is as we would expect. You define a new method to override the previous method and your class will ignore the overridden previous method.

    class DoSomethingOverriden( client: ApiClient): ApiClient by client {
        override fun callApi(request: String): String {
            return "Overwrite and do something different:$request"}}Copy the code

“Remember this! Delegate implementation is different from normal overwrite and polymorphism!”

1. The principal does not know the delegate object, so the principal cannot call the method of the delegate object and overwrite it. 3. Inside the override method, you cannot call 'super' because the delegate interface implementation class (the delegate) does not inherit from any class.Copy the code
Interface Base {val message: String funprintInt 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 {// Overwrite the message attribute // in b 'printThis property is not accessible in the implementation override val message ="Message of Derived"
        override fun print1Println (){return println(){return println();"Can I override the delegate method?")
        }
        
        override fun print3. Inside the override method, you cannot call 'super' because the delegate interface implementation class does not inherit from any class. //.... } } funmain() {val b = BaseImpl(10)// Construct the delegate object val Derived = derived (b)// construct the delegate interface implementation class Derived. Print ()//2. {BaseImpl: x = 10 message of derived {BaseImpl: x = 10 message of derived {BaseImpl: x = 10 message of derived}Copy the code

As far as I know, there is no direct way to call the delegate object’s methods from the override method inside the delegate, but we can still do it in another way. We can treat the delegate object as a private property and call its methods directly.

Interface Base {val message: String funprintInt 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 {// Overwrite the message attribute in bprintThis property is not accessible in the implementation override val message ="Message of Derived"
        override fun print(){
            b.print()
            println("Overwritten logic.")
        }
    }
    
    fun main() {val b = BaseImpl(10)// Construct a delegate object val Derived = derived (b)// construct a delegate interface implementation class derived. Print () println(derived. Message)} BaseImpl: x = 10// The delegate's override method successfully executed Message of DerivedCopy the code

Now let’s look at how do we implement multiple inheritance using delegates

Let me give you an example. Let’s say we have two interfaces, Printer and Scanner, each with an implementation class. Now we want to create a PrinterScanner Class to combine the two.

Data class Image (val name: String)// Interface Printer{// Interface Printer fun turnOn() fun isOn(): Boolean funprintFun turnOn() fun isOn(): Boolean Fun scan(name: String): Image} object laserPrinter: Printer {// Printer interface implementation singleton - laserPrinter 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 {// Scanner interface implementation singleton - fastScanner var working =false
        override fun isOn(): Boolean = working
        override fun turnOn() {
            working = true} override fun scan(name: String): Image = Image(name)} // This ScannerAndPrinter Class can be configured as a proxy. This ScannerAndPrinter Class can be configured as a proxy by using kotlin's BY keyword. It then passes the constructor parameters to the proxy to access Class ScannerAndPrinter(scanner: scanner, printer: printer): Scanner by scanner, Printer by printer { override fun isOn(): Boolean = (this as Scanner).ison ()// Both singletons need the same method, override fun turnOn() = (this as Scanner).turnon ()// Both singletons have the same method, need to display the overwrite fun scanAndPrint(imageName: String) =printCopy(scan(imageName))
    }
Copy the code

All right! And that’s exactly what we wanted it to do with multiple inheritance through BY. You can also knock it out and run it through the demo to help you understand it.

The latter

Ps: The following paragraph suggests that we find their own composite reuse of Chinese interpretation, MY level of limited translation is not accurate. So as not to mislead others

The principle of compound reuse in object programming: classes should implement polymorphic behavior and code reuse through their instances or other classes that perform the intended function, not by inheriting from base classes or superclasses. This principle is often mentioned in object-oriented programming, as in influential books such as Design Patterns.

The above principles are true in object-oriented programming and even functional programming. In addition, there is no open modifier in the Kotlin class by default, so inheritance is not adopted.

Kotlin’s BY operator is easy to use and perhaps underrated and overlooked. It makes the agent “invisible” and encourages you to think and learn about a better design than some of the previous reuse approaches.

Here’s one last example. A mini-library of objects in a region read from a database. The library does a good job of separating business from logic by using delegates rather than inheritance. Look at the comments in the example to understand (it is recommended to look up).

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> {// out is because we canreturnT or subtypes //(kotlin); // Interface to execute SQL statement andreturndomain objects fun builder(row: Row): T fun executeQuery(sql: String): List<Row> } //declared outside SqlRunner interface to avoid overriding and multiple implementations // declare outside the SqlRunner interface to avoid overwriting and multiple implementations. 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{//(pseudocode) an implementation class for a database interface //trivial example butin 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 forRetrieve Users interface UserPersistence{// Interface needed by the domain fun fetchUser(userId: Int): 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 domaininSQL statements but still abstract from DB connection,transactions etc. // implement SqlRunner<T> interface, 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 } // Construct FakeDbConnection, UserPersistenceBySql, UserSql object UserRepository: UserPersistence by UserPersistenceBySql(FakeDbConnection()) //example of use fun main() { val joe = Println (" touchuser $Joe ")}} userRepository. fetchUser(5) {}Copy the code