Too Many Interfaces

Interfaces are good stuff. Does that mean the more the better?


What is an Interface

The term interface is overloaded. For the purpose of this text I use the following definition: An interface is a language construct typical for strongly typed programming languages like Java or Smalltalk. An interface specifies behavior of objects which implement it.

Interfaces can be useful as a tool to implement good design practices like the Strategy pattern or the Interface segregation principle. On the other hand, mere use of interfaces doesn’t guarantee a good design. When overused, it could make things even worse.

Fallacies of Interfaces

Using interfaces does not automatically bring any benefits. Here are typical misconceptions:

Interfaces are Abstractions

An abstraction means a separation of what from how. Abstractions separate a solution from its implementation. The reason is decomposition. Decomposition is about dividing a complex problem into smaller composable sub-problems. As Eric Elliott tweeted:

The essence of development is composition. The essence of software design is problem decomposition.

Although often used to implement it, an interface doesn’t necessarily mean abstraction; check out the following code:

import com.example.vegetateframework.orm.Entity;

interface ProductRepository {

    Entity<Product> findProduct(Long id);
}

Is this a good abstraction? I don’t think so. The return type of the method is Entity<Product> which exposes the implementation details and binds the interface to an ORM framework. Another issue is the Long datatype of the product ID. Longs are usually used in the database for record IDs, but those database IDs must not leak into the interface. In my opinion, the name of the interface is bad too, as the term Repository is pure technical.

There is nothing in the language itself to prevent us from writing such interfaces. In general, interfaces are not abstractions.

All the other upcoming points are driven by this particular misconception, but it’s still worth mentioning them here explicitly as they are probably the most justified causes for incorrectly using interfaces.

Interfaces are Contracts

A contract is an unbreakable agreement between the provider and consumers about the expected functionality. Usually, describes behavior of a component in a declarative way.

The level of abstraction is important. Consider the following example:

interface PriceCalculator {

    Float calculatePrice(Product product);

    Float applyDiscount(Integer discount, Float basePrice);
}

The second method applyDiscount lives on a lower level of abstraction and shouldn’t be part of the interface. The class which implements this interface will probably have this method as a private member. Consumers of the contract are not interested in such low-level functionality and it’s not a good idea to bind them to it. Therefore, this interface is not a proper contract.

Interfaces are Loose Coupling

Level of coupling is defined by the number of dependencies. Adding an additional level of indirection (an interface) doesn't make the code more decoupled.

Compare these two code snippets:

class ShoppingCart {

    private PriceCalculator priceCalc;
    private Set<CartItem> items;
 
    public Double totalAmount() {
        return items.stream()
            .mapToDouble(ci -> ci.quantity() *
                priceCalc.calculatePrice(ci.product())
            ).sum();
    }
    // ...
}

class PriceCalculator {

    public calculatePrice(Product product) {
        return ...;
    }
}
class ShoppingCart {

    // as above ...
}

interface PriceCalculator {

    calculatePrice(Product product);
}

class PriceCalculatorImpl implements PriceCalculator {

    public calculatePrice(Product product) {
        return ...;
    }
}

In the former snippet, the class ShoppingCart has a dependency to the concrete class PriceCalculator, while in the latter a new layer of indirection, the interface, was added.

Did we make the code more loosely coupled? The ShoppingCart has still one dependency, nothing changed, but now there is a new dependency between the PriceCalculator interface and its implementation PriceCalculatorImpl. That means we’ve just made things worse! Complexity increased and the code became harder to understand. No good.

Interfaces are Reuse

Planned reuse has a lot to do with the DRY principle. When we find ourselves writing the same code several times, we usually cut the snippet out and put it into a separated module. Then, we reference this module as a dependency.

Another motivation for creating incorrect reuse is speculation about future scenarios. The YAGNI principle addresses this issue, however, it’s still not uncommon to see this principle being violated even by experienced developers. But that's a different story.

Excessive aiming for reuse leads often to wrong abstractions. Consider the following code:

interface Product {

    String name();
    Float price();
}

class ProductImpl implements Product {

    private String name;
    private Float price;

    ProductImpl(String name, Float price) {
        this.name = name;
        this.price = price;
    }
    public String name() { return name; }
    public Float price() { return price; }
}

