Lessons from Work - Interfaces 101
One very popular interview question that I kept running into over the last one year was - What is an Interface. A lot of my classmates had the same experience. Most of the answers I overheard people discussing were full of buzzwords like -
- It is a completely abstract class - no methods are implemented
- It is a contract for classes to follow
- It is used for Polymorphism to provide Flexibility
If you have read this really awesome book called HeadFirst Java, then you will be familiar with this example -
public interface Animal {
void eat();
}
public class Dog implements Animal {
void eat() {
//eat
}
}
public class Cat implements Animal {
void eat() {
//eat
}
}
Animal a = new Dog();
a.eat();
Animal b = new Cat();
b.eat();
The magic in this example, as mentioned in the book, is that you can use the reference type Animal to point to a Dog and Cat both. This ability of Animal to morph into multiple forms is Polymorphism.
But these explanations never had quite the impact on me - because I could not understand how something like this fits in our industry. We also do not really understand the real value of interfaces and polymorphism in OOP when we are in school by just seeing the theory and hence, do not start using them when we start working. In this article, I will roughly outline another example which is loosely based on an experience at work. This was when I first saw the buzzwords play out and came to appreciate the power of an interface. I also learnt this the hard way, by not using them first till my code review was swarmed with comments. I hope that the intuition I present here will help others to start thinking about interfaces in their code from the design phase itself.
Let us imagine a very successful E-Commerce business named Dramazon. For simplicity, we will assume that a single Java project drives this fantastic organization :
public class Recommender {
void magicallyRecommendBooks() {
//this method fetches book recommendations
}
void magicallyRecommendFurniture() {
//this method fetches furniture recommendations
}
void magicallyRecommendGadgets() {
//this method fetches gadget recommendations
}
}
/*Book collection*/
public class BooksLibrary {
private Recommender recommender;
public BooksLibrary(Recommender recommender) {
this.recommender = recommender;
}
public void showBooks() {
recommender.magicallyRecommendBooks();
}
}
/*Furniture collection*/
public class FurnitureCollection {
private Recommender recommender;
public FurnitureCollection(Recommender recommender) {
this.recommender = recommender;
}
public void showFurniture() {
recommender.magicallyRecommendFurniture();
}
}
/*Gadgets collection*/
public class GadgetsHeap {
private Recommender recommender;
public GadgetsHeap(Recommender recommender) {
this.recommender = recommender;
}
public void showGadgets() {
recommender.magicallyRecommendGadgets();
}
}
public class Dramazon {
public static void main(String[] args) {
Recommender recommender = new Recommender();
BooksLibrary books =
new BooksLibrary(recommender);
FurnitureCollection furniture =
new FurnitureCollection(recommender);
GadgetsHeap gadgets =
new GadgetsHeap(recommender);
}
}
A brief description of a huge example - Dramazon has a collection of books, furniture and gadgets. And these "collection classes" require a central recommendation engine which magically gets recommended items from these collections. This is usually how we build out classes in a school project - no interfaces in the picture.
Now lets say that there is another company named AwesomeRecommender. Coincidentally, they have made use of some cutting edge technologies to recommend books, gadgets and furniture and built a Java class named AwesomeRecommender. Dramazon acquires them and needs to integrate this new AwesomeRecommender class in their system. They will now have to change the instantiation in the main() method to
AwesomeRecommender recommender = new AwesomeRecommender();
But along with this, all the three collection classes will also have to change. The member variable in each of these classes needs to be changed from
private Recommender recommender;
to
private AwesomeRecommender recommender;
and the constructor of all the classes will have to change from
public GadgetsHeap(Recommender recommender) {
this.recommender = recommender;
}
to
public GadgetsHeap(AwesomeRecommender recommender) {
this.recommender = recommender;
}
Well, that seems fine - its just a simple find and replace right? But there are many issues when you go against DRY : Don't Repeat Yourself. The deal is that Dramazon has the potential to be very big. Instead of 3 classes they will probably have 3000 classes. Some of these collection classes will be managed by people on other teams in other countries. You will have to go around telling everyone that they need to remove Recommender and replace it with AwesomeRecommender. And honestly, they will not. There is no way to know what will break by just doing the replace. What if the new class does not have a magicallyRecommendGadgets(). The gadgets team will suddenly have no recommendations, no sales and retreating customers. There will be red lights blaring at midnight on a weekend and a potential question mark from the billionaire CEO Jeff Mesos.
But instead if we had built the Recommenders and the subsequent collection classes using an interface, we would have something as follows :
public interface IRecommender {
void magicallyRecommendBooks();
void magicallyRecommendFurniture();
void magicallyRecommendGadgets();
}
public class Recommender implements IRecommender {
@Override
void magicallyRecommendBooks() {
//recommend
}
//implement the remaining methods as well
.
.
.
.
}
public class BooksLibrary {
private IRecommender recommender;
public BooksLibrary(IRecommender recommender) {
this.recommender = recommender;
}
public void showBooks() {
recommender.magicallyRecommendBooks();
}
}
Here you will see the buzzwords that we mentioned at the start of the article, but in action.
1) Flexibility
We can now have
IRecommender recommender = new Recommender();
or easily replace it with
IRecommender recommender = new AwesomeRecommender();
provided that AwesomeRecommender adheres to the...
2) Contract
public class AwesomeRecommender implements IRecommender {
@Override
void magicallyRecommendBooks() {
//recommend
}
//implement the others as well
.
.
.
.
}
If the AwesomeRecommender implements the interface, it guarantees that it will fulfill the contract of providing the required magicallyRecommend...() methods and provide the functionality as needed. This gives us the ability to use...
3) Polymorphism
public class BooksLibrary {
private IRecommender recommender;
public BooksLibrary(IRecommender recommender) {
this.recommender = recommender;
}
public void showBooks() {
recommender.magicallyRecommendBooks();
}
}
Because the class is using IRecommender as the member type, it can point to an instance of both Recommender and AwesomeRecommender. The IRecommender is the Animal of our example, while the Recommender barks and the AwesomeRecommender purrs.
The collection classes on the other hand do not have to worry about what Recommender is sent in from main(). As far as it is an implementation of IRecommender, the classes will just automagically use the Recommender that Dramazon decides to use. No need to update all 3000 classes and no worries about contracts either. You have successfully containerized the expectations of Jeff Mesos.
This is how Polymorphism is used in some real world projects. Well, this is still an imaginary one, but hopefully it better helps capture the real world value of polymorphism. This will be one less code review comment to worry about if you think of it early on in your design.
Notes
- The example is loosely based on a creational pattern - the factory pattern. When on steroids, we come to know of it as Dependency Injection. I will write separately on DI as you are likely going to run into it soon.
- Coding to an interface as opposed to coding to an object has a number of other advantages. One important advantage is that it helps write unit testable code. I will write separately on those experiences as well.
- The monolith example that I describe is very rare now in the world of micro-services. Projects are smaller and you may not see such a use case. Irrespectively, it is a good practice to keep in mind that polymorphism is at your disposal.
Do let me know if you find other uses of abstract classes, interfaces and polymorphism. I would love to try them out in my projects! Thanks!
Very Nice Hamza.
Nice Blog Hamza! Keep it up