Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

41KB

FOCUS Architecture — Chapter-10: FOCUS in One Page

  • Source: /library/FOCUS Architecture/source-file.pdf
  • PDF pages: 212–255
  • Pages without text: none

FOCUS in One Page In this chapter, you’ll: follow the path from a tap on the screen to the updated total, crossing FOCUS’s four pieces in the order they talk to each other; fill in the canonical responsibility table, which states what each layer does and, above all, what each one forbids; justify why the architecture stops at four layers, and reject the fifth by the same criterion that approves the other four. In chapter 2, you changed one line in the discount calculation and broke three screens: the tab, the register, and the report. The blame wasn’t the language’s, and it wasn’t your carelessness either. It was an address problem: that rule lived in three screens at once, and nothing in the project said where it should live. What was missing has a name, and it fits on one page: a four-piece map that answers, for any line of code you write, which one of them it lives in. Chapters 4 through 9 delivered loose pieces. The right not to build what nobody asked for (ch. 4). Duplication that’s about knowledge, not text (ch. 5). Small contracts and dependencies pointing at abstractions (ch. 6). Pure functions and immutable models (ch. 7). Failure as a value, translated exactly once at the boundary (ch. 8). Constructor injection and an object graph

assembled in a single place (ch. 9). Each one solves a real problem on its own, and none of them answers the question left standing: where do the others go? Architecture is the agreement that answers that question before you need it. Without the agreement, every developer decides in the heat of the sprint, and the discount rule ends up in three screens again. With it, “where does this go?” has one answer, and it’s the same answer on Monday and two years from now. The four pieces below are FOCUS in full. Each chapter in Part III takes one of them apart; this one shows all four together, working, in the same gesture as always: adding a cappuccino to table 4’s tab. The handler that did everything Before the map, the pain. The “add to order” button in Rosie’s Coffee Shop app started small, grew with every order from the counter, and today looks like this, packed into a single method: Dart void onTapAdd(int table, Item item, int points) { // Validates the loyalty discount: business rule, right in here. var discountPercentage = 0; if (points >= 100) { discountPercentage = 10;

} // Builds and fires the network call, in here too. List items; try { items = api.saveItem(table, item); } on Exception catch (error) { // Handles the infrastructure failure in the middle of the logic. totalText = “Failed to add item: $error”; return; } // Recalculates the total and applies the discount to it. var totalInCents = 0;

for (final current in items) { totalInCents += current.priceInCents; } totalInCents -= totalInCents * discountPercentage ~/ 100; // Formats the screen text, in here for the fourth time. final dollars = (totalInCents / 100).toStringAsFixed(2); totalText = “Table $table: $$dollars”; } Line by line, and notice how each comment marks a different responsibility. The first block, four lines of code, decides how much the customer’s loyalty is worth: a hundred points buy ten percent off, and that’s a business rule from the coffee shop, written inside a screen method. The second calls the network and catches the exception right there: inside the catch , an infrastructure failure turns into interface text. The third recalculates the total, walks the items, and applies the percentage decided above. The last two lines format dollars and cents for the screen label. Four jobs, one method, thirty-four lines.

That code runs. The problem isn’t that it’s wrong today; it’s what it stops you from doing tomorrow. Three concrete roadblocks follow. The first: you can’t test the discount rule. To check that a hundred points buy ten percent off, the test has to build the screen, inject an API, and read the totalText field back, then compare a formatted string instead of a number. The rule sits in the if (points >= 100) lines, and it has no door of its own. The second: you can’t reuse the rule. The register closes the bill and needs the same discount; the monthly report needs it again. Since the calculation lives inside onTapAdd , the fastest way out is copying those lines into the other two spots, and that exact copy is what broke three screens in chapter 2. The third: the exception leaks into the logic. The catch sits in the middle of the method, so a dropped connection turns into a business decision in the same scope where the discount gets computed. Chapter 8 showed that an infrastructure failure should become a value at a single boundary; here it turns into screen text wherever the author was in a hurry. Try it: run https://focus.kodel.com.br/en/dart/10-02 (or https://focus.kodel.com.br/en/ts/10-02) and try exactly one thing: write a test that proves 120 points earn a ten percent discount, without instantiating TabScreen and without CoffeeShopApi . Prediction: you can’t, and that impossibility is this whole chapter’s argument. The path of a tap