Does the interface make the code more reusable? Hardly. What is the benefit of that? I can’t think of any...

Interfaces solve Circular Dependencies

Carola Lilienthal states in her book Sustainable Software Architecture:

If a system has cycles, it is because the tasks of the classes involved are not clearly defined.

Her observation tells us that the root cause of circular dependencies lies in incorrect design of the domain. It means the circular dependencies are fundamentally not a technical issue and therefore cannot be solved easily by mere technical solutions such as interfaces.

Breaking circular dependencies with an interface as middleman may remove only compile circular dependencies in code, but not the circular dependencies in runtime. The bad thing is, when compile cycles disappear, the runtime circular dependencies become more or less invisible, what leads not only to remaining in the system but also to justifying more and more of them as the codebase gets bigger.

Circular dependencies must be solved on the level of overall system design, otherwise a logical monolith (the Big ball of mud) will emerge. Interfaces often encourage hiding design flaws, such as high coupling and low cohesion, with purely technical shortcuts.

What is a Good Abstraction

Keep things simple by not providing abstractions until the abstractions provide simplicity. ~ Kent Beck

Every construct in software development must be a solution to a problem.

A good abstraction is a solution to an actual problem. An unnecessary abstraction is waste in the best case or, worse, increase of accidental complexity of the system. A good abstraction reduces complexity and provides simplicity. An interface with only a single implementation is usually a wrong abstraction and should be eliminated.

A good abstraction is simple and clear. All complicated details are hidden under a short and easy-to-use facade, that provides a clear insight and is intuitive to work with.

A good abstraction is relevant. All irrelevant concerns are hidden or separated from the interface. A good abstraction must be designed with customer needs in mind. A well-designed interface focuses on ease of use, not ease of implementation.

A good abstraction is domain-driven. No technical concern exists in the interface. Interfaces like ProductService, ProductRepository or ProductFactory are not particularly good abstractions.

Domain-Driven Abstractions

Domain-driven abstractions tend to be very stable. This idea is based on the presumption that business changes slower than software. It is a great benefit for code maintenance. Consider the following example:

class FindProduct {

    Product byCode(String code) {
        return ...
    }
}

@RestController
class CatalogController {

    private FindProduct findProduct;

    @GetMapping
    Product findProduct(String code) {
        return findProduct.byCode(code);
    }
}

In the example above, if we ever decide to replace the concrete class FindProduct with an interface, the client code will remain unchanged, because the abstraction is decoupled from the implementation structure:

interface FindProduct {

    Product byCode(String code);
}

class FindProductJdbc implements FindProduct {

    private JdbcTemplate jdbcTemplate;

    Product byCode(String code) {
        return jdbcTemplate.query(...);
    }
}

class FindProductMongo implements FindProduct {

    private MongoCollection collection;

    Product byCode(String code) {
        return collection.find(...);
    }
}

@RestController
class CatalogController {

    // NO CHANGES NEEDED HERE :-)
}

Good abstractions don't have to be sought, they emerge on their own when the design is domain-driven.

How Many is Too Much

To summarize a bit, an interface is too much when it doesn’t serve a good abstraction, particularly:

But What About…?

There are several problem scenarios that are traditionally solved using interfaces. A lot of literature, articles and tutorials recommend such. A technical solution is always easier to understand and apply, but I believe it’s a cure for symptoms only, not for the cause.

Dependency Injection

Dependency injection is one of the most important principles in software development. Please, use it!

In general, it controls which object is used. Choosing an implementation for an interface-based dependency is just a special case. The Dependency injection principle is equally useful without any additional level of indirection.

Dependency injection doesn't need interfaces as well as a good abstraction doesn't need interfaces.

Mocking

If you find yourself in a situation you need to introduce an interface to make a module testable, try to step back first and think about the design of the module. Focus on cohesion and modularity. Proper decomposition can solve a lot of coupling issues without introducing any additional interface.

When really needed, a concrete class can be mocked using existing tools as well.

Conclusion

Program to interfaces is a well known practice, but one shouldn't take it literally. Instead, saying "program to abstractions", focusing more on what rather than how, may clear things up.

Happy interfacing!