DEV Community

Fran Iglesias
Fran Iglesias

Posted on • Edited on

Dependencies and coupling

How to apply the Dependency Inversion Principle, understanding what dependency and coupling are. And how to know when we need to use injection or instantiation when creating objects.

In programming we use to say that a dependency is established when a software module utilizes another to complete its work. If we talk about classes, we say that one class (Client) has a dependency on another one (Service) when Client uses Service to get their own responsibilities done. Dependency is shown because Client class cannot work without Service class.

Software dependencies are not bad by themselves (judging by how they talk about them in some posts makes it seems so). The problem with dependencies is a level problem. You know, there are strong dependencies and light dependencies. The key here is how we manage them to make them to be the lightest possible.

In fact, the existence of dependencies is a good hint because it could be pointing to the fact that we are honoring the Single Responsibility Principle, making some classes delegate on others the tasks out of their scope.

The level of dependency between software units is called coupling. We say that there is a thigh coupling when we have to rewrite the Client class if we want to replace Service class with another one. This is a violation of the Open/Closed Principle. On the contrary, we say that there is loose (low) coupling when we can replace Service class with another, without having to touch Client class.

How can we do that? By using three tools:

  • The Dependency Injection Pattern
  • The D in SOLID: Dependency Inversion Principle
  • The Adapter Pattern

Hidden dependencies

Let's start with the worst situation: class Client uses class Service without nobody can guess. Let's see an example:

class Client {
    private $service;

    public function __construct() {
        $this->service = new Service();
    }
    public function doSomething() {
        $this->service->doTask();
    }
}

class Service {
    public function doTask() {
        echo 'Performed by Service';
    }
}


$Client = new Client();
$Client->doSomething();

To know that Client uses Service we should study the source code because we can't see anything from its public interface.

Here is where the Open/Closed Principle violation happens. Being constructed this way, the class Client is open to modification and to change its behavior we have to rewrite it.

In this case the coupling is maximum and when we have to touch Service by any cause, Client functionality could break. For example, imagine that Service is a class from a third party package or library and authors decide to perform a major upgrade and change the interface of the methods used by Client. Even having total access to the code in PHP you could imagine what nightmare would be the maintenance and the risk that this involves. In fact, you must to stay in a fixed version of Service and forget about upgrading.

To manage with this problem you have an easy solution as is to apply the Dependency Injection Pattern:

class Client {
    private $service;

    public function __construct(Service $service) {
        $this->service = $service;
    }
    public function doSomething() {
        $this->service->doTask();
    }
}

class Service {
    public function doTask() {
        echo 'Performed by Service';
    }
}


$Client = new Client(new Service());
$Client->doSomething();

Simply put, the Dependency Injection Pattern consists of injecting the dependency through the constructor (or a setter). Now, dependency becomes visible. There is thigh coupling, yet, but now we have more freedom, because we know how classes are related.

Dependency Inversion

Dependency Inversion is the road to follow to reduce coupling between classes or modules to the minimum. Dependency Inversion Principle states that:

  • High level modules should not depend on low level modules. Both should depend on abstractions.
  • Abstractions should not depend on details, details should depend on abstractions.

To sum up: every dependency should happen on abstractions, not on specific implementations.

In our example, dependency is now explicit, what is good, but Client keeps depending on a concrete implementation of Service, and that is bad.

To invert the dependency we should do the following:

Client should not wait for a concrete instance of Service, instead, it should wait for a class that meets certain conditions, that's the same as: it should honor a contract. And, as we know, a contract in programming is an interface. And an interface is the most abstract thing that we can get in software.

Service, on the other hand, should honor the interface to be allowed to be used by Client, that's it: it should depend on that abstraction.

So, we need to create an interface and make that Client waits for any class that implements it. How can I define the interface? You should start from the needs or interests of Client, and Service has to accept that.

interface ServiceInterface {
    public function doTheThing();
}

class Client {
    private $service;

    public function __construct(ServiceInterface $service) {
        $this->service = $service;
    }
    public function doSomething() {
        $this->service->doTheThing();
    }
}

class Service {
    public function doTask() {
        echo 'Performed by Service';
    }
}


$Client = new Client(new Service());
$Client->doSomething();

The code shown will not work yet: Service does not implement ServiceInterface, so Client will not accept it.

Why have I changed the way Client uses Service? Why have I changed the method that Client calls? Simply, because I want to illustrate the way that interface should be designed starting from the needs of the Client and also to show how we can make that Service meets that interface without changing its code.

In the code, Client depends on ServiceInterface, that means that it waits a class that implements a method doTheThing(). Nevertheless, Service doesn't have this method. To solve this we must both to modify the class Service, to implement ServiceInterface, or to apply the Adapter pattern to use the class Service honoring ServiceInterface.

An adapter is a class that implements an interface using another class, so we are going to include the adapter in our code:

interface ServiceInterface {
    public function doTheThing();
}

class Client {
    private $service;

    public function __construct(ServiceInterface $service) {
        $this->service = $service;
    }
    public function doSomething() {
        $this->service->doTheThing();
    }
}

// We don't touch Service

class Service {
    public function doTask() {
        echo 'Performed by Service';
    }
}

// We create an adapter

class ServiceAdapter implements ServiceInterface {
    private $service;

    public function __construct(Service $service)
    {
        $this->service = $service;
    }

    public function doTheThing()
    {
        $this->service->doTask();
    }
}

$Client = new Client(new ServiceAdapter(new Service()));
$Client->doSomething();

The call has become a little more complicated, but benefits are huge, because we have reduced the coupling to the lower.

