I have been mulling in my head for a while now about how I wanted to talk about this very common design pattern at work. Designing by contract is a very old idea in software and I have found myself having to explain it and teach it other developers at work. So hopefully in this post, I can elaborate on why using a contract is beneficial in the design and development of large applications.
This blog post is definitely not for a seasoned software developer - or at least should not be. Ideally, you're a new to intermediate developer and you're working with PHP and thinking "Why is my boss making me write all these interfaces?" and this blog post will give you a practical example of why its important to consider the interface, or contract, of different objects within your application.
Background
We got the opportunity at work to development a new service from scratch. This service would be replacing two other services by combining them, and ridding ourselves of some ancient (well in software terms) code. Its every developer's dream - we got to rewrite it from scratch! Well, not quite. We needed to maintain the legacy database that drove our entire production facility. Luckily for us, we are will begin the process of redesigning the schema for this database to be more relevant and accurate for our production facility as well as our reporting and metrics needs.
We decided that running an Express.js service with TypeScript was the right way to go as we had a lot of requirements around events and if there is one thing that Node.js is good at - its events. Although, this service at work was written in TypeScript, I am going to give the examples for what we did in PHP. Why? Because its my blog post. But, also, because I feel like this is a topic that isn't talked about often in the PHP world. The examples are also a bit more clear in PHP as I can make use of Namespacing to logically explain how the contracts work.
Finally, I am going to describe a little bit about the Domain of the service. There are two main models that will be talked about in this blog post - the Order
model and the Batch
model. An Order
is a customer purchase of some of our goods. A Batch
is a logical grouping of our Order
's.
The Contract
So we've got this old database, right? So we are going to write some Repository
classes that take in a DatabaseConnection
object that represents an active connection to a MySQL database. These classes will be responsible for actually getting the model data out of the database, and hydrating some models. So, with our goal of having a new database in the future, a legacy database in the now, and new software to write, I recommended we write some strong interfaces for the repositories.
Let's look at an example of the Order
model.
<?php
namespace Dave\Repository\Api;
use Dave\Model\Api\OrderInterface;
interface OrderRepositoryInterface
{
public function getOrder($id): OrderInterface;
public function updateOrder(OrderInterface $order): boolean;
public function saveOrder(OrderInterface $order): boolean;
}
So here we have a contract on what our OrderRepository
should do. Since we are implementing this interface against an old, legacy database, lets implement it in the following namespace.
<?php
namespace Dave\Repository\OldDatabase;
use Dave\Infrastructure\DatabaseConnection
use Dave\Repository\Api\OrderRepositoryInterface;
use Dave\Model\Api\OrderInterface;
class OrderRepository implements OrderRepositoryInterface
{
/**
* @var DatabaseConnection A Connection to the Old Database
*/
private $databaseConnection;
public function __construct(DatabaseConnection $oldConnection)
{
$this->databaseConnection = $oldConnection;
}
public function getOrder($id): OrderInterface
{
// do something with $this->databaseConnection, like craft a query
$rowData = $this->databaseConnection->query($query);
// and hydrate a model class like, `Order`
$order = new Order($rowData['order_number']);
return $order;
}
// ... implement the other methods
}
So above, we can write queries for directly getting the data we need out of the OldDatabase
. So far, the code must feel pretty redundant and not serve much purpose. This starts to change when we look at using Dependency Injection within the application.
Dependency Injection
Interfaces can play a huge role in Dependency Injection libraries, like PHP-DI. We can tell the DI Container "Hey, we have the shape of an OrderRepositoryInterface
in Dave\Repository\OldDatabase\OrderRepository
. And the DI Container says: "Awesome, thank you!" and goes on its way, injecting this concrete class wherever the OrderRepositoryInterface
is asked within the application.
Later on, when we've set up our new Dave\Repository\NewDatabase\OrderRepository
with out new database schema, we can tell the DI Container to use this concrete clas instead of our old OldDatabase
.
So, we can set up definition like this within our DI Container.
<?php
$container = new DI\Container();
$database = new DatabaseConnection();
use Dave\Infrastructure\OldDatabase\OrderRepository;
$orderRepository = new OrderRepository($database);
use Dave\Repository\Api\OrderRepositoryInterface;
$container->set(OrderRepositoryInterface::class, $orderRepository);
Ah, but now its the future and we have our new database schema. We simply need to implement a new OrderRepositoryInterface
.
<?php
namespace Dave\Repository\NewDatabase;
use Dave\Infrastructure\DatabaseConnection
use Dave\Repository\Api\OrderRepositoryInterface;
class OrderRepository implements OrderRepositoryInterface
{
/**
* @var DatabaseConnection A Connection to the New Database
*/
private $databaseConnection;
public function __construct(DatabaseConnection $newConnection)
{
$this->databaseConnection = $newConnection;
}
// ... implement the other methods
}
This definitely feels like a lot of code, but making sure you're using the OrderRepositoryInterface
throughout the codebase will allow us to simply change the implementation, and set the new Repository to the DI Container.
<?php
$container = new DI\Container();
$database = new DatabaseConnection();
// this line is the only real change
use Dave\Infrastructure\NewDatabase\OrderRepository;
$orderRepository = new OrderRepository($database);
use Dave\Repository\Api\OrderRepositoryInterface;
$container->set(OrderRepositoryInterface::class, $orderRepository);
Conclusion
I think this is a form of future-proofing that few younger developers think about. I'm sure for my team, after the interfaces were written, they felt it rather redundant to then write out the concrete classes that do the actual logic. It feels like a waste of time - until you need to actually use it. You may have noticed that I made use of the namespace Api
in the examples. Why? Well, API
stands for Appliction Programming Interface. I put all of the API for our Repositories (and models for that matter) into the Api
namespace to keep everything logically structured.