Before the new code, the design. FOCUS organizes the program around a fixed direction, and that direction has a name. Unidirectional flow is the rule that an event travels up one path and state travels down another, with nobody calling back whoever called them: the View announces that something happened, the request crosses the pieces in one direction, and the answer comes back as a data emission, never as a nested callback. Chapter 13 goes deeper into this once it builds the real orchestrator. The gesture is the usual one. The customer at table 4 orders a cappuccino, the clerk taps the button, and the event leaves the screen: Two things in this drawing tend to surprise anyone coming from another architecture. The first is that the use case never talks to the repository: it receives the tab already fetched, as data, and hands back another piece of data. The second is that the orchestrator sits between two arrows instead of one, because it’s the one that fetches, calls, and writes.

Calls to the repository are ordinary conversations: you ask for table 4’s tab, and it arrives; you send it off to be saved, and you get a confirmation or a failure back. Nothing sits there listening. That matters, because most systems talk to an API or a database built by another team, where nothing like “let me know when it changes” exists, and the architecture can’t depend on a feature only some databases offer. The path back to the screen runs on a different mechanism: That arrow is an emission, not a call. The orchestrator has no idea the screen exists: it publishes a state, and whoever wants to listen does. That’s why the View never has to ask anything, and that’s where an app’s reactivity, when it has any, actually lives. It belongs to the orchestrator, not to the repository. The tap becomes an event, the event reaches the orchestrator, the orchestrator asks the repository for the current tab and hands the tab and the item to the use case, the use case returns a new tab with the total recalculated, the orchestrator sends it off to be saved and publishes the state the screen redraws. One full loop, and the screen never called anyone back. The four pieces in the same gesture

Now the thirty-four-line handler comes apart. No line disappears and no example starts over: each job it does today moves to the piece that has the right to do it. The View sends the event and draws what comes back The View is the piece the user touches. It has two verbs and no more: fire the event when something happens, and render the state when it arrives. Nothing migrates here from the naive handler. Not the total calculation, not the dollars-and-cents formatting: the text arrives ready-made, because formatting is deciding how information looks, and no decision belongs to the screen. The orchestrator assembles that text when it publishes the state, and chapter 12 shows the screen that only receives. What the View sends is an event, and an event carries the bare minimum: Dart // The event that rises from the View. Carries the table and the // item, and no total: total is the result of a rule, and rules // belong to the use case. sealed class TabEvent {} final class AddItem extends TabEvent { AddItem(this.table, this.item);

final int table; final Item item; } TypeScript // The event that rises from the View. Carries the table and the // item, and no total: total is the result of a rule, and rules // belong to the use case. type TabEvent = { readonly type: “addItem”; readonly table: number; readonly item: Item; }; Both listings say the same thing by different roads, and the difference is the one chapter 8 already covered. Dart declares a closed family with sealed class , and the compiler knows every member of it. TypeScript has no sealed inheritance, so it tags each member with a literal field ( type: “addItem” ) and lets the

compiler narrow the type on that field, a feature called a discriminated union. With one lone event the two look like overkill; by the screen’s fifth event, that same frame is what stops a switch from forgetting a case. Notice what the event doesn’t carry: no total, no discount, no formatted text. The View doesn’t know how much the cappuccino costs after the discount, and that’s exactly why it will never be the reason that calculation changes. Chapter 12 fills this box in with Flutter, React, or whatever your platform uses. The orchestrator converts event into state The orchestrator is the piece that receives the event, fetches whatever’s needed, calls whoever decides, and publishes the resulting state. If the name sounds new, the role doesn’t: it’s the BLoC or the Cubit in Flutter, the store in Redux or Zustand, the ViewModel on Android. From here on, this book calls the piece the orchestrator and nothing else, because your ecosystem’s name changes and the role doesn’t; the full mapping lives in chapter 13. It’s worth telling the orchestrator apart from the MVC (Model- View-Controller) controller, the habit you most likely bring with you. The classic controller tends to decide: it validates, applies a rule, picks a path. The orchestrator decides nothing. It sequences. What migrates here from the naive handler is exactly the sequence, that chain of fetching the tab, applying the change, and publishing the result, with none of the decisions that used to sit in the middle. What it publishes is a state, and the state is a closed value: Dart // The state that flows down to the View. Three cases, one