From now, classes Client and Service can change and evolve separately one from the other given the condition that the interface doesn't change (and interfaces are meant to be stable in time except very important motivations). If it becomes necessary we'll have to modify ServiceAdapter in the case that Service changed its interface.

In the following example, we imagine that Service has suffered a change that breaks backwards compatibility:

interface ServiceInterface {
    public function doTheThing();
}

class Client {
    private $service;

    public function __construct(ServiceInterface $service) {
        $this->service = $service;
    }
    public function doSomething() {
        $this->service->doTheThing();
    }
}


// Service has change its public interface

class Service {
    public function doService() {
        echo 'Performed by Service';
    }
}

// We change Adapter according to the changes in Service

class ServiceAdapter implements ServiceInterface {
    private $service;

    public function __construct(Service $service)
    {
        $this->service = $service;
    }

    // We need to change the way the adapter uses Service

    public function doTheThing()
    {
        $this->service->doService();
    }
}


$Client = new Client(new ServiceAdapter(new Service()));
$Client->doSomething();

More: we could replace Service with any other class that implements the interface ServiceInterface by itself or by means of an adapter. This way, we can modify the behavior of Client without touching its code, honoring the Open/Closed Principle.

Now, we add a new class that can replace Service:

interface ServiceInterface {
    public function doTheThing();
}

class Client {
    private $service;

    public function __construct(ServiceInterface $service) {
        $this->service = $service;
    }
    public function doSomething() {
        $this->service->doTheThing();
    }
}

class Service {
    public function doService() {
        echo 'Performed by Service';
    }
}

class ServiceAdapter implements ServiceInterface {
    private $service;

    public function __construct(Service $service)
    {
        $this->service = $service;
    }

    public function doTheThing()
    {
        $this->service->doService();
    }
}

class NewService {
    public function theMethod()
    {
        echo 'Performed by New Service';
    }
}

class NewServiceAdapter implements ServiceInterface {
    private $service;

    public function __construct(NewService $service)
    {
        $this->service = $service;
    }

    public function doTheThing()
    {
        $this->service->theMethod();
    }

}

$Client = new Client(new ServiceAdapter(new Service()));
$Client->doSomething();
echo chr(10);

$Client2 = new Client(new NewServiceAdapter(new NewService()));
$Client2->doSomething();

About the adapters

There is a thigh coupling between adapters and adapted classes. Obviously, we cannot uncouple them. This coupling is not a problem because, from the point of view of our Client, the Adapter "is" the Service, and it is no worried by how it is implemented given the interface is honored. Plus, the Adapter is a class with trivial code, it limits itself to translate the messages between the Client and the Service.

This could lead us to think in hiding the dependency and make it implicit, as a way to save a bit of code when instantiating the adapter. But that's not a good idea. All dependencies should be explicit.

 When dependency injection becomes complex

As you can see in the previous examples, the decoupling increases complexity a bit at the time to instantiate the Client class. If this has several dependencies, every of which could use their own adapters, the dependency injection becomes tedious, but not complicated. Nonetheless, that means that you need to repeat a good chunk of code in different parts of the application to instantiate a certain object.

The solution for that is to use some of the several creational patterns such as factories, builders, pools or prototypes, that automate that instantiation.

Another solution is a, so called, Dependency Injection Container (DIC) that is, no more no less, a class with the responsibility to provide us with fully assembled object. DIC's make use of several creational patterns so we can register the way in which objects should be instantiated.

Not always uncoupling is a must

Coupling should be considered in the context of the behavior of the Client class. This uses a behavior of the Service class so it could complete their own behavior.

This is not the same in all cases. Take, for example, Value Objects. These don't contribute to the behavior in the same way. Value Objects are used in a similar way as primitive types and their behavior is intended to provide service to the class protecting their own invariants. Value Objects are immutable and don't have external dependencies or, if they do, they are on another value objects. So, these objects can be instantiated with a simple new, or a named constructor if provided.

So, we can make a distinction between "newable objects" and "injectable objects". Miško Hevery explains it very well. To sum up:

Newable objects are those that need some variable parameter at creation time. In fact, we will need a new object every time. Imagine an Email class that represents an email address or even a message (it doesn't send it, only represents it, but also has the ability to validate it and ensure it is well crafted). We will need a different object for every email address or message. See the example:

class Email {
    private $address;

    function __construct($address) {
        if (filter_var($address, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException("$address is not a valid email");
        }
        $this->address = $address;
    }

    public function getAdress()
    {
        return $this->address;
    }

}

$myEmail = new Email('franiglesias@mac.com');
$otherEmail = new Email('other@example.com');

Newable objects cannot be injected because the Dependency Injection Container doesn't know what parameters each of the concrete instances will need. We could have a factory to pass the parameter to and it would return the object, but new, in this case, is a factory as good as any other. In fact, it could be the best fit in many cases.

Newables don't need explicit interfaces because you don't need a variety of options. A newable object could extend another by means of inheritance and, in this case, the base class will become "the interface" if we are respecting the Liskov Substitution Principle, so both classes will be interchangeable.

Injectable objects, on the other hand, are objects that can be constructed with the help of a DIC because they don't need parameters that change on every instantiation. They use to have interfaces because they should be replaceable, that's to say: we want the used classes to have loose coupling with them.

Injectables and newables must not be mixed. An injectable cannot be created with a newable, and also a newable cannot be constructed with an injectable (it becomes an injectable if so). This is not the same to say that one cannot use the others, but they should be passed as parameters in methods.

Top comments (0)