Introduction: Decoupling from infrastructure
Part I
Decoupling from infrastructure
This part covers:
- Decoupling your domain model from the database
- Decoupling the read model from the write model (and from the database)
- Extracting an application service from a controller
- Rewriting calls to service locators
- Splitting a call to external systems into the “what” and “how” of the call
- Inverting dependencies on system devices for retrieving the current time, and randomness
The main goal of the architectural style put forward in this book is to make a clear distinction between the core code of your application and the infrastructure code that supports it. This so-called infrastructure code connects the core logic of your application to its surrounding systems, like the database, the web server, the file system, and so on. Both types of code are equally important, but they shouldn’t live together in the same classes. The reasons for doing so will be discussed in detail in the conclusion of this part, but the quick summary is that separating core from infrastructure…
- provides a strong technical foundation for doing domain-first development, and
- enables a rich and effective set of testing possibilities, making test-first development easier
To help you develop an eye for the distinction between core and infrastructure concerns, each of the following chapters starts with some common examples of “mixed” code in a legacy web application. After pointing out the problems with this kind of code we take a number of refactoring steps to separate the core part from the infrastructure part. After six of these iterations you will have seen all the programming techniques that can save you from having mixed code in your classes.
But before we start refactoring and improving the code samples, let’s establish a definition of the terms “core” and “infrastructure” code. We’ll define core code by introducing two rules for it. Any other code that doesn’t follow the rules for core code should be considered infrastructure code.
1.1 Rule no 1: No dependencies on external systems
Let’s start with the first rule:
Core code doesn’t directly depend on external systems, nor does it depend on code written for interacting with a specific type of external system.
An external system is something that lives outside your application, like a database, some remote web service, the system’s clock, the file system, and so on. Core code should be able to run without these external dependencies. Listing 1.1 shows a number of class methods that don’t follow this first rule, and should therefore be considered infrastructure code. You can’t call any of these methods without their external dependencies being actually available.
Listing 1.1:
Examples of code that needs external dependencies to run.
final class NeedsExternalDependencies { public function callARemoteService(): void { /∗ ∗ To run this code, we need an internet connection, ∗ and the API of remoteservice.com should be responsive. ∗/ $ch = curl_init(’https://remoteservice.com/api’); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); // ... } public function useTheDatabase(): void { /∗ ∗ To run this code, the database that we connect to ∗ using ‘new PDO(’...’)‘ should be up and running, and ∗ it should contain a table called ‘orders‘. ∗/ $pdo = new PDO(’...’); $statement = $pdo−>prepare(’INSERT INTO orders ...’); $statement−>execute(); } public function loadAFile(): string { /∗ ∗ To run this code, the ‘settings.xml‘ file should exist ∗ in the correct location. ∗/ return file_get_contents( __DIR__ . ’/../app/config/settings.xml’ ); } }
When code follows the first rule, it means you can run it in complete isolation. Isolation is great for testability. When you want to write an automated test for core code, it will be very easy. You won’t need to set up a database, create tables, load fixtures, etc. You won’t need an internet connection, or a hard disk with files on it in specific locations. All you need is to be able to run the code, and have some computer memory available.
1.2 Abstraction
What about the registerUser() method in Listing 1.2? Is it also infrastructure code?
Listing 1.2:
Depending on an interface.
interface Connection { public function insert(string $table, array $data): void; } final class UserRegistration { /∗∗ ∗ @var Connection ∗/ private Connection $connection; public function __construct(Connection $connection) { $this−>connection = $connection; } public function registerUser( string $username, string $plainTextPassword ): void { $this−>connection−>insert( ’users’, [ ’username’ => $username, ’password’ => $plainTextPassword ] ); } }
The registerUser() method doesn’t use PDO 3 directly to connect to a database and start running queries against it. Instead, it uses an abstraction for database connections (the Connection interface). This means that the Connection object that gets injected as a constructor argument could be replaced by a simpler implementation of that same interface which doesn’t actually need a database (see Listing 1.3).
Listing 1.3:
An implementation of Connection that doesn’t need a database.
final class ConnectionDummy implements Connection { /∗∗ ∗ @var array<array<string,mixed>> ∗/ private array $records; /∗∗ ∗ @param array<string,mixed> $data ∗/ public function insert(string $table, array $data): void { $this−>records[$table][] = $data; } }
This makes it possible to run the code in that registerUser() method, without the need for the actual database to be up and running. Does that make this code core code? No, because the Connection interface is specifically designed to communicate with relational databases, as the insert() method signature itself reveals. So although the registerUser() method doesn’t directly depend on an external system, it does depend on code written for interacting with a specific type of external system. This means that the code in Listing 1.2 is not core code, but infrastructure code.
In general though, abstraction is the go-to solution to get rid of dependencies on external systems. We’ll discuss several examples of abstraction in the next chapters, but it might be useful to give you the summary here. Creating a complete abstraction for services that rely on external systems consists of two steps:
- Introduce an interface
- Communicate purpose instead of implementation details
As an example: instead of a Connection interface and an insert() method, which only makes sense in the context of dealing with relational databases, we could define a Repository interface, with a save() method instead. Such an interface communicates purpose (saving objects) instead of implementation details (storing data in tables). We’ll discuss the details of this type of refactoring in Chapter 2.
1.3 Rule no 2: No special context needed
The second rule for core code is:
Core code doesn’t need a specific environment to run in, nor does it have dependencies that are designed to run in a specific context only.
Listing 1.4 shows some examples of code that requires special context before you can run it. It assumes certain things have been set up, or that it runs inside a specific type of application, like a web or a command-line (CLI) application.
Listing 1.4:
Examples of code that needs a special context to run in.
final class RequiresASpecialContext { public function usesGlobalState(): void { /∗ ∗ Here we rely on global state, and we assume this ∗ method gets executed as part of an HTTP request. ∗/ $host = $_SERVER[’HTTP_HOST’]; // ... } public function usesAStaticServiceLocator(): void { /∗ ∗ Here we rely on ‘Zend_Registry‘ to have been ∗ configured before calling this method. ∗/ $translator = Zend_Registry::get(’Zend_Translator’); // ... } public function onlyWorksAtTheCommandLine(): void { /∗ ∗ Here we rely on ‘php_sapi_name()‘ to return a specific ∗ value. Only when this application has been started from ∗ the command line will this function return ’cli’. ∗/ if (php_sapi_name() !== ’cli’) { return; } // ... } }
Some code could in theory run in any environment, but in practice it will be awkward to do so. Consider the example in Listing 1.5. The OrderController could be instantiated in any context, and it would be relatively easy to call the action method and pass it an instance of RequestInterface. However, it’s clear that this code has been designed to run in a very specific environment only, namely a web application.
Listing 1.5:
Code that is designed to run in a web application.
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; final class OrderController { public function createOrderAction( RequestInterface $request ): ResponseInterface { // ... } }
Only if code doesn’t require a special context, and also hasn’t been designed to run in a special context or has dependencies for which this is the case, can it be considered core code.
Listing 1.6 shows several examples of core code. These classes can be instantiated anywhere, and any client should be able to call any of the available methods. None of these methods depend on anything outside the application itself.
Listing 1.6:
Some examples of core code.
/∗ ∗ This is a proper abstraction for an object that talks to the database: ∗/ interface MemberRepository { public function save(Member $member): void; } final class MemberService { private MemberRepository $memberRepository; public function requestAccess( string $emailAddress, string $purchaseId ): void { $member = Member::requestAccess( EmailAddress::fromString($emailAddress), PurchaseId::fromString($purchaseId) ); $this−>memberRepository−>save($member); } } final class EmailAddress { private string $emailAddress; private function __construct(string $emailAddress) { if (!filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException(’...’); } $this−>emailAddress = $emailAddress; } public static function fromString(string $emailAddress): self { return new self($emailAddress); } } final class Member { public static function requestAccess( EmailAddress $emailAddress, PurchaseId $purchaseId ): self { // ... } }
Not having to create a special context for code to run in is, again, great for testability. The only thing you have to do in a test scenario is instantiate the class and call a method on it. But following the rules for core code isn’t just great for testing. It also helps keeping your core code protected against all kinds of external changes, like a major framework upgrade, a switch to a different database vendor, etc.
It’s not a coincidence that the classes in this example are domain-oriented. In Chapter 12 we will discuss architectural layering and define rules for the Domain and Application layers which naturally align with the rules for core and infrastructure code. In short: all of the domain code and the application’s use cases should be core code, and not rely on or be coupled to surrounding infrastructure.
This also explains why I’m using the words “core” and “infrastructure”. Infrastructure is a common term used to describe the technical aspects of an interaction. In a web application, infrastructure supports the communication between your application and the outside world. The core is the center of your application, the infrastructure is around it, both protecting the core and connecting it to external systems and users (Figure 1.1).
Figure 1.1:
Connecting the core to external systems and users through infrastructure
“Is all code in my vendor directory infrastructure code?”
Great question. In /vendor you’ll find your web framework, which facilitates communication with browsers and external systems using HTTP. You’ll also find the ORM, which facilitates communication with the database, and helps you save your objects in tables. All of this code doesn’t comply with the definition of core code provided in this chapter. To run this code, you usually need external systems like the database or the web server to be available. The code has been designed to run in a specific context, like the terminal, or as part of a web request/response cycle. So most of the code in /vendor should be considered infrastructure code.
However, being in a particular directory doesn’t determine whether or not something is infrastructure code. The rules don’t say anything about that. What matters is what the code does, and what it needs to do that. This means that some, or maybe a lot of the code in /vendor could be considered core code after all, even though it’s not written by you or for your application specifically.
1.4 Summary
Throughout this book we make a distinction between core and infrastructure code, which will be the foundation of some architectural decisions later on. Core code is code that can be executed in any context, without any special setup, or external systems that need to be available. For infrastructure code the opposite is the case: it needs external systems, special setup, or is designed to run in a specific context only.
In the next chapters we’ll look at how to refactor mixed code into properly separated core and infrastructure code which follows the rules provided in this chapter.
Exercises
1. Should the code below be considered infrastructure code? 4
$now = new DateTimeImmutable(’now’); $expirationDate = $now−>modify(’+2 days’); $membershipRequest = new MembershipRequest($expirationDate);
2. Should the code below be considered infrastructure code? 5
namespace Symfony\Component\EventDispatcher; class EventDispatcher implements EventDispatcherInterface { // ... public function dispatch( object $event, string $eventName = null ): object { $eventName = $eventName ?? get_class($event); // ... if ($listeners) { $this−>callListeners($listeners, $eventName, $event); } return $event; } // ... }
3. Should the code below be considered core code? 6
interface HttpClient { public function get(string $url): Response; } final class Importer { private HttpClient $httpClient; public function __construct(HttpClient $httpClient) { $this−>httpClient = $httpClient; } public function importPurchasesFromLeanpub(): void { $response = $this−>httpClient−>get( ’https://leanpub.com/api/individual−purchases’ ); // ... } }