When to add an interface to a class

Posted on by Matthias Noback

I'm currently revising my book "Principles of Package Design". It covers lots of design principles, like the SOLID principles and the lesser known Package (or Component) Design Principles. When discussing these principles in the book, I regularly encourage the reader to add more interfaces to their classes, to make the overall design of the package or application more flexible. However, not every class needs an interface, and not every interface makes sense. I thought it would be useful to enumerate some good reasons for adding an interface to a class. At the end of this post I'll make sure to mention a few good reasons for not adding an interface too.

If not all public methods are meant to be used by regular clients

A class always has an implicit interface, consisting of all its public methods. This is how the class will be known to other classes who use it. An implicit interface can easily be turned into an explicit one by collecting all those public methods (except for the constructor, which should not be considered a regular method), stripping the method bodies and copying the remaining method signatures into an interface file.

// The original class with only an implicit interface:

final class EntityManager
{
    public function persist($object): void
    {
        // ...
    }

    public function flush($object = null): void
    {
        // ...
    }

    public function getConnection(): Connection
    {
        // ...
    }

    public function getCache(): Cache
    {
        // ...
    }

    // and so on
}

// The extracted - explicit - interface:

interface EntityManager
{
    public function persist($object): void;

    public function flush($object = null): void;

    public function getConnection(): Connection;

    public function getCache(): Cache;

    // ...
}

However, regular clients of EntityManager won't need access to the internally used Connection or Cache object which can be retrieved by calling getConnection() or getCache() respectively. You could even say that the implicit interface of the EntityManager class unnecessarily exposes implementation details and internal data structures to clients.

By copying the signatures of these methods to the newly created EntityManager interface, we missed the opportunity to limit the size of the interface as it gets exposed to regular clients. It would be most useful if clients only needed to depend on the methods they need. So the improved EntityManager interface should only keep persist() and flush().

interface EntityManager
{
    public function persist($object);

    public function flush($object = null);
}

You may know this strategy from the Interface segregation principle, which tells you not to let clients depend on methods they don't use (or shouldn't use!).

If the class uses I/O

Whenever a class makes some call that uses I/O (the network, the filesystem, the system's source of randomness, or the system clock), you should definitely provide an interface for it. The reason being that in a test scenario you want to replace that class with a test double and you need an interface for creating that test double. An example of a class that uses I/O is the CurlHttpClient:

// A class that uses IO:

final class CurlHttpClient
{
    public function get(string $url): string
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        // This call uses the network!
        $result = curl_exec($ch);

        // ...

        return $result;
    }
}

// An explicit interface for HTTP clients like CurlHttpClient

interface HttpClient
{
    public function get(string $url): string;
}

Introducing an interface for such classes is usually an excellent situation to apply the Dependency inversion principle as well: make sure the interface is more abstract than the class. A first step would be to remove the specificness from the class name and methods, as we did in the example: we went from CurlHttpClient to HttpClient, hiding the fact that Curl is used to do the actual work. The next step would be to find out how this interface will be used. For example, is it used to communicate with a remote service to load user data from, like in the AuthenticationManager class below?

final class AuthenticationManager
{
    private $client;

    public function __construct(HttpClient $client)
    {
        $this->client = $client;
    }

    public function authenticate(Request $request): void
    {
        $username = $request->request->get('username');

        $userData = json_decode($this->client->get('/user?username=' . $username), true);

        // ...
    }
}

In that case, we could take the design to the next level by acknowledging that AuthenticationManager doesn't really need an HttpClient, but rather a "user data provider". This is a more abstract concept, which can easily be modelled as an interface:

// A proper abstraction for something that "provides user data":

interface UserDataProvider
{
    public function getByUsername(string $username): array;
}

final class AuthenticationManager
{
    private $userDataProvider;

    public function __construct(UserDataProvider $userDataProvider)
    {
        $this->userDataProvider = $userDataProvider;
    }