// exhaustive switch, the infrastructure failure already translated // into a value (ch. 8). sealed class TabResult {} final class TabUpdated extends TabResult { TabUpdated(this.tab); final Tab tab; } final class InvalidItem extends TabResult { InvalidItem(this.reason); final String reason; } final class InfraFailure extends TabResult {

InfraFailure(this.reason); final String reason; } Line by line. The first declaration opens the sealed family and has no body, because it exists only to name the set. TabUpdated carries the new tab, total already recalculated, and it’s the happy case. InvalidItem carries the reason as text and covers a rule rejecting something, an item with no price on the menu. InfraFailure carries the reason for the technical failure and exists because the network drops. Three cases, and the screen needs to know how to draw all three; the compiler collects on that in the switch . TypeScript // The state that flows down to the View. Three cases, one // exhaustive switch, the infrastructure failure already translated // into a value (ch. 8). type TabResult = | { readonly type: “tabUpdated”; readonly tab: Tab } | { readonly type: “invalidItem”; readonly reason: string }

| { readonly type: “infraFailure”; readonly reason: string }; The TypeScript version fits in four lines because the union is written in one shot, vertical bars separating the cases, instead of one class per case. The difference is syntax and origin: Dart models variants through sealed inheritance, TypeScript through a union of literal types. The practical effect is identical, and that’s what matters here. The tab’s orchestrator receives the repository dependency through the constructor, the way chapter 9 required, and chapter 13 fills this box in. The use case is the only one that decides The use case is where the business rule lives, and it’s a pure function in chapter 7’s sense: same input, same output, no touching network, database, clock, or screen. What migrates here from the naive handler are the four lines of the loyalty discount and the loop that recalculates the total. This is the migration that pays for the whole chapter: those lines were untestable inside the screen, and now they’re a function that takes data and returns data. The signature tells the whole story: Dart · TypeScript // The use case's signature. Chapter 14 fills in the body; notice it // takes data and returns a Result, with no repository along the way. typedef AddItemToTab = TabResult Function( Tab tab,

Item item, int loyaltyPoints, ); The three parameters are data, not collaborators: the current tab, the item coming in, and the customer’s loyalty points. No repository, so this function can’t reach the database even if the author wanted it to. The return is the TabResult you just saw, which means a rejected rule is a return value, not a thrown exception. In TypeScript the same contract is a three-argument function returning the same type, an identical gesture, and that’s why the two symbols share a single listing. Testing this function means calling it: build a tab, pass 120 points, check the total. No screen, no network, no fake. Chapter 14 fills in the body. The repository is the only boundary with the world The repository stores and returns data, and it’s the only piece that knows what’s out there. What migrates here from the naive handler are the two loose responsibilities left over: the network call and the try/catch that rides along with it. The exception doesn’t stop existing; it stops circulating. It’s born here, it dies here, and it leaves converted into a value, exactly as chapter 8 established. Dart · TypeScript // The repository's contract. Chapter 15 fills this box in with a // real database and network; here there are only two operations,

// on demand: find a table's tab and save the whole tab. Neither // one observes anything; whoever needs the data asks for it on the // spot and gets the answer back. Both return a Result because the // exception dies right here, at the boundary, and leaves as a // value (ch. 8). abstract interface class TabRepository { Future findTab(int table); Future save(Tab tab); } Two methods, and both are ordinary conversations: question and answer. Finding and saving are two of the four operations in CRUD (Create, Read, Update, Delete), the basic set of things you do with stored data, and that whole set belongs to the repository. You ask for table 4’s tab and it arrives; you send it off to be saved and get a confirmation or a failure back. Neither one sits there listening for a change, and that choice is deliberate. Continuous observation exists in some databases and is great where it exists, but it’s a special case: when the data lives in another team’s API,

