Search code examples
androidkotlinsolid-principles

Is it possible to strictly comply with Liskov's principle with interfaces?


I keep digging into Liskov's principle and although I have managed to understand it, I have a doubt... the definition tells us that to comply with Liskov's principle, the parent class must be able to be replaced by the child classes without having to alter the code and being able to use all its functions.

Then the following question has arisen in my mind. Let's imagine we have the typical duck casuistry:

open class Duck {

    open fun swim() {
        println("The duck is swimming")
    }
    open fun quack() {
        println("The duck says quack")
    }
    open fun walk() {
        println("The duck is walking")
    }
}

class NormalDuck: Duck()

class MetalDuck: Duck() {

    override fun swim() {
        throw NotImplementedException("MetalDucks can't swim")
    }
}

Applying Liskov's principle I would be left with something like:

open class Duck {    

    open fun quack() {
        println("The duck says quack")
    }

    open fun walk() {
        println("The duck is walking")
    }
}

open class DuckThatCanSwim : Duck() {

    open fun swim() {
        println("The duck is swimming")
    }
}

class NormalDuck : DuckThatCanSwim()

class MetalDuck : Duck()

val duck = Duck()

duck.quack()
duck.walk()

In this case there is no problem, I can comply with the definition of the principle since I have a Kotlin open class and I can create an instance of it and even if I change it to NormalDUck or MetalDuck, its parent class methods will still work.

But I've seen people posting examples with interfaces, something like:

interface Duck {

    fun quack()
    fun walk()
}

interface DuckThatCanSwim : Duck {

    fun swim()

}

class NormalDuck : DuckThatCanSwim {

    override fun swim() {
        println("The duck is swimming")
    }

    override fun quack() {
        println("The duck says quack")
    }

    override fun walk() {
        println("The duck is walking")
    }
}

class MetalDuck : Duck {


    override fun quack() {
        println("The duck says quack")
    }

    override fun walk() {
        println("The duck is walking")
    }
}

In this case, although Liskov's principle is also fulfilled, it is not completely fulfilled, since I cannot create an instance of an interface, so... with Liskov is it correct to use interfaces or should only classes be used in order to fulfill the whole statement?


Solution

  • The fact that you cannot create an instance of an interface does not matter as far as Liskov's principle is concerned. The principle is regarding types and not just classes. Though you may not be able to create an instance of an Interface, you can certainly define them.

    For example, you could define a variable with the type Duck and then assign either NormalDuck or MetalDuck and in that case both NormalDuck and MetalDuck would satisfy behaviour of interface Duck. Like so:

    val duck: Duck = if (type == "metal") {
        MetalDuck()
    } else {
        NormalDuck()
    }
        
    duck.quack()
    duck.walk()