    public function authenticate(Request $request): void
    {
        $username = $request->request->get('username');

        $userData = $this->userDataProvider->getByUsername($username);

        // ...
    }
}

Introducing the UserDataProvider abstraction makes the AuthenticationManager class much more flexible, allowing us to plug a different strategy for providing user data. It will also make it easier to provide test doubles for the dependencies of AuthenticationManager. Instead of preparing an HttpClient stub which returns a carefully crafted HTTP response object, we can now simply return an array of user data.

If you'd like to know more about using test doubles to replace I/O calls, take a look at my article series on "Mocking at architectural boundaries":

If the class depends on third-party code

If there is some third-party code (e.g. from a package you don't maintain yourself) that is used in your class, it can be wise to isolate the integration of your code with this third-party code and hide the details behind an interface. Good reasons to do so are:

  • The (implicit) interface wouldn't be how you would’ve designed it yourself.
  • You're not sure if the package is safe to rely on.

Let's say you need a diffing tool to calculate the differences between two multi-line strings. There's an open source package (nicky/funky-diff) which provides more or less what you need, but the API is a bit off. You want a string with pluses and minuses, but the class in this package returns a list of ChunkDiff objects:

class FunkyDiffer
{
    /**
     * @param array $from Lines
     * @param array $to Lines to compare to
     * @return array|ChunkDiff[]
     */
    public function diff(array $from, array $to): array
    {
        // ...
    }
}

Besides offering a strange API, the package is being "maintained" by someone you've never heard of (and it has 15 open issues and 7 pull requests). So you need to protect the stability of your package and you define your own interface. Then you add an Adapter class which implements your interface, yet delegates the work to the FunkyDiffer class:

// Your own interface:

interface Differ
{
    public function generate(string $from, string $to): string;
}

// The Adapter class:

final class DifferUsesFunkyDiffer implements Differ
{
    private $funkyDiffer;

    public function __construct(FunkyDiffer $funkyDiffer)
    {
        $this->funkyDiffer = $funkyDiffer;
    }

    public function generate(string $from, string $to): string
    {
        return implode(
            "\n", 
            $this->funkyDiffer->diff(
                explode("\n", $from),
                explode("\n", $to)
            )
        );
    }
}

The advantage of this approach is that from now on you can always switch to a different library, without changing the bulk of your code. Only the adapter class needs to be rewritten to use that other library.

By the way, a good old Façade might be an option here too, since it would hide the use of the third-party implementation. However, due to the lack of an explicit interface, you wouldn't be able to experiment with alternative implementations. If the code is part of a package, the same goes for its users: they won't be able to write their own implementation of a "differ".

// A simple Façade, no explicit interface:

final class Differ
{
    public function generate(string $from, string $to): string
    {
        $funkyDiffer = new FunkyDiffer();

        // delegate to FunkyDiffer
    }
}

If you want to introduce an abstraction for multiple specific things

If you want to treat different, specific classes in some way that is the same for every one of them, you should introduce an interface that covers their common ground. Such an interface is often called an "abstraction", because it abstracts away the details that don't matter to the client of that interface. A nice example is the VoterInterface from the Symfony Security component. Every application has its own authorization logic, but Symfony’s AccessDecisionManager doesn't care about the exact rules. It can deal with any voter you write, as long as it implements VoterInterface and works according to the instructions provided by the documentation of that interface. An example of such an implementation:

final class MySpecificVoter implements VoterInterface
{
    public function vote(
        TokenInterface $token, 
        $subject, 
        array $attributes
    ): int {
        // ...
    } 
}

In the case of the VoterInterface, the package maintainers serve the users of their package by offering them a way to provide their own authorization rules. But sometimes an abstraction is only there for the code in the package itself. In that case too, don’t hesitate to add it.

If you foresee that the user wants to replace part of the object hierarchy

In most cases, a final class is the best thing you can create. If a user doesn't like your class, they can simply choose not to use it. However, if you're building up a hierarchy of objects you should introduce an interface for every class. That way the user can replace a particular piece of logic somewhere in that hierarchy with their own logic. It will make your code useful in as many situations as possible.

A nice example comes from Tactician, which offers a command bus implementation.

The package ships with a CommandBus class. It's a class, because its implicit interface isn't larger than its explicit interface would be - the only public method is handle().

class CommandBus
{
    // ...

    public function __construct(array $middleware)
    {
        // ...
    }

    public function handle($command)
    {
        // ...
    }

    // ...
}

To set up a working CommandBus instance, you need to instantiate a number of "middleware" classes, which all implement the Middleware interface:

interface Middleware
{
    public function execute($command, callable $next);
}

This is an example of an interface that was introduced as an abstraction, allowing the package maintainer to treat multiple specific things in some generic way, as well as to allow users to plug in their own specific implementations.

One of those middlewares is the CommandHandlerMiddleware, which itself needs a "command name extractor", a "handler locator" and a "method name inflector". All of which have a default implementation inside the package (the command name is the class name, the handler for a command is kept in memory, the handle method is "handle" plus the name of the command):

$handlerMiddleware = new CommandHandlerMiddleware(
    new ClassNameExtractor(),
    new InMemoryLocator([...]),
    new HandleClassNameInflector()
);

$commandBus = new CommandBus(
    [
        // ...,
        $handlerMiddleware,
        // ...
    ]
);

Each collaborating object that gets injected into CommandHandlerMiddleware can easily be replaced by re-implementing the interfaces of these objects (CommandNameExtractor, HandlerLocator and MethodNameInflector respectively). Because CommandHandlerMiddleware depends on interfaces, not on concrete classes, it will remain useful for its users, even if they want to replace part of the built-in logic with their own logic. For example when they would like to use their favorite service locator to retrieve the command handler from.

By the way, adding an interface for those collaborating objects also helps the user to decorate existing implementations of the interface by using object composition.

For everything else: stick to a final class

If your situation doesn't match any of the ones described above, most likely the best thing you can do is not to add an interface, and just stick to using a class, preferably a final class. The advantage of marking a class as "final" is that subclassing is no longer an officially supported way of modifying the behavior of a class. This saves you from a lot of trouble later on when you're changing that class: you won’t have to worry about users who rely on your class's internals in some unexpected way. This advice applies to both package and application developers by the way.

Classes that almost never need an interface are:

  • Classes that model some concept from your domain.
  • Classes that otherwise represent stateful objects (as opposed to classes that represent stateless services).
  • Classes that represent a particular piece of business logic, or a calculation.

What these types of classes have in common is that it's not at all needed nor desirable to swap their implementations out. More specifically, some good examples of these classes are:

  • Entities
  • Value objects
  • Domain services
  • Application services

The reason is that these are classes from the domain or application layer. A domain model should model parts of your business domain, and it doesn't make sense to prepare for certain elements of that model to be replaced. Making things as concrete and specific as possible is usually good advice in this realm. As for application services: they reflect the use cases of the application these services belong to. It would be weird if you'd aim for replaceability and extensibility of these classes. They are unique to this application. So again: let them be plain old classes.

PHP object design interfaces
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Aschwin Wesselius

Hello Matt, nice read. There is still so much to learn and to adapt about interfaces (or contracts).

 

I’m just curious if you have anything to say about implementing multiple interfaces in a class.

Some people would say each interface would represent a facet of the behavior of the objects. One object can have multiple behaviors and still be responsible for just one thing, keeping the SRP applied.

 

What do you think?

And for the size, are huge interfaces not a clear sign of bad abstraction? Any thoughts?

Maybe you can have a look at this video and let us know what you think.

https://youtu.be/JPPByGt8Zqs

Kind regards,

Aschwin Wesselius

Jerome Gill

Candidates for just being a final class are fairly broad, but obviously cannot be mocked without some hacky workaround (https://github.com/dg/bypas...

Do you feel the workaround is justified or do you feel that there is some domain model issue causing you to even want to inject these classes in tests?

Matthias Noback

It's a matter of the "testing school" you belong to, but if a class is final, it should not be mocked. If it needs to be mocked because it does some IO, it should have an interface that you can mock instead.

Hello Matt, great article, as usual, it gave me some insights about designs decisions to take in a project I'm currently working on.

However, I have a little doubt, in the adapter example you're injecting a concrete class in the constructor, so I assume that class exists in the service container besides not having an abstract interface attached to it. Assuming the class FunkyDiffer exists in the container (or not, isn't important), how am I supposed to test the adapter class if I can't swap the FunkyDiffer implementation?

In this specific case is not a big deal because the FunkyDiffer just make a set of calculations, but let's suppose I have an APIClient as dependency and I can't swap it because it does not implement and interface (spoiler alert: that's exactly my case IRL), am I misusing/misunderstanding the adapter pattern or exist a better option?

Thanks in advance!

Matthias Noback

The point is exactly that: if a dependency just provides some data without relying on IO, you can use it in your test (instantiate it as it is). Otherwise, you'd make sure it has an interface and create a test double for it (like in the case of the API client).

Maarten Troonbeeckx

Been learning a lot lately reading your books and articles :)
I totally get the reasoning behind the AuthenticationManager using an UserDataProvider.
But which design pattern(s) or SOLID principle(s) are applied here exactly?
Is this the Strategy pattern?

Matthias Noback

Thanks, awesome!
Switching from HttpClient to UserDataProvider is an example of the Dependency Inversion Principle being applied.

Maarten Troonbeeckx

Ok, clear, tnx!

But I was looking at how UserDataProvider behaves towards AuthenticationManager.
Would you say that is an application of the Strategy Pattern?

Matthias Noback

Never thought of it like that, but yes :)