or in a database that only answers queries, there’s nothing to observe. The architecture needs to hold up in both scenarios, so the contract stays at the common denominator. Notice both operations return TabResult , and that’s on purpose. Reading fails just as much as saving does: the server drops mid- query, the database refuses the connection, the table doesn’t exist. If findTab returned the raw tab, reading would be the one spot in the program where an infrastructure failure had no way to become a value, and this section’s opening sentence would stop being true. The price is one extra check in the orchestrator, which needs to look at what the fetch brought back before calling the use case. It’s a fair price for a promise that holds all the way through. In TypeScript the contract is an interface with the same two methods; only Future becomes Promise . The equivalence is trivial and earns the shared listing. Notice what the contract doesn’t have: no method called applyDiscount . Chapter 15 fills this box in. If your screen needs to refresh on its own, that job belongs to the orchestrator, which already publishes state to whoever’s listening. That’s where a timer, a manual refresh, a WebSocket, or a database’s watch (where the feature exists) comes in. The repository keeps answering questions, and the View still never knows where the data came from. Try it: run https://focus.kodel.com.br/en/dart/10-01 (or https://focus.kodel.com.br/en/ts/10-01) and change the discount from ten to twenty percent. Prediction: you’ll find the number in exactly one place, inside the pure function, and neither the screen nor the repository needs to be opened. Then run 10-02 and look for the same number there.

The same slice in your language You just watched the four pieces in Dart and in TypeScript. Nothing they do depends on those two languages, and the proof is the other eight, below. In every listing, look for the same sequence: the event that rises from the View, the state that flows down to it, the use case’s signature, and the repository’s contract. The names don’t change. What changes is how each language writes a closed set of cases, and what it charges you when a case gets forgotten. All ten implementations are published, they compile, and they print the exact same line: Table 4: $17.06 Kotlin writes the slice almost the way Dart does: sealed class for both families, data class for each case. The difference that matters shows up on the consuming side. A when over a sealed class is exhaustive by the compiler’s own obligation, so forgetting a case doesn’t compile. Kotlin sealed class TabEvent data class AddItem( val table: Int,

val item: Item, ) : TabEvent() sealed class TabResult data class TabUpdated(val tab: Tab) : TabResult() data class InvalidItem(val reason: String) : TabResult() data class InfraFailure( val reason: String, ) : TabResult() typealias AddItemToTab = (tab: Tab, item: Item, loyaltyPoints: Int) -> TabResult

interface TabRepository { fun findTab(table: Int): TabResult fun save(tab: Tab): TabResult } Swift swaps sealed inheritance for an enum with associated values, and the state’s three cases fit in three lines inside one single type. The switch is exhaustive by obligation too. Notice findTab : it returns the standard library’s Result , because in Swift the generic result type already comes built in and needs no inventing. Swift struct AddItem { let table: Int let item: Item } enum TabResult { case tabUpdated(Tab)

case invalidItem(reason: String) case infraFailure(reason: String) } typealias AddItemToTab = (Tab, Item, Int) -> TabResult protocol TabRepository { func findTab(table: Int) async -> Result<Tab, Error> func save(tab: Tab) async -> TabResult } C# writes everything with record , and abstract record plays the family’s role. This is where the first real gap shows up: the hierarchy is closed by convention, not by the compiler, so a switch over TabResult emits a warning instead of an error when a case is missing, and the code needs a dead arm that never runs. C#

public abstract record TabEvent; public sealed record AddItem(int Table, Item Item) : TabEvent; public abstract record TabResult; public sealed record TabUpdated(Tab Tab) : TabResult; public sealed record InvalidItem(string Reason) : TabResult; public sealed record InfraFailure(string Reason) : TabResult; public delegate TabResult AddItemToTab( Tab tab, Item item,

int loyaltyPoints); public interface ITabRepository { Task FindTab(int table); Task Save(Tab tab); } Java 21 has a genuinely closed set: sealed interface for the family, record for each case, and a switch with patterns the compiler collects on. The use case’s signature is the one spot that’s a nuisance, because Java has no type alias for a function: it becomes a one-method interface, tagged @FunctionalInterface . Java sealed interface TabEvent {} record AddItem(int table, Item item) implements TabEvent {} sealed interface TabResult {}

