In this post, we cover why interfaces are used and how they help to build maintainable software.

Abstraction of the Actual Implementation

In its most basic form, an interface hides a concrete implementation from the caller and thereby creates an abstraction. This abstraction meets certain conditions (or requirements) that are guaranteed by its (implicit) contract.

In the Java programming language, the classical example is the List interface, which is an ordered sequence of elements allowing for random access1. Well-known implementations are ArrayList and LinkedList. Both fulfill the contract of a list, but their underlying implementation and data structure differ. ArrayList is optimized for random access in constant time, while LinkedList allows for fast addition and removal of elements.

To work correctly, the application doesn’t care which specific implementation is used. It relies on the List interface abstraction only.

Level of Abstraction

Finding the right level of abstraction is difficult. A general rule is to build the interface for the consumer. This implies that the interface serves the user and not the implementation.

For example, when adding an element to a list, it’s easier for the callee not to worry that the underlying data structure has enough capacity for the new element. The implementation should handle the allocation of memory automatically, if necessary.

Moreover, the callee has to decide which contracts are required. In the List example this includes the questions of whether the list needs to be ordered, are duplicates allowed, is random access required, etc. Besides List also Collection, Set, Queue may fulfill the application requirements.

Replacing Implementations

Depending on our measured or expected interaction pattern, we decide on an implementation that suits us. We decide on the implementation once (usually during the initialization), and the application continues to use the implementation through the interface.

List<String> words = new ArrayList();
// |                      \--> concrete implementation
// \--> Application uses the list List abstraction

When we later discover that another implementation is more suitable, we only change the initialization code, and we’re done. The application continues to use the implementation through the interface.

As this is usually just one line, it’s simple. Furthermore, the test should still be green - assuming we stick to testing the behavior of our component and not the internal state of the list.

Reusability

Interfaces tend to be building blocks that can be reused, which cover one concept, ideally not more.

Particularly with lower-level abstractions, there is a high chance that they’re used multiple times within an application. Even when not, separation of concerns is a great motivation to define clear boundaries and build clean software.

As soon as we discover that a certain functionality covers multiple concepts, we refactor and separate those so that each class/component/module has a single responsibility2.

Interfaces for Only One Implementation

A purist would say that each concept needs an interface, even when it’s only used once.

However in practice, there is a cost to having an interface. Even with today’s refactoring tools it still takes time to initially write, read, and maintain it. It depends on you to decide whether it’s worth it.

Multiple Methods in One Interface

An interface should be easy to use. Therefore, the fewer methods the better.

Fewer methods also tend to indicate a better level of abstraction, as the goal of an interface is to hide the complicated internals. And these internals shouldn’t be exposed through configuration options, etc - at least not through the interface.

In the case of the ArrayList, the underlying data structure is an array, which has a capacity. The capacity is managed by the ArrayList. It can be manually adjusted through ensureCapacity, which is a method on the ArrayList class - not the List interface.

Interfaces in Combination with Visibility Modifiers

The poor man’s interface is the use of visibility modifiers like public and private. Still, as those are easy to change (just one word), it can be desirable to use a real interface so that more thoughts are put into the design and changes become more obvious in a code review.

Typically, a software application has a certain folder structure or package hierarchy. I find interfaces to be a nice gateway to packages. Assuming I have the following structure, only the interface is public. All implementations stay hidden and aren’t accessible from the outside:

- package.scanner
  - PackageScannerInterface  (public)
  - ClasspathPackageScanner  (package private)
  - FileSystemPackageScanner (package private)

- package.callee
  - DoWorkComponent          (can only use the PackageScanner interface)

Use in Dependency Injection

One common use are dependency injection frameworks. Classes contain their business logic, and all dependencies (i.e. database connections, logging frameworks, other services) are injected from the outside by the framework. The class doesn’t require a specific implementation, but rather any implementation that fulfills the interface’s contract.

The overall benefit is loose coupling as application flow can be easily adjusted - even during runtime.

Testing

When a single responsibility isn’t considered in the application code, it usually becomes apparent in the testing code. Test methods become clunky, they need lots of initialization code or a class has a lot of different test cases. Use it as an indicator that the chosen abstraction isn’t optimal.

Writing test code (early) is a great indicator whether an abstraction is well picked.

Testing of Implementations

Assuming we want to test ArrayList. Some test cases check that the List contract is fulfilled, while some other tests verify that our specific implementation behaves as it should.

Therefore, we can create one test suite to verify all List implementation pass our list requirements, like: When one element is added to an empty list then the size is one.

For the basic list example this might be sufficient, but I also want to verify the internal behavior. For example: When a specific amount of elements are added, the capacity needs to be increased. This is a specific test of the ArrayList implementation - separate from the List interface tests. As tests are split between two test suites now, one needs to be careful when placing and maintaining unit tests.

Leakage of Internals

Leakage of internals through not ideal abstractions has been discussed before.

Another tricky aspect is error handling. Internal errors need to be translated into errors of its abstraction.

For example when accessing the second element in an empty ArrayList, the implementation might access an invalid index in the array and get an IndesOutOfBoundsException. However in the list abstraction, this type of error doesn’t exist. Instead, it’s translated to a NoSuchElementException as the list is conceptionally different from an array.

Ideally, the errors are still useful. Still, as abstractions hide details, also the abstraction’s errors might hide details. Use the cause property of abstracted exceptions to indicate the original error.

Conclusion

In summary, interfaces are a great way to organize software code and create structure through abstractions. It’s basically architecture on the code level.

There’s some overhead, but it’s required to build maintainable software using small, easy to understand and reusable blocks.

In the real world, we come across them all the time. The last time you mounted a picture, you were looking for something that could hold the weight of the picture on the underlying surface (abstraction). Objects that fulfill this contract include nails, screws, pins, and possibly magnets (implementation).

Interfaces aren’t that theoretical, why not use them more?


  1. One example is a sequence of numbers, i.e. 1 4 7 7 2. Random access states that these elements can be accessed by their list index directly. ↩︎

  2. See the single responsibility principle as part of SOLID ↩︎