Pavel Bisse

Nice article.
While reading this one I had a feeling you are describing my daily concept thinking.

@lakiboy:disqus
-> You implement LocationResolver and inject it into listener.
+ 1 :)

Matthias Noback

Cool, thanks for letting me know.

Dmitri Lakachauskis

Nice one as usual.

The disease of making interfaces for entities in Symfony community is spread everywhere. Take any FOS bundle: UserInterface, GroupInterface, CommentInterface. It provides bad examples for beginner developers.

Btw domain service is also a good candidate for the interface. The typical scenario for me is to inject some domain service in domain listener e.g. resolve location (lat/lng) for the entity. You don't want to rely on Geocoder for example. You implement LocationResolver and inject it into listener.

Carlos Marques

Do you believe that entities should never have interfaces? I mean, in FOSUserBundle it's clear that something isn't right but, what if the project isn't a bundle or library? Let's say we are dealing with an internal project. We have the entities Post and Author. If it's necessary to manage multiple Posts or Authors (e.g. to list them), wouldn't be acceptable to create an EntityInterface and use it in a Collection class, so it could handle collections of both Posts and Authors?

E.g.

class Post implements EntityInterface {}
class Author implements EntityInterface {}

class Collection extends ArrayObject {
public function append(EntityInterface $entity) {
$this->append($activity);
}
}

$post = new Post();
$posts = new Collection();
$posts->append($post);

$author = new Author();
$authors = new Collection();
$authors->append($author);

Dmitri Lakachauskis

In my opinion the legit case for entity to implement an interface is to denote some feature i.e. Post implements Publishable. If we apply the old-school rule "is-a" / "has-a" to your example, then Post is an entity, hence interface is useless, but post can have a feature i.e. be Publishable.

Matthias Noback

Thanks!
Well, I didn't know that, but it's not smart for that popular bundle to set this example.
I agree about the domain service, but not all of them - some will just do some work/calculations based on one or more aggregates. Your example is spot on: the clue is that you need some infrastructure class to do the real work, but you need the representation of the result and the action of resolving it in your domain/application layer. So that definitely requires an interface.