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 more interfaces to their classes, to make the overall design of the package or application more flexible. However, not every needs an , and not every makes sense. I thought it would be useful to enumerate some good reasons for adding an 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.



Source link https://matthiasnoback.nl/2018/08/when-to-add-an-interface-to-a-class

LEAVE A REPLY

Please enter your comment!
Please enter your name here