Subscribe to PHP Freaks RSS

When to add an interface to a class

syndicated from planet-php.net on August 28, 2018

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

Truncated by Planet PHP, read more at the original (another 12564 bytes)