From 695dce2445c68750190ac031bcbcb867ded82dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Thu, 16 Apr 2020 19:23:58 +0200 Subject: [PATCH 1/9] Add book: Advanced Web Application Architecture --- books/web-application-architecture.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 books/web-application-architecture.md diff --git a/books/web-application-architecture.md b/books/web-application-architecture.md new file mode 100644 index 0000000..e69de29 From 3be81f810785570a53b5a92b84cc8ba8ec6ebdf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Thu, 16 Apr 2020 19:40:46 +0200 Subject: [PATCH 2/9] Update the book index of content --- books/web-application-architecture.md | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/books/web-application-architecture.md b/books/web-application-architecture.md index e69de29..b44ba4a 100644 --- a/books/web-application-architecture.md +++ b/books/web-application-architecture.md @@ -0,0 +1,32 @@ +# Advanced Web Application Architecture + +by Matthias Noback + +------ + +> The missing manual for making your web applications future-proof +> +> Web applications deserve to outlive the currently fashionable framework. Your application's core use cases deserve to be decoupled from their surrounding infrastructure. And all of your domain-specific code needs to be testable; it has to be tested after all. +> +> This book helps you get your web applications back in shape. It contains many techniques for decoupling from infrastructure (like the framework, the database, or remote web services). + +* [Leanpub](https://leanpub.com/web-application-architecture/) +* [Source code application sample](https://enjoy.gitstore.app/repositories/matthiasnoback/read-with-the-author) + +------ + +## Preface + +* + +## Decoupling from infrastructure + +### Intropduction + +* + +### The domain model + +* + +## ... \ No newline at end of file From bbf745df32f51eb5f1de06106860ed01800400f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Mon, 20 Apr 2020 18:25:18 +0200 Subject: [PATCH 3/9] Update README.md --- README.md | 2 +- ...architecture.md => advanced-web-application-architecture.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename books/{web-application-architecture.md => advanced-web-application-architecture.md} (100%) diff --git a/README.md b/README.md index aaac69b..ec89c84 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Here is a personal collection of highlight quotes, notes and summaries on differ ## πŸ“š Books -* _.gitkeep_ +* [Advanced Web Application Architecture](books/advanced-web-application-architecture.md), by **Matthias Noback**, 2020 ## πŸŽ“ Courses diff --git a/books/web-application-architecture.md b/books/advanced-web-application-architecture.md similarity index 100% rename from books/web-application-architecture.md rename to books/advanced-web-application-architecture.md From 5c98f99e9ea5f7520ce4dea33194447f97ef77e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Mon, 20 Apr 2020 20:06:08 +0200 Subject: [PATCH 4/9] Update notes, add chapter 1 --- .../advanced-web-application-architecture.md | 102 +++++++++++++++++- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/books/advanced-web-application-architecture.md b/books/advanced-web-application-architecture.md index b44ba4a..08dc201 100644 --- a/books/advanced-web-application-architecture.md +++ b/books/advanced-web-application-architecture.md @@ -13,20 +13,114 @@ by Matthias Noback * [Leanpub](https://leanpub.com/web-application-architecture/) * [Source code application sample](https://enjoy.gitstore.app/repositories/matthiasnoback/read-with-the-author) +Related resources + +* Noback's blogposts + * [Is all code in vendor infrastructure code?](https://matthiasnoback.nl/2020/02/is-all-code-in-vendor-infrastructure-code/) + * Dividing responsibilities - [Part 1](https://matthiasnoback.nl/2019/07/dividing-responsibilities-part-1/) and [Part 2](https://matthiasnoback.nl/2019/07/dividing-responsibilities-part-2/) + ------ ## Preface +* The characteristics of some common types of objects: *controllers*, *entities*, *value objects*, *repositories*, *event subscribers*, etc. (...) how these different types of objects find their natural place in a set of architectural layers +* This book is a showcase of design patterns, about how these different objects work all together in a "well-architected" application + * As a guide to decoupling your domain model and your application's use from the infrastructure (the framework, the database, and so on) +* Start using a standard set of layers (called **Domain**, **Application**, and **Infrastructure**) you can easily mark: + * your decoupled use cases as **Ports** + * and the supporting implementation code as **Adapters** +* Using layers, ports and adapters β†’ A great way to standardizing your high-level architecture + * If your app is supposed to live longer than two years β†’ decoupling from infra is a safe bet + * If not, that might be a good reason not to care about +* Software always becomes a mess, even if you follow all the known best practices for software design. But with this practices, it takes more time to become a mess + +## Part I. Decoupling from infrastructure + +### Preface and Introduction + +* Why is separating infrastructure concerns from your core application logic so important? + * It leads to a domain model that can be developed in a *domain-driven* way + * It lead to application code that is very easy to test, and to develop in a *test-driven* way +* Make a clear distinction between the core code of your application, and the infrastructure code that supports it +* Both types of core are equally impiortant, but thet shoudn't live together (in the same classes, neither in the same layer) +* Distinction between infrastructure code and non-infrastructure code (core code) β†’ **Not all code in vendor is infrastructure code** + * Usually, most of the code in `vendor/` should be considered infrastructure code (your web framework which facilitates communication with browsers and external systems via HTTP, the ORM which communicates with the database, etc.) + * Nevertheless, code placed in a particular directory doesn't determine whether or not something is infrastructure code + * What matters is what the code does, and what it needs to do that (see the rules) + * Some `vendor/`code could be considered core code, event though it's not written by you or for your application specifically +* We'll define core code by introducing **two rules for it**. Any code that doesn't follow both rules **at the same time**, should be considered infrastructure code + * **Rule 1. No dependencies on external systems**. 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 + * **Rule 2. No special context needed**. 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 +* Notes about the Rule 1 + * An external system is something that lives outside your application (a database, a remote web service, the system's clock, the file system, etc.) + * Core code should be able to run without these external dependencies + * When code follows the first rule, it means you can run it in complete isolation + * Automated test for core code will be very easy to write (no database setup/teardown, no internet connection, no hard disk I/O, etc.) +* Notes about the Rule 2 + * Only if code doesn't require a special context, **and also** hasn't been designed to run in a special context, can it be considered core code + * Not having to create a soecial context for code to run it makes easy to write automated tests +* 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 + +### The domain model + * -## Decoupling from infrastructure +### Read models and view models + +* -### Intropduction +### Use cases * -### The domain model +### Service locators + +* + +### External services + +* + +### Time and randomness + +* + +### Validation + +* + +### Conclusion * -## ... \ No newline at end of file +## Part II. Organizing principles + +### Introduction + +* + +### Key design patterns + +* + +### Architectural layers + +* + +### Ports and adapters + +* + +### Process modelling + +* + +## Part III. Conclusion + +### A testing strategy for decoupled applications + +* + +### Context is everything + +* From 201c81b60ef63970a72b0904b1bb550fc6033515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Fri, 24 Apr 2020 19:45:47 +0200 Subject: [PATCH 5/9] Update book/web-application-architecture --- books/advanced-web-application-architecture.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/books/advanced-web-application-architecture.md b/books/advanced-web-application-architecture.md index 08dc201..bb5d589 100644 --- a/books/advanced-web-application-architecture.md +++ b/books/advanced-web-application-architecture.md @@ -36,7 +36,7 @@ Related resources ## Part I. Decoupling from infrastructure -### Preface and Introduction +### Introduction * Why is separating infrastructure concerns from your core application logic so important? * It leads to a domain model that can be developed in a *domain-driven* way @@ -58,9 +58,15 @@ Related resources * Automated test for core code will be very easy to write (no database setup/teardown, no internet connection, no hard disk I/O, etc.) * Notes about the Rule 2 * Only if code doesn't require a special context, **and also** hasn't been designed to run in a special context, can it be considered core code - * Not having to create a soecial context for code to run it makes easy to write automated tests + * Not having to create a special context for code to run it makes easy to write automated tests * 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 +**As a summary** + +* Distintion between core and infrastructure code + * **Core** code is code that can be executen in **any context** + * **Infrastructure** code is the opposite, it **needs external systems, special setup, or** is designed to run in a **specific context** only + ### The domain model * From 64c6fd53f1d8fa154cffceb1e3242407b55020e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Wed, 6 May 2020 19:32:21 +0200 Subject: [PATCH 6/9] Update book/web-application-architecture --- .../advanced-web-application-architecture.md | 315 ++++++++++++++++-- 1 file changed, 295 insertions(+), 20 deletions(-) diff --git a/books/advanced-web-application-architecture.md b/books/advanced-web-application-architecture.md index bb5d589..51c0e0a 100644 --- a/books/advanced-web-application-architecture.md +++ b/books/advanced-web-application-architecture.md @@ -4,11 +4,11 @@ by Matthias Noback ------ -> The missing manual for making your web applications future-proof +> The missing manual for making your web applications **future-proof** > -> Web applications deserve to outlive the currently fashionable framework. Your application's core use cases deserve to be decoupled from their surrounding infrastructure. And all of your domain-specific code needs to be testable; it has to be tested after all. +> Web applications deserve to outlive the currently fashionable framework. Your **application's core use cases deserve to be decoupled from their surrounding infrastructure**. And all of your domain-specific code **needs to be testable**; it has to be tested after all. > -> This book helps you get your web applications back in shape. It contains many techniques for decoupling from infrastructure (like the framework, the database, or remote web services). +> This book helps you get your web applications back in shape. It contains many **techniques for decoupling from infrastructure** (like the framework, the database, or remote web services). * [Leanpub](https://leanpub.com/web-application-architecture/) * [Source code application sample](https://enjoy.gitstore.app/repositories/matthiasnoback/read-with-the-author) @@ -27,39 +27,39 @@ Related resources * This book is a showcase of design patterns, about how these different objects work all together in a "well-architected" application * As a guide to decoupling your domain model and your application's use from the infrastructure (the framework, the database, and so on) * Start using a standard set of layers (called **Domain**, **Application**, and **Infrastructure**) you can easily mark: - * your decoupled use cases as **Ports** - * and the supporting implementation code as **Adapters** + * your **decoupled use cases** code as **Ports** + * and the **supporting implementation** code as **Adapters** * Using layers, ports and adapters β†’ A great way to standardizing your high-level architecture * If your app is supposed to live longer than two years β†’ decoupling from infra is a safe bet * If not, that might be a good reason not to care about -* Software always becomes a mess, even if you follow all the known best practices for software design. But with this practices, it takes more time to become a mess +* Software **always** becomes a mess, **even** if you follow all the known best practices for software design. But with this practices, it **takes more time** to become a mess ## Part I. Decoupling from infrastructure ### Introduction * Why is separating infrastructure concerns from your core application logic so important? - * It leads to a domain model that can be developed in a *domain-driven* way - * It lead to application code that is very easy to test, and to develop in a *test-driven* way -* Make a clear distinction between the core code of your application, and the infrastructure code that supports it -* Both types of core are equally impiortant, but thet shoudn't live together (in the same classes, neither in the same layer) + * It leads to a domain model that can be **developed in a *domain-driven* way** + * It leads to application code that is very **easy to test**, and to develop in a **test-driven way** +* Make a **clear distinction** between the core code of your application, and the infrastructure code that supports it +* Both types of core are **equally important**, but thet **shoudn't live together** (in the same classes, neither in the same layer) * Distinction between infrastructure code and non-infrastructure code (core code) β†’ **Not all code in vendor is infrastructure code** * Usually, most of the code in `vendor/` should be considered infrastructure code (your web framework which facilitates communication with browsers and external systems via HTTP, the ORM which communicates with the database, etc.) - * Nevertheless, code placed in a particular directory doesn't determine whether or not something is infrastructure code - * What matters is what the code does, and what it needs to do that (see the rules) - * Some `vendor/`code could be considered core code, event though it's not written by you or for your application specifically -* We'll define core code by introducing **two rules for it**. Any code that doesn't follow both rules **at the same time**, should be considered infrastructure code + * Nevertheless, code placed in a particular directory **doesn't determine** whether or not something is infrastructure code + * What matters is **what** the code **does**, **and what it needs** to do that (see the rules) + * Some `vendor/`code could be considered core code, **even though** it's not written by you or for your application specifically +* We'll define core code by introducing **two rules for it**. Any code that **doesn't follow both rules at the same time**, should be considered infrastructure code * **Rule 1. No dependencies on external systems**. 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 * **Rule 2. No special context needed**. 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 * Notes about the Rule 1 - * An external system is something that lives outside your application (a database, a remote web service, the system's clock, the file system, etc.) + * An external system is something that **lives outside** your application (a database, a remote web service, the system's clock, the file system, etc.) * Core code should be able to run without these external dependencies - * When code follows the first rule, it means you can run it in complete isolation - * Automated test for core code will be very easy to write (no database setup/teardown, no internet connection, no hard disk I/O, etc.) + * When code follows the first rule, it means you can run it in **complete isolation** + * Automated test for core code will be **very easy to write** (no database setup/teardown, no internet connection, no hard disk I/O, etc.) * Notes about the Rule 2 - * Only if code doesn't require a special context, **and also** hasn't been designed to run in a special context, can it be considered core code + * Only if code doesn't require a special context, **and also hasn't been designed** to run in a special context, can it be considered core code * Not having to create a special context for code to run it makes easy to write automated tests -* 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 +* 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 **As a summary** @@ -69,7 +69,282 @@ Related resources ### The domain model -* +When we deal with domain models, we have to cover: + +* Extracting an entity and a repository (from code that interacts with databases) +* Using an entity to protect the model (against inconsistent data) +* Mapping the entity to a database table (using a repository pattern) +* Providing an entity witg identity (**before** saving it) + +#### SQL statements all over the place + +First approach is using SQL statements all over the place. Why bad? + +* This approach ends up with a mixed code in controller: database code, exposed database estructure, SQL code mixed with PHP one, code not object oriented, no use of entity pattern, etc +* So, this way is hard to find the "story" (*the intention*) or scenario (*use case*) of the controller's action +* Lot of implementation details that obscure the view of higher level steps of the scenario +* Mixing high-level steps (like "save order") with low-level implementation details + * This directly couples the use case itself to any related technological decisions + * This makes it very dificult to change directions later on + +#### Trying to fix it with a table data gateway + +Second approach. Trying to fix it with a [Table Data Gateway](https://www.martinfowler.com/eaaCatalog/tableDataGateway.html) (TDG) + +* Traditional solution: pushing SQL statements outside of regular code, using the TDG pattern +* TDG hides the SQL statements (or other implementation details related with databases) behind a simple interface per database table +* In this way, the controller intention is definitely easier to understand, what's going on +* Anyway, the code still remains very table-oriented way, so: + * We are still very much coupled to the technological decisions we are making in the code + * And we're stuck using a relational database (table-oriented) +* More problems are that there's nothing preventing us from inserting inconsistent values within the model fields +* TDG is not able to protect the internal consistency of a model + +#### Designing an entity + +* Before saving an entity (an order, for instance) we should make sure it's complete and correct +* We also want to make sure that what ends up in the database, can safely be used by other parts of the application +* The client has to supply all the required arguments to create an entity, using its constructor + * We can improve the constructor by doing some basic checks inside of it, ussing assertions +* The entity validates its constructor arguments using assertions + * Assertions prevent an entity object from being instantiated with invalid data + * They can provide **basic consistency** for the data it holds (*Sidenote: just basic consistency, no business rules at all*) +* Can we use these assertion functions for validating user input? + * No. Assertions won't be very useful when we need to validate user input + * Instead, they should be used by objects as a way to **protect themselves** against incomplete, inconsistenmt or meaningless data +* When we can can call an object an Entity? + * Is a stateful object that **guarantees its own consistency** + * Also, is going to be **persisted somehow** (doesn't need to be in a database) + * It has an identity, a unique value that identifies itself from the other entities + +* If we still use the TDG pattern, in order to save an entity we still need a map of columns to values. An associative array (a map) that knows the structure of the table and links it with its values + + * So, it seems that the thing we want to save (an entity, an order e.g.) and the thing that can save it (a TDG, e.g the `OrdersGateway`) turn ou to be incompatible + * Anyhow, we still want to save the `Order`entity to the database, somehow. We need to find a diferent design for the thing that can do this + + +#### Introducing a repository + +[Repository](https://martinfowler.com/eaaCatalog/repository.html) + +* If a certain facility is not yet available in a project, you can apply a programming trick: act as if it was already available + + * Sample: you're looking for a "thing" that can "save" an "Order" to "the database" + + * Just imagine that the thing already exists. Start using it + + ```php + $order = new Order(/*...*/); + $lastInsertedId = $orderSaver->insert($order); + ``` + +* "Object savers" are usually called repositories. So they are a solution to a common problem: the need to save a domain object, and later reconstitute it + +* It's good know that `save()` method has a symmetrical counterpart called `getById()` + + ```php + interface OrderRepository + { + public function save(Order $order): void; + public function getById(int $orderId): Order; + } + ``` + +#### Mapping entity data to table columns + +* Again, the repository needs a map of columns (structure) and values + +* Writing an implementation that actually does (saving an entity to the database) is not as straighforward as it seems + +* There're different options here + + * **Using an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping)** + + * This allows to add mapping configuration between the entity properties and the database-table fields + * Repository is now implemented by injecting an entity manager that interacts with the ORM + * This solution saves you lot of time, but it may also get you into trouble + * Example with Doctrine ORM, the problem is not doctrine itself, but using **generic abstractions** + * Hiding away so many implementation details and so much "magic" behind a single abstract `EntityManagerInterface` means you'll run into trouble sooner or later + * Several advantages to using a popular ORM + * Extensive documentation, online examples, etc. + * Out-of-the-box solutions for common problems like data migrations, fixture loading, etc. + * It's okay to use an ORM? Rules you can use to stick with it: + * Only use **simple mapping configuration**. No table inheritance, no "embeddables", no custom types, etc. Just simple mapping + * Stick to one-to-many associations + * Reference entities by their ID + * Don't jump from entity to entity using association fields + + * **Manual mapping** + + * In the past few years, Noback comes to the conclusion more than once tht doing the mapping manuallt, that is, writing the code for this himself, this can be a pretty good solution + + * In this solution, the entity exposes its internal data as an array, doing the mapping inside the repository + + ```php + final class Order { + // ... + public function mappedData(): array { + return [ + 'field1' => $this->field1, + ]; + } + } + + final class SqlOrderRepository implements OrderRepository { + // ... + public function save(Order $order): void { + // ... + $data = $order->mapperData(); + // with this array, the repository can build the SQL statement + // making a mapping between entity and database fields + } + } + ``` + + * The downside with this is that the `Order` entity has knowledge about the names and types of the database columns. This can be improved making the entity to expose its internal data + + ```php + final class Order { + // ... + public function mappedData(): array { + return get_object_vars($this); + } + } + ``` + + * But, in this way the downside is that it reduces the level of encapsulation of `Order` + + * A related problem with this is, whenever you rename a property, change its type, add or remove properties, you'd also have to go to the repository and change the corresponding mapping code + + * It will be very hard to refactor the `Order` if its internals are no longer private + + * The ability to freely change the internal structure of an obnject is what enables you to improve its design + +* Having `mappedData` method where we have column names inside the entity... Doesn't that cause a mix of infrastructure and core code? + + * No. To demostrate that having column names inside an entity class does not automatically turn it into infrastructure code, let's check the rules + * Has the code of this method dependencies at all? β†’ No + * Can `mappedData()` run in any context and/or environment, without special setup? β†’ Yes + * Does `mappedData()` depends on external systems, or has code written to interfacting with a specific type of system? β†’ No + * So, `mappedData` complies with both rules for core code. because it only does some simple transformations on values in memory (no more) + +* How about adding Doctrine mapping annotations to your entity, like `@Entity`, `@Table`, `@Column' , ...? + + * Does that result in mixed code, infrastructure / core code? β†’ No + * Just instantiating an entity with mapping annotations doesn't require any special setup + * Calling any method doesn't require any external dependencies to be available + * So, it should still be considered core code, not infrastructure code + +* However, an entity with Doctrine annotations or a `mappedData` method does still contain technical implementation details (like table and column names, column types) + + * So, when you get to the point where you need/want to switch databases after all, you will still have to modify this code + * *Sidenote*: Anyhow, it's still core code! + +#### Generating the identifier earlier + +* Previous repository presented the save method in this way + + ```php + public function save(Order $order): int; + ``` + +* By introducing the `OrderRepository` we hide most of the implementation details related to saving orders into database + +* But there is one left: the ID of the order: + + * We are saving an inconsistent Order (without id), then we get the last ID and returns it + * That "last ID" (an integer one) still reveals that the mechanism used to persist an Order uses an auto-incrmeenting integer column dfor the primary ID of an order + * Not every database technology will support auto-incrementing ID columns, (*Sidenote*: or `int`values for that) + * Another problem is that the Order entity is supposed to be complete from uts beginning + * It should hold the minimum set of data, in order to be useful, and consistent in its behaviour + * Order isn't consistent until the database has finished saving it... this is not good + +* So, is better to provide an entity (Order, e.g) with an ID at the moment we instantiate. As a required constructor argument + +* Also, delegate the creation of next identities to the repository + + ```php + interface OrderRepository + { + // ... + public function nextIdentity(): int; + } + ``` + +##### Naive impelentation of `nextIdentity()` + +```php +public function nextIdentity(): int +{ + return (int)Β·this->connection-->execute( + 'SELECT MAX(id) AS highestId FROM orders' + )->fetchColumn(0) + 1; +} +``` + +* It has problems, if the application has many concurrent users + +##### Naive implementation but using a sequence table + +* If you are having concurrency issues, you could switch to another solution with a sequence table at datanase-level + +#### Using a value object for the identifier + +* To fully encapsulate the actual data type of the identifier, wrap it inside a value object +* This makes the actual identifier a mere implementtion detail of the entity +* We can add a simple `asString()` method to the `OrderId` object value + +#### Active Record versus Data Mapper + +* [Data Mapper](https://martinfowler.com/eaaCatalog/dataMapper.html) design pattern. [Active Record](https://www.martinfowler.com/eaaCatalog/activeRecord.html) one +* A common alternative for storing entities is the Active Record design pattern +* The entity will be able to load itself from the database, and it can save and delete itself as well + +```php +final class Order ActiveRecordEntity +{ + public static function getById(int $orderId): self {/*...*/} + public function delete(): void; + public function save(): void; + // ... +} +``` + +* There are downsides to it from a design perspective + * We loose the isolation we need for proper unit testing, by inheriting infrastructure code + * This code is specific to the framework that owns `ActiveRecordEntity`, so you are making your domain model directly coupled to, and only functional in the presence of that framework + * Clients of `Order`can do many more things with the object than they most likely should ne allowed to do +* By introducing an abstraction you achieve decoupling from surrounding infrastructure +* Comparing these design patterns, it's clear that data mapper allows for a better separation between core and infrastructure code + +#### Rules + +The Noback's advice is to mitigate some of the downside by keeping yourself to the following rules: + +* Design your Active Record (AR) entities like real entities +* Don't use the same AR entity for changing state and retrieving state + * Separate your model into a write and a read model +* Don't use your AR entity to navigate from one AR entity to other AR entities + * If you want to make a change to a different AR entity, fetch it by its ID from the corresponding repository + * Ignore the fact that an AR entity provides typical service facilities, like saving and deleting + * Keep using a repository and *[double dispatch](https://books.google.es/books?id=WVQF2PK2tlgC&pg=PA409&dq=%22double+dispatch%22)* to perform these tasks + +```php +final class ActiveRecordOrderRepository implements OrderRepository { + public function save(Order $order): void { + $order->save(); + } +} +``` + +* Anyway, the problem doesn't fo away with this AR approach. There will be code inside your AR entity that is framework or ORM-specific + +#### Summary + +* Trying to separate core code from infrastructure one, we took an intermediate refqctoring step, introducing a table gateway +* Two known design patterns: Entity and Reposiroty +* The repository turned out to be a good place for generating IDs +* Using value objects for those IDs ### Read models and view models From 30b724db26f19de5379df9e4a24519558f6245a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Tue, 19 May 2020 23:53:33 +0200 Subject: [PATCH 7/9] Update book/web-application-architecture --- .../advanced-web-application-architecture.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/books/advanced-web-application-architecture.md b/books/advanced-web-application-architecture.md index 51c0e0a..697c737 100644 --- a/books/advanced-web-application-architecture.md +++ b/books/advanced-web-application-architecture.md @@ -348,6 +348,57 @@ final class ActiveRecordOrderRepository implements OrderRepository { ### Read models and view models +* Different solutions for implementing a read model repository + * Hiding query complexity behind view models +* Scenario: retrieving the price of an ebook + * Hiding the low-level implementation details (database, ebooks table, price column, etc) + * Do it behind a high-level interface, which represents what information we're interested in +* Entity to represent the ebook and retrieve it from its repository + +#### Reusing the write model + +* Assuming we already have an `Ebook` entity and an `EbookRepository` interface in our project + + ```php + final class Ebook + { + private EbookId $ebbokId; + private int $price; + // ... + public function changePrice(int $newPrice): void {...} + public function show(): void {...} + public function hide(): void {...} + } + ``` + +* Seems that the `Ebook`entity is a convenient object to quickly get the information. But there are a couple of issues with reusing an existing entity in a diferent context + + 1. Existing object was not designed to **retrieve** information from. Instead, it was designed **to add** new ebooks to our catalog + + So, anywhere we load this entity, we do **with the intention** to manipulate it and save it. If we reuse it in a "retrieval context", we gain access to all other methods that can change state (`changePrice`, `hide`, `show`) + + It's generally a smart idea to limit the number of methods that a client of an object has access to + + 2. About reusing objects in general, not just entities. Doing this, the object starts to play too many roles at the same time (...) Soon, it becomes too big to read the code and understand what it does (...) Also, object becomes *resistant* to change, which is a bad quality for objects in general + +* But, without any reuse it would become really hard to accomplish anything + + * Keep track of the intended use of objects, and watch for tension in the design + * You can prevent a lot of this design tension by introducing separate objects foir changing and retrieving information + +* Client that needs an object to retrieve information (read) should not use the same object as client that want to make changes to it (write) + +* `Ebook` read-only model that its intention is to know the price of the ebook + +* Anyway, write `Ebook`entity can have some read methods (getters) that should be necessary as well. Like get the ID of the entity, or the internally recorded events + +#### Creating a separate read model + +* Frame the question in such a way that it's easy for you to ask. And design the type of answer you want to retrieve + * sample question: *give me the price of an ebook with ID <...>* + * sample answer; an object that represents the price of the ebook (it's intention) +* Two options for modeling the question with code + * * ### Use cases From a11e9762275df882cde237c1990a3f4480709905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Fri, 29 May 2020 10:26:12 +0200 Subject: [PATCH 8/9] Update book/web-application-architecture --- .../query_decoupling_from_infrastructure.png | Bin 0 -> 18724 bytes .../advanced-web-application-architecture.md | 157 +++++++++++++++++- 2 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 books/.assets/advanced-web-application-architecture.md/query_decoupling_from_infrastructure.png diff --git a/books/.assets/advanced-web-application-architecture.md/query_decoupling_from_infrastructure.png b/books/.assets/advanced-web-application-architecture.md/query_decoupling_from_infrastructure.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e8a9ec5cc2d727894f6352d5fbc07671f8903f GIT binary patch literal 18724 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 * sample answer; an object that represents the price of the ebook (it's intention) * Two options for modeling the question with code - * -* -### Use cases + * the repository pattern, once more (more common solution) + + ```php + interface EbookRepository + { + /** @throws CouldNotFindEbook */ + public function getById(EbookId $ebookId): Ebook; + } + ``` + + Useful when you want to group and retrieve more information inside `Ebook`, the `price()` and `title()` for instance. + + * create classes with the same or a similar name as the entity classes themselves. In this case, you have to put the code in a different namespace, to make it easy to distinguish between the read and write model + + ```php + interface GetPrice + { + /** @throws CouldNotFindEbook */ + public function ofEbook(EbookId $ebookId): int; + } + ``` + + If on the other hand, you only need one single piece of information (the price), you could let go for this solution: a pseudo-entity + + * Taking the first or second approach, as always, would depend on your situation + + +#### First approach. Read model repository implementations + +* Wheter a new Ebook **entity** gets created (by an employee, e.g), there should also be a corresponding `Ebook` **read model** that exposes the ebook price (to the clients e.g) +* Whenever the entity changes, the corresponding read model should also be updated + +##### Sharing the underlying data usage + +* The simplest solutions. The read model use the same data source of the entity. So, the read model's repository gets its data from the same table and creates an `Ebook read model from the entity's data source +* When write and read models share the same data source, this can lead to new problems + * Sample: `price` column contains the price of the ebook, in cents. But what if the write model switches to a native decimal representation? The read model would start to provide bad prices, because "1.5€" in the database when cast to an integer (in the read model) will become 1 cent in the application +* One way to reduce the previous risk is to write integration tests for your read model repositories +* Another alternative, is having a single class implementing both the write and read model repository interfaces + +##### Using write model domain events to synchronize the read model + +* Dispatch a **domain event** for every important change inside the entity + +* The read model is then able to update its own state based on the information contained in those events + +* What is needed to have + + * An entity that can record domain events internally + * A domain event for every state change that is relevant to the read model(s) + * Sample: `PriceChanged` an object that holds the `EbookId`, as well the new price (`int`) + * A service that subscribes to these domain events and updates the read model according to the changes indicated by the events + +* Note that setting the price on the read model **isn't the same** thing as changing the price on the entity + + * The change on the entity is the real change + + * Updating the read model, we're merely **reflecting** that original/real change onto our read model (using information from the dispatched event `PriceChanged`) + + ```php + final class UpdateEbookReadModel + { + // ... + public function whenPriceChanges(PriceChanged $event): void + { + $readModel = $this->readModelRepository->getById($event->ebookId()); + $readModel->setPrice($event->newPrice()); + $this->readModelRepository->save($readModel); + } + } + ``` + + * what happens inside save() method? This is an infrastructural concern: do we save it to the same database where we save entities? different databases? are read models stored as documents within Elasticsearch? etc. + * None of this really matters for the core of the application, since we already have a read model repository interface (a port, an abstraction) which indicates "*I have a particular need, but I don't care how you'll fullfill this need; don't care if you need to talk to something outside the application for it*" + * What matters is that any client can use the repository interface to get the information it needs + +##### Using value objects with internal read models + +* A read model is often designed to supoprt a specific use case for one of its clients + +* If clients code looks like + + ```php + // ... + $ebookPrice = $ebook->price(); // int + $orderAmountInCents = (int)$request->get('quantity') * $ebookPrice + // ... + ``` + +* What happens if $ebook-price() returns a string instead of an integer? + + * Changing the underlaying data type of a read model's property will affect the clients of that read model + +* By using a value object, we can ensure that clients won't rely on raw data, nor on the particular primitive type of that data + +* An added benefit is that using value objects allows you to decouple from th einfrastructure + + * Clients can keep using value objects wven if the underlying data type changes + +##### A specific typer of read model: the view model + +* Introducing a dedicated read model and read model repository because an `OrderController` needed the price of an ebook, this could classify the resulting model as an **internal read model**, since the data it provides is **only used internally** (by the application itself) and never directly shared with or displayed to the user +* On the other hand, there're places in our application where we fetch data in order to show it to the user +* When we need to have a view model for users that needs to render a view model with an HTML template + +```php +interface Ebooks +{ + /** @return array */ + public function listAvailableEbooks(): array +} +``` + +* This `Ebook` class is not the same as the entity class +* Objects of this type will serve as read models for displaying data on an HTML page +* Such read models can be calles **view models**, since they are used to dusplay data to users of external systems +* Most of the data exposed by a view model should be of type `string`, because rendering it (within a Twig template, e.g) is basically an exercise in string concatenation + * In a view model, `price()` doesn't return a `Price` value object.Returns a string, properly formatted money, including the currency sign and with the correct precission +* Templates in particular shouldn't need to know anything about the domain objects that our application uses internally + * This means, our view model should only return primitive-type values + +##### β€œ I'm afraid we'll end up with too many classes... ” + +* You will definitely have more classes and interfaces when you separate core from infrastructure code + +* The interface represents the answer, and the other class is an implementation of the query interface + +* After decoupling a query from underlying inrastructure, you will have three elements + + .![Decoupling query from underlying infrastructure](.assets/advanced-web-application-architecture.md/query_decoupling_from_infrastructure.png) + +* If you want to decouple from infrstructure, this is the way to go +* Anyway, there're several ways to keep the number of elements manageable: + * Only introduce an interface for objects that actually communicate with something outside your application (this saves some interfaces) + * Combine several interface methods in a single interface + * Let a single class implement multiples interfaces (this save some classes) + * Reuse the "answer" class for different queries (this also saves some classes) +* Keep an eye on tension in the design. Reducing elements has also downsides + * With fewer interfaces, it can be harder to replace an actual service implementation with a test double. If this happens, reintroduce the interface + * An interface with many methods can give clients access to many unrelated methods + * A class that implements multiple interfaces will become hard to maintain. So, only implement intefraces with the exact methods that are truly related + * If a class starts to be used in too many places, it will be harder to change it, since there are many clients that need to be updated and may potentially break + +##### Summary + +* We recognize the need to get information about a related entity +* We decided not to combine write and read responsibilities in the same object +* An immutable `Ebook`read model, specialized in providing information; that comes with its own read model repository interface +* Different alternstives to repository implementations +* Another need is to show some data to the user; created a dedicated view model + +### Application services * From d06f8ebd6108b494dc02f26d8e236c914593fb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Barbeito=20Garc=C3=ADa?= Date: Fri, 29 May 2020 12:15:05 +0200 Subject: [PATCH 9/9] Update book/web-application-architecture --- books/advanced-web-application-architecture.md | 1 + 1 file changed, 1 insertion(+) diff --git a/books/advanced-web-application-architecture.md b/books/advanced-web-application-architecture.md index ba7f75a..bcee37e 100644 --- a/books/advanced-web-application-architecture.md +++ b/books/advanced-web-application-architecture.md @@ -12,6 +12,7 @@ by Matthias Noback * [Leanpub](https://leanpub.com/web-application-architecture/) * [Source code application sample](https://enjoy.gitstore.app/repositories/matthiasnoback/read-with-the-author) +* [Link registry for the book](https://advwebapparch.com) Related resources