Spring Framework is only one example of "programming to interface" rule implementation. Through this article we'll cover this topic in more detailed way.
Data Engineering Design Patterns
Looking for a book that defines and solves most common data engineering problems? I'm currently writing
one on that topic and the first chapters are already available in π
Early Release on the O'Reilly platform
I also help solve your data engineering problems π contact@waitingforcode.com π©
At the begin of the article we'll remind us about interface and class definitions. After that we'll explore the superiority of interface design over implementation design.
Interfaces and implementations
Before talking about "programming to interface" approach, we need to define the elements participating it: interface and class. An interface can be understood as a contract. Interface define a set of behavior and rules to respect by the classes implementing it. The implementations, by explicitly implementing given interface, commit to respect defined rules.
The implementations are the classes which define the way of working for methods defined in interface. Unlike interfaces, they do another thing that only define the behavior. They contain code and logic for implemented interface.
Advantages of "programming to interface" approach
After reminding a little bit about interface and implementation definition, we can start to explain why it's better to code to interface rather than for implementation. The first reason of that is the code maintenance. Imagine the situation where you use a java.util.List to store some collections. You can define that in two ways:
Listdata = new ArrayList (); // OR ArrayList data = new ArrayList ();
Imagine that some time after writing this code you find a library which is faster than ArrayList. You can use it easier in the first case when you must change only the right side. In the second case, you should change left and right side. In additionally, maybe you used methods specific for ArrayList which are not defined in List interface. It will lead you to change other fragments of your code and so, risk to introduce some of involuntary bugs.
The second important reason to prefer interface over implementation is testability. When you want to test an object depending on interface, you can either mock or fake interface to provide needed data to execute the test. Take a look on these two classes:
// not really testable class public class NotReallyTestable { // EmailTeller is an implementation public String tellToFriend(EmailTeller teller) { ResponseParams params = new HttpResponseParams(); if (teller.tell()) { params.put("wasTold", true); } return makeView(params); } } // more testable class public class MoreTestableClass { // Teller is an abstraction public String tellToFriend(Teller teller) { ResponseParams params = new HttpResponseParams(); if (teller.tell()) { params.put("wasTold", true); } return makeView(params); } } // test case - we want to only check generated view without // executing the real action in Teller implementation // (we can do it with mock objects, but we can also use test // implementation of Teller interface) public void testTellToFriendOutput() { Teller okTeller = new Teller() { @Override public boolean tell() { return true; } }; Teller nokTeller = new Teller() { @Override public boolean tell() { return true; } }; String resultOk = tellToFriend(okTeller); // make some checks String resultNok = tellToFriend(nokTeller); // make some checks }
The next reason of interface superiority is code portability. Application based on interfaces are decoupled from the implementation part. If we want to reuse only some classes in another systems and replace the rest, it will be simpler to do with interfaces. Imagine that you have two systems which share some features: one for sending e-mails and another for sending SMS. They both can be similar in 95% and the only difference will be the method that sends something (e-mail or SMS). With factory design pattern, the code returning implementation which sends e-mail or SMS will be simple:
// factory class public class SenderFactory { public static Sender createSenderInstance(SenderType type) throws IllegalStateException { switch (type) { case SenderType.EMAIL: return new EmailSender(); break; case SenderType.SMS: return new SmsSender(); break; } throw new IllegalStateException("Sender can't be resolved from provided type: "+type); } } // E-mail application public class Configuration { public static final SenderType SENDER_TYPE = SenderType.EMAIL; } // SMS application public class Configuration { public static final SenderType SENDER_TYPE = SenderType.SMS; } // Controller, service or anything else will be the same for // both applications. Sender will be resolvable like that: Sender sender = SenderFactory.createSenderInstance(Configuration.SENDER_TYPE); sender.sendMessage(messageContent, messageRecipient);
Imagine now how it will be difficult to reuse this code in yet another context, for example in the case of private messages sending through Facebook, without SenderFactory and interface-oriented approach. Without them, you should modify manually every place in the code where sender is invoked. In additionally, you'll be forced to check if it uses methods defined out of Sender interface scope. With the approach presented previously, you'll need only to add new case in the factory and new entry for SenderType enum, as below:
// factory class public class SenderFactory { public static Sender createSenderInstance(SenderType type) throws IllegalStateException { switch (type) { case SenderType.FACEBOOK_PM: return new FacebookPmSender(); break; case SenderType.EMAIL: return new EmailSender(); break; case SenderType.SMS: return new SmsSender(); break; } throw new IllegalStateException("Sender can't be resolved from provided type: "+type); } }
The fourth reason of using interface over implementation can be found in dependency inversion principle. To remind quickly, dependency inversion prefers that high-level modules don't depend on low-level modules. Instead, they all should depend upon abstractions. By low-level modules we can understand the parts making some unitary operations, as writing changes in a file, opening database connection. In the other side, high-level modules encapsulate low-level ones to make operations associated with business logic. For example, a high-level module can be a class creating new object and persisting it into database. It'll use some of low-levels to: open database connection, transform object into format accepted by database, commit SQL query and closing connection. If both modules are based on abstractions (interfaces), they depend automatically on a concept and not implementation. In this way, we can reduce the risk of introduce accidental bugs. It also reduces coupling between modules and thanks to it we can, for example, fake some objects before making test cases.
An example of software design pattern using dependency inversion of principle is dependency injection. In this pattern the objects live in dependency (as for low and high level modules). Low levels modules are considered here as dependencies while high level as dependent objects (clients). The idea is still based on the separation of dependencies creation from its behavior. We can find this principle in a lot of application frameworks, and especially in Spring Framework. In another category you can find some articles about Spring Framework.
This article proves why it's better to write a system based on interfaces rather than on implementations. Lose coupling, dependency handling, testability and refactoring facilities are the main reasons which come down for interfaces. Even for one-client applications which, one day, can grow up and be deployed by another customers with some of customized features.