Roger Ngo's Website

Personal thoughts about life and tech written down.

Interface and Abstract Classes

Interfaces and Abstract Classes

Let us take a moment to first realize what the purpose of interfaces and abstract classes are for. You are probably reading this because you have some sort of idea what these two types of classes are, but aren't really sure what specific purpose, or application they actually serve that would make it beneficial. To make this realization happen, first we need to be firm on the fact that interfaces and abstract classes are part of the object-oriented programming paradigm. That means that if you are working with some object oriented language, then the concepts of interfaces and abstract classes both exist.

Interfaces Explained (Almost) Simply

You might have read that interfaces are "contracts". What does that even mean? Let's take an example. When we create an interface, we are saying that any sort of class that chooses to implement the interface must override, or provide an implementation of all the methods defined by the interface.

Now, why would we even be interested in doing this? Well, implementation of an interface allows us to do one thing: we need to take advantage of the fact that if an object implements an interface, then we are guaranteed that any object instantiated using the interface type have this method be callable.

The above mentioned is called polymorphism. The running program does not care about how something is implemented, just as long as it is implemented and the signature exists. If we adhere to the signature defined by the interface contract, then we can generically invoked the object of the class which implements this contract.

In summary, the specific implementation tied to the object which implements the interface will be executed as long as the method to callable through interface implementation during program execution.

This is especially useful when it comes to working with generics. For example, suppose we have an interface called Drinkable. Our Drinkable interface has two methods, refill() and drink(). It is important to note that these two methods are merely signatures and have no implementation. It is up to the class which implements the interface to provide the implementation. You will see why now.

Any classes then which implements Drinkable must override refill() and drink() as that is guaranteed by contract through implementation of the interface. If I had a Coffee class that implements Drinkable, then there is expectation that refill() and drink() should exist in the Coffee class. If I had a Tea class that implements Drinkable, then I should also expect to have both refill() and drink().

The compiler enforces that if we declare an implementation of a particular interface, we must provide the implementation to the interface methods. I have been very careful about the use of my terminology here. Interfaces are not classes. When a class declares that it will implement a particular interface, it has entered in agreement that the class will provide a set of declarations and definitions of the methods which the interface requires. The compiler will throw an error if this contractual obligation is not met.

Therefore, it is important to note that implementing an interface does not solve a problem per say. It merely helps construct patterns to aid in solving the problem. To think of it in a naive fashion, we can say interfaces help organize code to make things more modular.

Going back to generics, and containers. Now, suppose we have a LinkedList of Drinkable objects. We can define and initialize a generic container to hold various types of objects which implement this particular interface.

public interface Drinkable {
    void drink();
    void refill();
}

Defining and initializing our list:

List<Drinkable> drinks = new LinkedList<Drinkable>();

Taking this a step further, let’s initialize various objects which implement the Drinkable interface: Coffee, Tea, Soda and Water.

public class Coffee implements Drinkable {
    public void drink() {
        System.out.println("Sip, sip, sip... You're drinking coffee!");
    }

    public void refill() {
        System.out.println("Careful, don't drink too much caffeine!");
    }
}
public class Tea implements Drinkable {
    public void drink() {
        System.out.println("Sip, sip, sip... You're drinking tea! It's very hot!");
    }

    public void refill() {
        System.out.println("Careful, don't drink too much caffeine!");
    }
}
public class Soda implements Drinkable {
    public void drink() {
        System.out.println("Gulp, gulp. *burp* Excuse you!");
    }

    public void refill() {
        System.out.println("Too much sugar for you!");
    }
}
public class Water implements Drinkable {
    public void drink() {
        System.out.println("GULP! GULP! GULP!... You're drinking water!");
    }

    public void refill() {
        System.out.println("Drink up! Water is great for you!");
    }
}
Coffee c = new Coffee();
Tea t = new Coffee();
Soda s = new Soda();
Water w = new Water();

Since all the above types of object classes implement the Drinkable interface, they are all common types and can be added to our list.

drinks.add(c);
drinks.add(t);
drinks.add(s);
drinks.add(w);

Now, suppose we want to drink all these drinks. We can do so easily by just accessing each object in our list. We know that everything in this list has a drink() method due to contractual obligation in that each of these objects belonging to the classes which implement Drinkable, have the drink() method.

for(Drinkable d : drinks) {
d.drink();
}

The output:

