SitePoint
  • Blog
  • Forum
  • Library
  • Login
Join Premium
Advanced Web Application Architecture
Close
    • Advanced Web Application Architecture
    • Introduction
    • 1 Preface
    • 2 Why is decoupling from infrastructure so important?
    • 3 Who is this book for?
    • 4 Overview of the contents
    • 5 The accompanying demo project
    • 6 About the author
    • 7 Acknowledgements
    • 8 Changelog
    • Part I Decoupling from infrastructure
    • 1.1 Rule no 1: No dependencies on external systems
    • 1.2 Abstraction
    • 1.3 Rule no 2: No special context needed
    • 1.4 Summary
    • 2.1 SQL statements all over the place
    • 2.2 Trying to fix it with a table data gateway
    • 2.3 Designing an entity
    • 2.4 Introducing a repository
    • 2.5 Mapping entity data to table columns
    • 2.6 Generating the identifier earlier
    • 2.7 Using a value object for the identifier
    • 2.8 Active Record versus Data Mapper
    • 2.9 Summary
    • 3.1 Reusing the write model
    • 3.2 Creating a separate read model
    • 3.3 Read model repository implementations
    • 3.4 Using value objects with internal read models
    • 3.5 A specific type of read model: the view model
    • 3.6 Using view models for APIs
    • 3.7 Summary
    • 4.1 Considering other infrastructures
    • 4.2 Designing a use case to be reusable
    • 4.3 Extracting an application service
    • 4.4 Introducing a parameter object
    • 4.5 Dealing with multiple steps
    • 4.6 Summary
    • 5.1 From service location to explicit dependencies
    • 5.2 Depending on global state
    • 5.3 Injecting dependencies
    • 5.4 Injecting configuration values
    • 5.5 Using method arguments for job-specific data
    • 5.6 Clients of reusable services
    • 5.7 Testing
    • 5.8 Effective testing
    • 5.9 The Composition root is near the entry point
    • 5.10 Summary
    • 6.1 Connecting to the external service
    • 6.2 Introducing an abstraction
    • 6.3 Architectural advantages
    • 6.4 Testing
    • 6.5 Summary
    • 7.1 Passing current time and random data as method arguments
    • 7.2 Introducing factories
    • 7.3 Introducing value objects
    • 7.4 Improving the factories
    • 7.5 Manipulating the current time
    • 7.6 Integration tests again
    • 7.7 Summary
    • 8.1 Protecting entity state
    • 8.2 Using value objects to validate separate values
    • 8.3 Form validation
    • 8.4 Using exceptions to talk to users
    • 8.5 When validation is not the answer
    • 8.6 Creating and validating command objects
    • 8.7 Summary
    • 9.1 Core code and infrastructure code
    • 9.2 A summary of the strategy
    • 9.3 Focus on the domain
    • 9.4 Focus on testability
    • 9.5 Pure object-oriented code
    • 9.6 Summary
    • Part II Organizing principles
    • 11.1 Framework-inspired structural elements
    • 11.2 Entities
    • 11.3 Repositories
    • 11.4 Application services
    • 11.5 Event subscribers
    • 11.6 Read models
    • 11.7 Process modelling
    • 11.8 Summary
    • 12.1 MVC
    • 12.2 A standard set of layers
    • 12.3 The Dependency rule
    • 12.4 Making layers tangible
    • 12.5 Summary
    • 13.1 Hexagonal architecture
    • 13.2 Ports
    • 13.3 Adapters for outgoing ports
    • 13.4 Adapters for incoming ports
    • 13.5 The application as an interface
    • 13.6 Combining ports and adapters with layers
    • 13.7 Structuring the Infrastructure layer
    • 13.8 Summary
    • 14.1 Unit tests
    • 14.2 Adapter tests
    • 14.3 Contract tests for outgoing port adapters
    • 14.4 Driving tests for incoming port adapters
    • 14.5 Use case tests
    • 14.6 End-to-end tests
    • 14.7 Development workflow
    • 14.8 Summary
    • 15.1 Is a decoupled architecture the right choice for all projects?
    • 15.2 My application is not supposed to live longer than two years
    • 15.3 My application offers only CRUD functionality
    • 15.4 My application is a legacy application
    • 15.5 I can never make my entire application decoupled
    • 15.6 Isn’t this over-engineering?
    • Notes

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:

  1. Introduce an interface
  2. 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).

PIC

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’         );          // ...     } } 
End of PreviewSign Up to unlock the rest of this title.

Community Questions