record TabUpdated(Tab tab) implements TabResult {} record InvalidItem(String reason) implements TabResult {} record InfraFailure(String reason) implements TabResult {} @FunctionalInterface interface AddItemToTab { TabResult apply( Tab tab, Item item, int loyaltyPoints); } interface TabRepository { TabResult findTab(int table); TabResult save(Tab tab);

} PHP has no sealed class. The closed set gets written by hand, in the union type of every signature, which is why TabUpdated|InvalidItem|InfraFailure reappears in full on every return type. It works. The price is the usual one: the day a fourth case is born, nobody gets warned, and you go hunting for the unions one by one. PHP final readonly class AddItem { public function __construct( public int $table, public Item $item, ) {} } final readonly class TabUpdated {

public function __construct(public Tab $tab) {} } final readonly class InvalidItem { public function __construct(public string $reason) {} } final readonly class InfraFailure { public function __construct(public string $reason) {} } interface AddItemToTab { public function __invoke(

Tab $tab, Item $item, int $loyaltyPoints, ): TabUpdated|InvalidItem|InfraFailure; } interface TabRepository { public function findTab( int $table, ): TabUpdated|InvalidItem|InfraFailure; public function save( Tab $tab, ): TabUpdated|InvalidItem|InfraFailure; }

Python closes the set in a single line, the named union TabResult , and every match over it gets checked against that line by the type checker, never by the interpreter. The models are frozen dataclass instances, chapter 7’s immutability enforced at runtime. The repository is a Protocol : the concrete class inherits nothing, it just needs the methods. Python @dataclass(frozen=True, slots=True) class AddItem: table: int item: Item @dataclass(frozen=True, slots=True) class TabUpdated: tab: Tab @dataclass(frozen=True, slots=True)

class InvalidItem: reason: str @dataclass(frozen=True, slots=True) class InfraFailure: reason: str TabResult = TabUpdated | InvalidItem | InfraFailure AddItemToTab = Callable[[Tab, Item, int], TabResult] class TabRepository(Protocol): async def find_tab(self, table: int) -> TabResult: ... async def save(self, tab: Tab) -> TabResult: ...

Go is the book’s counterpoint, and it’s where the design truly changes. There’s no union type: the state stops being a single value and becomes the pair (Tab, error) , the two failure cases become distinct error types, and the View swaps the exhaustive switch for a chain of errors.As . Forgetting a case still compiles. Notice TabData at the end of the listing: with no union, the value- plus- error pair needs to travel together in a struct to reach the screen intact. The architecture survives; the compiler’s safety net doesn’t. Go type AddItem struct { Table int Item Item } type InvalidItem struct{ Reason string } func (e InvalidItem) Error() string { return e.Reason } type InfraFailure struct{ Reason string }

func (e InfraFailure) Error() string { return e.Reason } type AddItemToTab func( tab Tab, item Item, loyaltyPoints int, ) (Tab, error) type TabRepository interface { FindTab(table int) (Tab, error) Save(tab Tab) (Tab, error) } type TabData struct { Tab Tab Error error

} Rust sits at the opposite extreme from Go. The algebraic enum declares the three cases as a single type, match is exhaustive by obligation, and the repository’s Result is the same Result the whole language uses. The immutability chapter 7 asked for is the default here, so there’s nothing left to lock down. Rust struct AddItem { table: u32, item: Item, } enum TabResult { TabUpdated(Tab), InvalidItem(String), InfraFailure(String), }

