Designing object-oriented program means creating interactions between involving objects and that usually create dependencies among the interacting objects. The dependencies could sometimes lead to designing application which is hard to maintain and extend. In this article, we would look at designing loosely-coupled programs with the help of dependency injection.
Introduction
The diagram above is a typical monolith application that fetches data from a data source and renders that to a certain user interface, in this case, a web browser. First, the application is structured to contain View
-- the actual user interface elements, in this case, a list of items displayed in the browser. It also has a Presentation
layer that contains logic for driving our UI. Here, anytime the web browser loads, a method call is made to retrieve all people and populate the browser with such data. Then we have our Repository layer
, here it is called PersonRepository and this is a repository that's responsible for interacting with Service which could be a data store. And the bottom layer is the Service
layer, helps to provide data for the application, it can database, API or any data store. Currently, the application service uses a hardcoded array of data.
Let's start diving a little bit deeper into each of these layers.
Shared Object
<?php
namespace testapp\sharedObjects;
final class Person
{
public $name;
public $gender;
public $age;
public function __construct(string $name, string $gender, string $age)
{
$this->name = $name;
$this->gender = $gender;
$this->age = $age;
}
}
Above is a sample object that could be considered as a data transfer object as it provides a structure of how a Person Entity should be and shared across various layers. In the end, it makes it easier to model either incoming data from the outside world into a structure the application can work with or create an entity that could be persisted in the application. It's a very simple class with three properties - a name, gender, and age.
NB These properties could be value objects instead of primitives but for brevity, we would maintain the primitive types.
Service Layer
<?php
namespace testapp\services;
use testapp\sharedObjects\Person;
final class PersonService
{
public function getPeople(): array
{
return [
new Person("Oliver Mensah", "Male", 27),
new Person("Olivia Ennin", "Female", 22),
];
}
}
The service has a method that creates an entity object and eventually persists it. It also has a method to return the available entities.
Repository Layer
<?php
namespace testapp\repositories;
use testapp\services\PersonService;
final class PersonRepository
{
public function __construct()
{
$this->personService = new PersonService();
}
public function getPeople(): array
{
return $this->personService->getPeople();
}
}
The repository helps to interact with the service. Here it reads the data and then presents the results to the presentation layer.
Presentation Layer
<?php
namespace testapp\presentation;
use testapp\repositories\PersonRepository;
final class UserModel
{
public function __construct()
{
$this->personRepository = new PersonRepository();
}
public function getPeople(): array
{
return $this->personRepository->getPeople();
}
}
Here, we have the presentation layer that contains all of the presentation logic for driving our UI.
View Layer
<?php foreach ($people as $person): ?>
<li><?php echo $person->name?></li>
<?php endforeach;?>
Controller
use testapp\views\Template;
use testapp\presentation\UserModel;
$userModel = new UserModel();
$indexPage = new Template("./testapp/views/pages/index.php");
$indexPage->people = $userModel->getPeople();
echo $indexPage;
This accepts the incoming request and calls a method from the presentation layer to render the UI by basically listing users' name in the browser.
The problem of Tight Coupling
The above structure might have a pretty good design since we have good separation of concerns between our layers. But if you take a closer look, the repository right now has a direct reference to the service. Anytime, a repository class is instantiated, the service is automatically newed up. This means, anytime we need up the PersonRepository the data should come from the PersonService. This makes the repository tightly coupled to the repository to the service thereby taking the responsibility for creating and managing the lifetime of the service.
The presentation layer which drives the UI too is tightly-coupled to the Repository and this is where the real issue comes in. Indirectly, it makes the View tightly coupled to the service as well. And in that case, if you want to have different service say load data from a CSV file or get data from an API, the Presentation layer now takes the responsibility to know which repository is needed before making a call to the such a service. In code this would look as shown below;
//.. namespace and imports here
final class UserModel
{
public function __construct()
{
switch($service){
case 'raw': $this->service = new PersonRepository();
break;
case 'csv': $this->service = new PersonCSVRepository();
break;
case 'sql': $this->service = new PersonSQLRepository();
break;
}
}
public function getPeople(): array
{
return $this->service->getPeople();
}
}
This is really painful, the presentation is not meant to do such work. Its responsibility is to send UI actions to data storage through a repository.
We have gotten to know that our application has a tightly-coupled code that When the application wants to switch to a different data source, the cyclomatic complexity increases. ANd that is what should be avoided.
To make sure each layer performs its single responsibility, "someone" else must take up the responsibility of knowing what should be done at a certain part of the application. And Dependency Injection comes can help in achieving that. Next, we will look at Dependency Injection, what it is and how that can help in creating loosely-coupled code.
Dependency Injection(DI) to the Rescue
When you google for the meaning of dependency injection, there are so many definitions but to keep it pretty much simple, it is a set of software design principles and patterns that enable software practitioners like yourself to develop loosely-coupled code. And below are the various means that dependency injection can be implemented;
- Constructor injection
- Property injection
- Method injection
- Ambient context
- Service locator
In our case, we can apply constructor injection to resolve most of the issue since the tight coupling happens in most of the class constructors. But the question then becomes which of the services should be injected? And currently, they are PersonRepository, PersonCSVRepository, and PersonSQLRepository. And there could be other services in the future. So instead of injecting specific class implementations like;
//.. namespace and imports here
final class UserModel
{
public function __construct(PersonRepository $personRepo,PersonCSVRepository $csvRepo, ... )
{
switch($service){
case 'raw': $this->service = $personRepo;
break;
case 'csv': $this->service = $csvRepo;
break;
case 'sql': $this->service = $sqlRepo;
break;
}
}
// ...other methods
}
Even though the responsibility is shifted from the presentation layer, however, the application is limited in terms of extending to other services. In this case, injecting an interface that establishes class contracts can help solve the issue.
Interfacing the Repositories;
An Interface provides solutions to a lot of the problems that we've been looking at up to this point. Conceptually, an interface is a contract that defines a set of methods that have to be implemented by whatever class implements the interface. It's a formalization of a general design term called a class contract, though the idea of a class contract encompasses not just the set of methods, but also the behavior of those methods. In other words, classes that implement the contract have to behave in certain ways, not just implement a set of functions and in this case, talk to various services in the application.
Defining the Interface
For now, most of the repositories just retrieve information from the service and populate it to the UI. We could just work with that functionality or add more to the interface.
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
interface IPerson
{
public function getPeople(): array;
public function addPerson(Person $newPerson);
}
Working with the Interface
We can now just inject the interface and any service that the application needs can be worked with once its repository implements the defined interface.
<?php
namespace testapp\presentation;
// ...imports here
final class UserModel
{
public function __construct(IPerson $repository)
{
$this->repository = $repository;
}
public function getPeople(): array
{
return $this->repository->getPeople();
}
}
Defining class contracts with Interface
With the interface defined, all you need to do is to allow your repositories that interact with the service implements it, that's the interface. Now, the repositories implementation would look like below;
<?php
namespace testapp\repositories;
use testapp\services\PersonService;
use testapp\sharedObjects\Person;
final class PersonRepository implements IPerson
{
public function __construct()
{
$this->personService = new PersonService();
}
public function getPeople(): array
{
return $this->personService->getPeople();
}
public function addPerson(Person $person)
{
}
}
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
final class PersonCSVRepository implements IPerson
{
public function __construct()
{
$this->csvService = new CSVService();
}
public function getPeople(): array
{
return $this->csvService->getPeople();
}
public function addPerson(Person $person)
{
}
}
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
final class PersonSQLRepository implements IPerson
{
public function __construct()
{
$this->sqlService = new SQLService();
}
public function getPeople(): array
{
return $this->sqlService->getPeople();
}
public function addPerson(Person $person)
{
}
}
We can implement as many repositories depending on our services. We could also see the services are also tightly coupled to the repositories and this is much of an issue when you want to test the code. During testing, you may not get the actual service running hence mocking it would be the best option.
Ease Code Testing by Mocking Services
To mock a service, dependency injection plays a key role. And henceforth, the repositories can now work with services through injection as shown below;
<?php
namespace testapp\repositories;
use testapp\services\PersonService;
use testapp\sharedObjects\Person;
final class PersonRepository implements IPerson
{
public function __construct(PersonService $service)
{
$this->personService = $service;
}
public function getPeople(): array
{
return $this->personService->getPeople();
}
public function addPerson(Person $person)
{
}
}
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
final class PersonCSVRepository implements IPerson
{
public function __construct(CSVService $service)
{
$this->csvService = $service;
}
public function getPeople(): array
{
return $this->csvService->getPeople();
}
public function addPerson(Person $person)
{
}
}
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
final class PersonSQLRepository implements IPerson
{
public function __construct(SQLService $service)
{
$this->sqlService = $service;
}
public function getPeople(): array
{
return $this->sqlService->getPeople();
}
public function addPerson(Person $person)
{
}
}
With these changes, we can easily mock any services during unit testing of the code.
You can use any kind of tool out there to test your code. In this case, Mockery is used.
<?php
use PHPUnit\Framework\TestCase;
use testapp\presentation\UserModel;
use testapp\sharedObjects\Person;
class UserModelTest extends TestCase
{
public function testGetPeople()
{
$repository = \Mockery::mock('testapp\repositories\IPerson');
$repository->shouldReceive('getPeople')
->once()
->andReturn([new Person("Oliver Mensah", "Male", 24), new Person("Geddy Addo", "Female", 21)]);
$userModel = new UserModel($repository);
// Verify
$this->assertEquals([new Person("Oliver Mensah", "Male", 24), new Person("Geddy Addo", "Female", 21)], $userModel->getPeople());
}
}
In this case, we are just testing the getPeople
method by mocking our service.
Conclusion
Here, we looked at applying dependency injection rule, a good principle in writing effective production-quality object-oriented systems. We familiarize ourselves on how to structure code to be highly maintainable, extensible and easy to modify by avoiding common pitfalls. And thus below should be the common takeaways from this little piece of work;
- Layering of Application
- The problem of Tight Coupling
- Dependency Injection(DI) to the Rescue
- Interfacing the Repositories
- Ease Code Testing by Mocking Services
I hope you enjoyed as I enjoyed putting this together.
The sample codes can be found here
Further Reading
Matthias Noback's description is one of the best in-depth discussion of Style Guide for Object Design from a practical design principle perspective.
Keep In Touch
Let's keep in touch as we continue to learn and explore more about software engineering. Don't forget to connect with me on Twitter or LinkedIn
Top comments (0)