Sip, sip, sip... You're drinking coffee!
Sip, sip, sip... You're drinking tea! It's very hot!
Gulp, gulp. *burp* Excuse you!
GULP! GULP! GULP!... You're drinking water!

How about refilling? It is the same approach.

for(Drinkable d : q) {
    d.refill();
}

All this was easy in that we had a guarantee that by having classes which implement the contract Drinkable, we had a guaranteed precondition that the methods drink() and refill() would exist.

If we had not decided to implement Drinkable, then each one of our object classes: Coffee, Tea, Soda and Water would not have any reinforcement for which what should be called a "drink" method and a "refill" method. For example, Coffee would have drinkMe() while Tea could have drinkTea() and Soda and Water could coincidentally have drink(). If the aforementioned was the case, then we would have something like this:

c.drinkMe();
t.drinkTea();
s.drink();
w.drink();

It would be really hard to write modular code that would process these types of drinks in a predictable fashion.

Various applications would be file input and output where we have a File processor which takes in some sort of FileReader object. We can implement an abstraction of the file to be read called Readable. A FileReader object doesn't have to be concerned with what type of source the file is coming from -- albeit a database, file system on disk, network, etc. The only concern is if there is some sort of read() and write() method through an interface contract by implementing the Readable interface. From there, any FileReader can just call read() and write() and process the file accordingly. This allows lots of modularity within software packages.

Abstract Classes

Abstract classes can be confused with interfaces because they also leverage the concept of polymorphism, and they include methods which are implemented and some which are not implemented.

These methods which are not implemented are declared abstract. Again, keep in mind that abstract classes do have implemented methods too. This is the distinction between an interface and abstract class.

Any class declared abstract will not be able to be instantiated. This means you cannot create an instance of an abstract class. The purpose of an abstract class is to derive a new class-type from it. That means to use the abstract class, we must create another class that extends, or inherits the abstract class. The formal word for this is inheritance.

When we inherit the abstract class, we can override the methods the abstract methods. We also gain the properties of the abstract class with our new class in which we have inherited from the parent class.

If we think of interfaces as contracts, where we must implement all the methods defined in the contract, we can think of abstract classes as templates: where we actually have some implementation and properties that a class can acquire along with signatures of methods in which the acquiring class must implement.

Abstract classes usually define the type and properties of the class, while interfaces define the actions in which a class can do. This is why you sometimes see the combination of the two being used when classes get defined. For example:

class Coffee extends Drink implements Drinkable {}

We say that the Coffee class extends a class called Drink. This means that Coffee now acquires all properties of the Drink class and will include additional properties that will make it be distinguishable as a Coffee, rather than a drink. Additionally, the Coffee will implement the Drinkable interface, which means that it will be able to let a client perform actions which are valid for a drink.

Declaring a class as abstract and then having a class inherit from the abstract class is advantageous in the sense that child classes can be simple. If done properly, it is assumed that calling a method in the parent class in which is overridden by the child class is completely valid and required to be callable. This is the Liskov Substitution Principle.

Going back to our Drink example, suppose Drink is our parent class and Coffee inherits from the Drink class.

public abstract class Drink {
    public void sayHi() {
        System.out.println("Hello, there! I'm a drink and my type is... ");
        printType();
    }
    abstract void printType();
}

public class Coffee extends Drink implements Drinkable {
    public void doSomething() {
        super.sayHi();
    }

    public void printType() {
        System.out.print("Coffee");
    }

    public void drink() {
        System.out.println("Sip, sip, sip... You're drinking coffee!");
    }

    public void refill() {
        System.out.println("Careful, don't drink too much caffeine!");
    }
}

The method within the Drink class which exercises the Liskov Substitution Principle is sayHi(). Notice how the abstract method printType() is made available and the sayHi() method within the calls it? What actually happens is that when sayHi() is invoked within the child class, the child class’s overridden version of printType() is executed. This is possible due to the requirement in that child classes must always override parent methods which are declared abstract.

The main differentiation between interface contracts and abstract classes is fuzzy, but we just need to restate and remember what makes them unique:

  1. Interfaces serve as implementation contracts for predictable usage of code.
  2. Abstract classes provide a way to inherit properties and methods from their parent along with overriding any parent methods which are declared abstract.

Both take advantage of polymorphism to aid for method calls, and because of that, both must be used appropriately for certain situations. That's purely a design choice and because there is no clear cut, binary answer on when to use an interface or an abstract class in many teams I've been in, is in my opinion, probably the reason why there is much confusion between the two.