type AddItemToTab = fn(&Tab, &Item, u32) -> TabResult; trait TabRepository { fn find_tab(&self, table: u32) -> Result<Tab, String>; fn save(&mut self, tab: Tab) -> Result<Tab, String>; } None of this makes Go or C# bad languages for FOCUS. It makes them languages where exhaustiveness gets paid for with discipline and review, instead of billed by the compiler. Chapter 18 works through that bill gap by gap, with what to do in each case. All ten complete slices, orchestrator, use case, and repository filled in, run at the routes below, all under https://focus.kodel.com.br: Language Route Dart /en/dart/10-01 TypeScript /en/ts/10-01 Java /en/java/10-01 C# /en/csharp/10-01 Go /en/go/10-01

PHP /en/php/10-01 Python /en/python/10-01 Kotlin /en/kotlin/10-01 Swift /en/swift/10-01 Rust /en/rust/10-01 Try it: open your language’s route and Go’s side by side. Look, in both, for the spot where the loyalty discount gets calculated. Prediction: you’ll find it in both in under thirty seconds, and in both it sits inside the use case, alone, with no network nearby. Why just four Anyone who’s already taken a beating from layered architecture has an objection ready at this point, and it’s a fair one. Mozaic Works published the argument in full, in the article “Is Hexagonal Architecture Overengineering?” (https://mozaicworks.com/blog/is-hexagonal-architecture- overengineering): layers turn into folders, folders turn into interfaces with a single implementation, interfaces turn into indirection, and the team ends up writing five files to add one field, with nobody able to point at what got gained. The critique isn’t against separating responsibilities. It’s against separating for ceremony’s sake. The lineage the critique targets is well known. Alistair Cockburn described Ports and Adapters in 2005: the application talks to the world through ports, and the world adapts to them. Robert C.

Martin distilled the idea in the post “The Clean Architecture,” in 2012, and later in the book Clean Architecture, in 2017: concentric rings and the Dependency Rule, which states that the code’s dependencies always point inward, toward the business rule, and never outward, toward infrastructure and the screen. That’s the spine of the design you just saw, and chapter 6 already showed its technical half, dependencies pointing at abstractions. Both ideas are good, and neither one closes the subject, because neither says how many layers your coffee shop app needs. They say which direction the dependencies run. FOCUS’s answer to the critique fits in one sentence: what gets preserved is the dependency rule and the single boundary where an exception becomes a Result, not the ring count in the drawing. If your code respects both with four boxes, four boxes are enough. If someone adds a ring just to look like the picture in the article, that ring is exactly the ceremony Mozaic Works is calling out, and FOCUS agrees with the complaint. The criterion that settles this is simple to state and uncomfortable to apply: every layer pays its own way. A layer only earns its place if it can point at a verifiable gain that would vanish without it, and “organization” and “best practices” aren’t verifiable gains. Apply it to the four. The View pays because, in isolation, it can be swapped out whole (from Flutter to React, from mobile to web) without one rule line changing. The orchestrator pays because it creates a single spot where the screen’s state is born, and that’s what lets you reproduce a screen bug with no network involved. The use case pays the steepest price and gives back the biggest refund: it’s testable with zero infrastructure, and it’s where the calculation that broke three screens in chapter 2 lives. The repository pays because it concentrates in one file the only place in the program where an exception can be born.

Now the fifth layer, the one almost every project ends up proposing: a DTO (Data Transfer Object) mapper between the use case and the repository, meant to translate the domain model into the persistence model. The translation is necessary; nobody disputes that. The question is who owns it. It belongs to the repository. Look at what the repository knows that nobody else does: the database table’s column names, the date format some API returns, the field that came back as a string when it should have been a number, the foreign key the server demands. That’s someone else’s rule. The database wasn’t designed for your tab, and the tax system’s API even less so. What comes out of the repository is what the orchestrator and the use case asked for, in the shape they asked for it, because the contract is theirs. Turning one thing into the other is the job of whoever signed both contracts, and only the repository signed the third party’s. This isn’t a layer, it’s a data transformation. An adapter that converts someone else’s rule into ours, and it lives inside the box that already exists. The practical difference shows up the day the server renames a field: with the translation inside the repository, one file changes; with a separate mapping layer, the mapper changes, whoever calls the mapper changes, and both tests change. That’s where the rule behind the table’s “forbids” column comes from: the layers above never know the layer below’s model. The persistence model belongs to the repository and dies inside it. If your use case imports the class that represents the table row, it just inherited the database’s migration calendar, and the pure function you wrote in chapter 7 now depends on an ALTER TABLE . That’s why the prohibition is written down instead of assumed:

this is the boundary that leaks first, and it leaks with the best of intentions, to “avoid duplication” between two models that only look alike. At a coffee shop where the tab model and the tab’s database table share the same fields, a separate mapper costs one extra file and a field-by-field copy someone will forget to update. When the two models really do diverge, and in some systems they diverge a lot, the translation grows and earns its own name, file, and test. It stays inside the repository. What changes is the box’s size, not the number of boxes. There’s another reason, and it’s the most common one of all: what the screen needs is almost never what the database has to offer. The tab screen wants the item’s name, its price, and the total. The table also stores the date it was added, who rang it up, the shift ID, and a field left over from a 2019 migration. What the view needs is almost always less, and in a different shape: a trimmed-down entity, not everything available. Who defines that contract are the orchestrator and the use case, because they’re the ones consuming what got asked of the repository. The repository fills the order it received; it doesn’t hand over everything the database has and leave the checking to whoever called. I once worked on a system with seven carefully christened layers. I spent two weeks tracing why a new field never reached the screen and found that five of those seven did nothing beyond receiving an object, building another one with the same values, and passing it along. Nobody had the nerve to remove any of them, because each one had a respectable name and showed up in the diagram the consultancy had delivered. I didn’t remove any either, and that’s exactly why I’m writing this: my rule ever since

is that a layer that can’t say what it pays for is a layer that goes, and FOCUS has four because that’s as far as I’ve managed to answer that question. The recipe the orchestrator follows The two diagrams from the start showed who talks to whom. What’s left is showing the order, which is what you’ll reproduce every time you write a new orchestrator. It receives an intent from the View, and from there it follows a four-step recipe, numbered in the diagram: gathers the ingredients the use case is going to need, hands everything over at once, persists only what held up, and publishes one state, always just one. It never tastes the batter along the way.

Notice what leaves the use case and what reaches the View: a single value. Either it worked or it didn’t, and both cases travel back through the same path, in the same type. It’s chapter 8’s Result doing its job right here: the View doesn’t ask “did it fail?” before it draws; it draws whatever case arrived. There’s no

intermediate state sneaking out the side, no exception climbing outside the flow, and no second channel where the failure travels. One intent goes in, one Result comes out. That’s why the orchestrator is the only piece that talks to two others. It collects from the repository because the use case has no right to, and it calls the use case because the decision isn’t its own. The recipe stays the same every time, which is why chapter 13 can turn it into code you copy from feature to feature. Want to test business rules? Test the use cases. They’re the units of unit testing. Want to test integration? Test the orchestrator. It’s the piece of code that defines one action, from intent to the database or the API. Testing the View gets a lot simpler too, because all you need is firing the intent (the event) and checking how the result gets drawn. Pitfalls The first shows up the following Monday, and almost always with the same line: “it was just an if .” A last-minute request comes in, the tab needs to reject an item once the table has already closed out, and the closest spot to the keyboard is the orchestrator, which is already sitting there sequencing things. The symptom is an orchestrator that grows while the use case stays small; the rule’s test goes back to needing a repository fake. The way out is mechanical: if the line decides something about the business, it goes down to the use case, even if it’s three lines and even if the deadline is real. The second is the View that reads the repository directly, “just to show a counter.” It looks harmless, because it’s reading, not writing. The symptom shows up weeks later, when the counter shows a different number from the rest of the screen, because

now there are two sources of state and nobody keeps them in sync. The way out is having the counter born from the same state as the rest of the screen, published by the orchestrator, even if that costs one extra field in the state. The third is the hardest to spot, because the code looks clean: the use case that takes the repository instead of data. The signature turns into addItem(TabRepository repo, int table, Item item) and everything looks fine, except now the function fetches, decides, and saves. The symptom is the test that goes back to needing a fake and the function that can now fail from a network error. It’s an orchestrator wearing a use-case costume, and the way out is handing the fetch back to whoever holds that right: the use case always receives the tab already ready. Q&A What about when the use case has no rule at all? On a plain CRUD screen, it sits empty. It sits nearly empty, yes, and the layer stays. The real cost is one signature and one line that returns the validated data, and the payoff is that the day the first rule shows up (and on a real system, it does), there’s an obvious place for it, instead of a debate. I won’t pretend that cost is zero: on a screen that just manages menu categories, this layer is bureaucracy for a few weeks. The bet is that the software’s lifespan runs longer than a few weeks. What’s the practical difference between the orchestrator and my controller? The word “decides.” Most frameworks’ controllers validate, apply a rule, and pick a path, all inside themselves. The orchestrator only sequences: fetch, call, publish. If you open your orchestrator and find an if that talks about the business, it just turned into a controller.

Can a screen have more than one use case? It can, and it will. The tab screen adds an item, removes an item, applies a discount, and closes the bill, and each one of those is a use case with its own signature. The screen’s orchestrator knows all four; none of the four knows the others. Quick tip Before you write the line, say its verb out loud. “Draws” goes to the View, “sequences” and “formats” go to the orchestrator, “decides” goes to the use case, “stores” goes to the repository. A verb that doesn’t fit any of the four is usually two lines wearing one line’s clothes. Quick reference The table below has two columns instead of one because a dependency rule is easy to promise on paper and hard to collect on in code review. The “does” column is the promise; the “forbids” column is what turns the promise into something a reviewer can point at on screen, with no debate about style. “This if decides whether the discount applies, and it sits in the orchestrator” is a checkable sentence. “This code seems a bit coupled” isn’t. Layer Does Forbids View fires events business rules renders state data access Orchestrator converts event to state deciding

rules fetches data from the repository persisting calls use cases publishes state Use Case the only place for business rules IO is a pure function framework takes data, returns a Result domain exception Repository CRUD (fetch and save) business rules the only place an infra exception becomes a Result This table is the contract for the rest of the book, and chapters 11 through 17 cite its lines the way someone cites a statute. No line from the naive handler got thrown out along the way here: thirty-four lines in one method became four pieces with four responsibilities, and the output stays the same, Table 4: $17.06 . Notice what that half page does to reading. It answers where a rule may live and where it may not, without opening a single file. The property has a name: semantic compression, a design’s capacity to fit into a short description that still serves for deciding, and not only for describing. For a newcomer, the table’s eleven lines stand in for reading the naive handler’s thirty-four, and they keep standing when that code changes, because what they record is each piece’s intent.

It’s this book’s thesis in action, and this is the chapter where it can be said with all four pieces already on the table: architecture lowers the cost of change because it makes the intent of the system recoverable, navigable and predictable for humans and for models. Recoverable is finding the discount rule from the table, knowing nothing about the project. Navigable is landing on it in one F12 jump. Predictable is knowing, before opening the file, that it isn’t in the View. Exercises

  1. Fill in the table below unaided, without looking back at “Quick reference.” The eight cells are the contract the next seven chapters cite, and it’s worth rebuilding them wrong now and checking your answer, rather than just recognizing them when they show up later. Layer Does Forbids View Orchestrator Use Case Repository
  2. Take the naive handler’s first three lines (the ones that decide the discount percentage) and say which layer each one lives in. Then do the same with the line totalText = “Table $table: $$dollars”; and with the catch line. Answer key: the first three are use case, because they decide a rule; the text line is orchestrator, because assembling the state’s text is a decision,

and the View gets the text ready-made; the catch line is repository, because that’s the boundary where the exception is born. 3. Could you place, across the four layers, the flow for closing table 4’s tab and splitting it three ways between customers? Start with the event the screen fires, decide what it carries, and write the use case’s signature before anything else. If the signature needs the repository, go back and reread the third pitfall. Tip 10 If you don’t know which layer the code belongs in, it isn’t ready to be written yet. Next chapter: you’ll find out that the folder called views/ , with every screen in the app inside it, is usually the first place where this map gets betrayed.

Powered by TurnKey Linux.