10 minutes read

Sometimes, working with classes, we have an idea of what a class is supposed to do, but we don't really need to – or can't for some reason – code the whole class at once. That's when interfaces come in handy. Let's see what they are and how they work.

What are interfaces?

Imagine, you'd like to get a pet but can't yet decide between a cat, a dog, and, let's say, a lizard. You just want your pet to run around the house and make some sounds. Lots of pets do that, even if in their own way. So, as long as we know that they do what we want them to do, any pet will fit your definition. Same with interfaces, or contracts for the implementing classes: interfaces define what methods and properties the implementing class must have. A class might implement these methods in its own way, but as long as it does what it's supposed to do, we are fine.

We are already familiar with the concept of classes – an indispensable part of object-oriented programming. For example, if we are programming a simulation or a game where we use animals, we might need some classes that represent them:

class Cat() {
    val numberOfLimbs: Int = 4
    fun move() {
        run()
    }

    fun communicate() {
        sayMeow()
    }
}

class Parrot {
    val numberOfLimbs: Int = 2
    fun move() {
        fly()
    }

    fun communicate() {
        talk()
    }
}

Generally speaking, if we need to implement several entities of the same category, they must have some shared properties. Like in the above example: despite the obvious difference between the animals, all of them have limbs and all of them share the abilities to move and somehow communicate.

Interfaces provide a way to make something like the skeleton of a class. We can say that for our purposes, all animals have:

  • A certain number of limbs;

  • The ability to move;

  • The ability to communicate.

Thus, our "skeleton" for a category of animals will certainly have a field for the number of legs and methods like move and communicate. This can be achieved via the usage of interfaces.

An important note: all animals are different. The bird moves differently compared to the cat: the former uses wings to fly, while the latter jumps and runs on feet. An interface doesn't need to know the specifics: it just states that if X is an animal, then it can move. The manner of moving depends on the implementation of the interface – on the class that implements that interface.

See it as a box with fixed inputs and outputs. We don't know what happens inside, but as long as we know that it's an Animal, it can move. One way or another.

Implementation

In Kotlin, interfaces are defined similarly to classes, only without constructors – interfaces cannot store states.

It means that we can't create an instance of an interface, but we can create an instance of the class implementing that interface.

interface Animal {
    val numberOfLimbs: Int
    fun move()
    fun communicate(): String
}

Here we have a simple interface – a "skeleton" for the classes that will represent different animals. Now, we just need to learn how to create classes based on such a structure.

Just to brush up on it, the interface implies that whatever implements it, needs to have, in our example, a certain number of legs (or at least, a corresponding variable) and methods for communicating and moving. But the classes that implement the interface might all be different, so in their respective specific cases, the details of these implementations may vary. The cat moves differently from a bird, but since they both implement the same Animal interface, they are guaranteed to be able to move, i.e., to have the move function in our simulation. The methods of an interface, just like any other methods, might return some values (as communicate in the example above).

The interface is implemented similarly to inheriting from another class:

class MyAnimalClass : Animal {
	/* ... */    
}

Then, each field or method declared in the interface, needs to be declared in the class with the keyword override, as it shows that we "overwrite" the general case of the interface with the specifics of its implementation.
If in this new class we need to have some methods that are not part of the interface we're implementing, override should be omitted.

interface Animal {
    fun myAnimalMethod() { /* ... */
    }
    // The body of the method is going to be replaced by the implementation of MyAnimalClass.
}

class MyAnimalClass : Animal {
    override fun myAnimalMethod()
    /* ... */
}

Example

Let's take a look at a more in-depth example featuring our Animal interface:

class Cat : Animal {
    override val numberOfLimbs: Int = 4

    override fun move() {
        run()
    }

    override fun communicate(): String {        
        return sayMeow()
    }
}

Note: the cat is an animal and has a defined type of movement, as opposed to the general movement ability of all animals stated in the interface.

class Parrot : Animal {
    override val numberOfLimbs: Int = 2

    override fun move() {
        fly()
    }

    override fun communicate(): String {
        return speak()
    }
}

Note: the parrot is also an animal, but of a specific type (bird), and its implementation is different: it has only 2 lower limbs, and its way of moving differs from that of the cat.

There's an important thing about implementing interfaces: a class derived from the interface must implement all abstract members of the interface (functions and methods without implementation). Otherwise, we'll get an error:

class Cat : Animal {
    override val numberOfLimbs: Int = 4

    override fun move() {
        run()
    }

/*  an error here

    override fun communicate(): String {
        return sayMeow()
    }
*/
}

Note: notice that if we remove the commented part – essentially, making the simulated cat unable to communicate – we'll get an error on the class declaration stating that we haven't implemented a member of the interface.

It's like constructing a car – if we follow the blueprint but fail to install some of the parts, the car won't be able to function properly. The same is true about the class we're implementing.

Also, note that we don't have to override every single property or method: if they have a default implementation (which we will discuss in the next part), you don't need to override them. You can do it, though, if the default implementation doesn't suit your goals.

Adding default implementation

Since an interface cannot maintain states, as it's simply a contract for other classes to implement, we can't construct an interface in the following way:

interface Animal {
    val numberOfLimbs: Int
    fun move()
    fun communicate(): String
    val age = 10 // Error: Property initializers are not allowed in interfaces
}

However, we can use getters to achieve the same result (you can't use setters, though, since there are no instances to assign anything to):

interface Animal {
    val numberOfLimbs: Int
    fun move()
    fun communicate(): String

    val age: Int
        get() = 10
}

Since methods represent a kind of series of actions or a certain behavior, default implementation applies to them as well:

interface Animal {
    val numberOfLimbs: Int
    fun move()
    fun communicate(): String

    val age: Int
        get() = 10

    // Default implementation of a method
    fun printNumberOfLimbs() {
        print(numberOfLimbs)
    }
}

Note: default implementation allows you to skip overriding certain properties or methods in a derived class, except for the cases when the default functionality is not sufficient.

More than just a pattern

So far, it might seem like interfaces are a convenient way of making patterns for building classes. However, that's not exactly the case because an interface is mostly used as a model of interaction with a certain object. Interfaces might be compared to contracts because whatever is using this interface is guaranteed to possess the range of qualities defined in it. Thus, we know what to expect from a class, and we can be confident that any particular class that implements a certain interface will have method1 and method2. Moreover, interfaces also define the way we can interact with the implementing class.

interface DataHolder {
    val id: Int
    val description: String
    val currentState: String

    fun printInfo()
    fun updateInfo()
    fun clearInfo()
}

class Entity : DataHolder {
    /* some code */
}

Note: whatever implements the DataHolder interface, we can expect it to possess the methods listed above.

The word "contract" describes the idea of an interface well since when using an implementation of an interface, we are guaranteed to get certain particular methods and properties.

Conclusion

Interfaces provide a neat way of generalizing our code and keeping it clean: having first stated what we expect to get from a class, we can create a structure which we will rely on later. If a class implements some interfaces, we know in advance that it will have all the functionality these interfaces promise to us. In the context of OOP, interfaces represent the concepts of abstraction and encapsulation.

142 learners liked this piece of theory. 4 didn't like it. What about you?
Report a typo