Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

28KB

FOCUS Architecture — Chapter-09: Explicit Dependencies: DI and the Composition Root

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

Explicit Dependencies: DI and the Composition Root In this chapter, you’ll: refactor the payment orchestrator so every dependency it has shows up in the constructor, visible in any signature you read; write the Composition Root in main and turn a forgotten dependency, the kind that takes down production today, into a compile error; decide, piece by piece, who gets an injected dependency and who gets plain data, following the asymmetry FOCUS embraces on purpose. Friday, 7pm, and the phone rings: no card goes through at Rosie’s Coffee Shop. The afternoon deploy shipped with every test green, the payment code didn’t change a single line, and yet the first charge of the night dies with an exception no signature announced. You’re about to find the hidden dependency that set this trap, drag it out of hiding, and put the compiler on guard in its place. Chapter 8 ended with a question hanging in the air: payTab was born with the card processor wired straight into the function body, so which door do the real processor, the server that goes down, and the rest of the failing dependencies come through? Failure already turned into a value; what’s missing is deciding

who hands the orchestrator the repository and the gateway capable of producing those failures. The answer has two parts, and each fits in one sentence. First: every dependency comes in through the constructor, where any reader can see it. Second: the whole object graph is born in a single place, next to the program’s entry point. The rest of this chapter exists so those two sentences stop sounding like bureaucracy and start being the reason you sleep better on a Friday. Four languages carry this chapter, each with its own job. Dart stays the running example’s mother tongue. Kotlin appears stacked alongside it, because a constructor with a field typed by contract is the identical gesture in both, and that equivalence is part of the argument. C# steps in because this chapter’s vocabulary (Composition Root, Pure DI) was born in the .NET community, and that’s where the difference between the pattern and the tool shows up most sharply. Python closes the chapter as a counterpoint: with no nominal interface, it tests whether the rule survives when the compiler doesn’t help. If your language is none of these, follow the Dart track; chapter 10 redoes the recipe in all ten of the book’s languages. The payment that fetched its own dependencies The pain comes before the technique, as always. Two terms need a definition before the criticism starts. Dependency Injection, or DI (Dependency Injection), is the name Martin Fowler coined in 2004, in the article “Inversion of Control Containers and the Dependency Injection pattern” (martinfowler.com), for a specific move: instead of a class fetching its own dependencies, someone outside hands them over ready-made. Fowler coined the term precisely to disambiguate the generic IoC (Inversion of Control), which back then named almost anything. A Service Locator is

the alternative he describes in the same article: a global registry, usually a map from type to instance, that any class can ask “give me service X” the moment it needs it. Rosie’s Coffee Shop’s payment orchestrator was written with the second option, and code just like it runs in thousands of apps right now: Dart // The locator: a global map from type to instance. class ServiceLocator { static final _instances = <Type, Object>{}; static void register(T instance) => _instances[T] = instance; static T get() { final instance = _instances[T]; if (instance == null) { throw StateError(“no instance registered for $T”);

} return instance as T; } } class PaymentOrchestrator { // The constructor doesn't mention any dependency. PaymentOrchestrator(); PaymentResult pay(int table, String cardNumber) { // The dependency shows up here, in the middle of the method. final gateway = ServiceLocator.get(); return gateway.charge(cardNumber); }

} Read the constructor. It says: “I need nothing.” A lie. The pay method depends on a PaymentGateway , but that information only exists buried in the body, on the ServiceLocator.get line. Whoever creates a PaymentOrchestrator() has no way of knowing it needs a gateway registered beforehand; the signature, the class’s public contract, hides the requirement. Now, Friday. Rosie’s Coffee Shop switched card processors, and someone wrote a NewProcessorGateway . The class was ready, its test passed, the deploy shipped. Except nobody called ServiceLocator.register with the new gateway in the right spot, and no tool caught it, because registration is a line of runtime code the compiler can’t tell apart from any other. The program compiled. The tests passed, and here’s the twisted part: they passed because every test registers its own fake in the global map before it runs, so the orchestrator’s test never exercises the production registration. At 7pm, the first customer of the night tries to pay, get can’t find the type, and it blows up: Friday 7pm: Bad state: no instance registered for PaymentGateway That line came from a real run of this chapter’s source, not from imagination. Three pains, then, each with a name. The signature lies: the constructor promises independence and the method demands a global registration. The composition error is a runtime error: wiring the program wrong only shows up once the program is running, at the worst possible hour. And the test requires global registration: every suite has to populate the map before it runs, which couples the tests to each other and, worse, hides the exact oversight that took down production.

Dependencies move up to the constructor The fix is short and has a christened name: Constructor Injection, the form of dependency injection where the class declares everything it needs as constructor parameters and stores the dependencies in immutable fields. No global map. The orchestrator now asks for the two contracts the DIP from chapter 6 required you to create, and notice that it asks for the contract, never the implementation: Dart · Kotlin // The contracts the DIP from chapter 6 required: one method each. abstract interface class PaymentGateway { PaymentResult charge(String cardNumber); } abstract interface class TabRepository { LookupResult findTab(int table); } class PaymentOrchestrator {

// The constructor declares everything the class needs. PaymentOrchestrator(this.repository, this.gateway); final TabRepository repository; final PaymentGateway gateway; PaymentResult pay(int table, String cardNumber) { // Step 1: find the tab; infra failure is already a value (ch. 8). final lookup = repository.findTab(table); if (lookup is InfraFailure) { return NoConnection(); } // Step 2: charge the card at the processor. return gateway.charge(cardNumber);

} } Line by line. Both contracts have one method each, and that’s deliberate: charge takes the card number and returns the sealed PaymentResult from chapter 8; findTab returns the LookupResult from the boundary that same chapter built, with the infrastructure failure already translated into a value. A smaller contract doesn’t exist. The constructor receives both dependencies and stores them in final fields; in Kotlin the gesture is identical, val parameters in the primary constructor, which is why the two symbols share a single listing. The body of pay is the flow you already know: find the tab, bail out if the infrastructure failed, charge the card. The logic didn’t change one bit. Only the origin of the dependencies changed. Compare the signatures of the two states, because the whole chapter lives in that comparison. Before: PaymentOrchestrator() , a promise of independence, a hidden requirement. After: PaymentOrchestrator(repository, gateway) . Whoever reads the constructor knows everything the class needs, without opening a single method body. The signature stopped lying. That’s what explicit dependency means in this book: not a moral judgment about the code, but the concrete property that the signature declares what the body consumes. If nobody calls the locator, who builds the graph?

It’s a fair question. The locator, for all its flaws, solved a real problem: somewhere, someone has to create the concrete ProcessorGateway and hand it to whatever depends on PaymentGateway . That “somewhere” now has a name and a fixed address. The Composition Root is the one place in the program where concrete classes get instantiated and wired to each other; the term comes from Mark Seemann, in the book Dependency Injection in .NET (Manning, 2011; second edition with Steven van Deursen, Dependency Injection Principles, Practices, and Patterns, 2019). The set of objects created and wired there is the object graph: every object is a node, every dependency is an edge. And the right place for that root is next to the entry point, the spot the platform calls to start the program. In Dart, Kotlin, and Python, that’s main . Three names, one idea: near the entry, and only there, the whole program gets assembled. Rosie’s Coffee Shop’s main ends up like this: Dart · Kotlin void main() { // Compose the IO boundary: the concretes are born here, and only // here. final repository = ServerRepository(); final gateway = ProcessorGateway(); // Wire the orchestrator to the gateway and the repository.

final orchestrator = PaymentOrchestrator(repository, gateway); // The rest of the program just uses the ready graph. print(paymentScreen(orchestrator.pay(4, “5090 1112”))); print(paymentScreen(orchestrator.pay(4, “5090 1117”))); print(paymentScreen(orchestrator.pay(4, “5090 1119”))); } Six lines of composition. The first two create the concretes at the IO boundary: the simulated repository and the processor that declines any card ending in 7. The third wires everything together: the orchestrator is born already holding both dependencies, complete from its first instant. From there on, the program just uses the graph; no class below main creates a dependency, none asks a global registry for anything. Here’s the graph, with the creation arrows kept apart from the usage arrows:

There’s a gain here that never shows up in the compiler and only gets charged during maintenance. The question “which concrete implementations does this program use?” has, with the composition root, one file for an answer, and it fits on one screen. With the locator, the same question is answered by reading every file that calls the registry, because each of them decides on its own what to fetch, and there is no place where the whole wiring is written down. Whoever joins the team on Monday pays that difference once per dependency; whoever gets the task without ever having opened the project pays it every time, because there is no way to know the other files exist. Now, time to cash in the lead’s promise. Repeat Friday’s mistake in this new code: pretend the new gateway arrived and that you, in the rush, forgot to hand it to the orchestrator. Delete the gateway line and its argument in the constructor call. The program doesn’t even get to run. Dart’s answer, a literal transcript: Error: Too few positional arguments: 2 required, 1 given. final orchestrator = PaymentOrchestrator(repository);

Kotlin gives the same refusal in a different accent: error: no value passed for parameter ‘gateway’. The exact same oversight that used to sail through compiler, tests, and deploy to blow up at 7pm on a Friday now dies on your screen, in seconds, and the error points straight at the line. No new test was written for this; the constructor’s signature became the spec, and the compiler became the one on call. Try it: run https://focus.kodel.com.br/en/dart/09-01 (or your language: https://focus.kodel.com.br/en/kotlin/09-01, https://focus.kodel.com.br/en/csharp/09-01, https://focus.kodel.com.br/en/python/09-01) and delete, inside main , the line that creates the gateway , along with its argument in the constructor call. Prediction: in Dart, Error: Too few positional arguments: 2 required, 1 given. ; in Kotlin, error: no value passed for parameter ‘gateway’. ; in C#, error CS7036 . In Python the program does start and stops on the TypeError from composition’s first line, and the counterpoint section explains why that difference matters. Pure DI before any container Time for C#, and the choice is historical: this chapter’s vocabulary was born in the .NET community, in Seemann’s book, and .NET is where the confusion between the pattern and the tool shows up the most. There, “doing DI” became synonymous with “using Microsoft’s container,” and this chapter exists to undo that fusion. First, the same Rosie’s Coffee Shop graph, composed by hand in Main , with no library at all; Seemann named this form Pure DI, dependency injection in its purest state, just constructors and the composition root:

C# // Pure DI: the whole graph composed by hand, right here in Main. var repository = new ServerRepository(); var gateway = new ProcessorGateway(); var orchestrator = new PaymentOrchestrator(repository, gateway); Console.WriteLine(PaymentScreen(orchestrator.Pay(4, “5090 1112”))); Console.WriteLine(PaymentScreen(orchestrator.Pay(4, “5090 1117”))); It’s Dart’s main with semicolons in the right accent. Nothing new, and that’s the thesis: DI is this, dependencies in the constructor plus a single place of assembly. If you forget the gateway here, the platform’s compiler answers with error CS7036 , “There is no argument given that corresponds to the required parameter ‘gateway’”, transcribed from a real compile of this chapter’s source. Now, and only now, the tool. A DI container is a library that assembles the graph for you: you register which concretes implement which contracts, and the container resolves the chain of constructors on its own. The SAME graph, registered in the platform’s official container, Microsoft.Extensions.DependencyInjection: C#

// The SAME graph, now registered in a DI container. var services = new ServiceCollection(); services.AddSingleton<ITabRepository, ServerRepository>(); services.AddSingleton<IPaymentGateway, ProcessorGateway>(); services.AddSingleton(); var provider = services.BuildServiceProvider(); var fromContainer = provider.GetRequiredService(); Console.WriteLine(PaymentScreen(fromContainer.Pay(4, “5090 1119”))); The three AddSingleton lines tell the container what Main told it with new in the Pure DI version: which concrete serves each contract, and that the orchestrator exists. BuildServiceProvider freezes the registration, and GetRequiredService asks for the finished orchestrator; the container looks at the constructor, sees the two contracts, finds the registered concretes, and assembles everything. No business class changed. The orchestrator doesn’t know whether it came from a new or from a container, and that’s exactly how it should be: the container lives in the Composition Root and never leaks past it.

My position, so you can calibrate your own: I don’t use a container in an app whose graph fits inside a 30-line main , and most of the apps that have passed through my hands fit that description. The cost is real (a forgotten registration turns back into a runtime error, as GetRequiredService for an unregistered type proves in two seconds) and the benefit, at that size, is zero. A container starts paying for itself once the graph has dozens of nodes and distinct scopes, one instance per request on the server, one per screen in the app; that’s when the chain of constructors you’d wire by hand turns into expensive upkeep, and the tool takes over. Even then, notice: the pattern stays identical, dependencies in the constructor, composition at the root. A container is assembly convenience, never a requirement of the pattern. The Python counterpoint: discipline instead of syntax Python takes apart a common excuse: “my language doesn’t have interfaces, so DI doesn’t apply.” The same graph, without a single nominal contract: Python class PaymentOrchestrator: # The constructor declares everything; there's no nominal # interface. Any object with find_tab and charge works # (duck typing).

def init(self, repository, gateway): self.repository = repository self.gateway = gateway def main(): # Compose the IO boundary: the concretes are born here, and only # here. repository = ServerRepository() gateway = ProcessorGateway() # Wire the orchestrator to the gateway and the repository. orchestrator = PaymentOrchestrator(repository, gateway) This part of the text is language-specific, and three differences deserve attention. The first: there’s no PaymentGateway declared anywhere. The constructor accepts any object that has find_tab and charge with the right shapes; it’s duck typing as usual, the contract exists, just in the team’s heads instead of in a file. If you ever want that contract checkable by a tool, the standard library’s typing.Protocol declares the same shape without coupling to any implementation, and one sentence about it is

enough for now. The second difference is a trap built into the language itself: every importable Python module is an accidental singleton, because every import of the same module hands back the same module object, with the same state. The temptation to write gateway = ProcessorGateway() at the top of a module and import it everywhere is the Service Locator back in slippers. The rule that holds the door is discipline, not syntax: compose everything inside the entry module’s main , never in the body of an importable module. The third difference is the one that justifies the discipline. With no compiler, a main missing the gateway doesn’t turn into a compile error; this chapter’s broken source fails like this, a literal transcript: TypeError: PaymentOrchestrator.init() missing 1 required positional argument: ‘gateway’ Notice when this blows up: at instant zero of the program, on composition’s first line, before any customer’s request. That’s the best a dynamic language can offer, and it beats the alternative by a mile: with a locator, that same oversight would wait for the first charge of the night. Constructor Injection plus Composition Root pull the error toward the cheapest moment the platform allows; with a compiler, before the program runs; without one, at second one of execution. DI for the boundary, data for the rest

If dependency injection is so good, why didn’t calculateTotal(items, customer) from chapter 7 get an injected repository? Look at it: the signature is still the same as in that chapter, it takes the list of items and the customer, and returns the total. It didn’t change in this chapter, didn’t get an interface, didn’t enter main ’s graph. And that wasn’t an oversight. This is the FOCUS asymmetry: dependency injection is for the IO boundary, and only for it. Repositories and gateways talk to the server, the database, and the card processor; they fail, they carry latency, they cost money to hit in a test, and that’s where an injectable contract pays for itself, because the fake that replaces the processor in tests comes in through the same constructor (chapters 15 and 17 explore both pieces in detail). Use cases are the opposite: pure functions that take data and return data, the way chapter 7 built them. Injecting a repository into a use case would make it impure and tie it to infrastructure; FOCUS prefers the orchestrator to fetch the data at the boundary and hand the function ready-made values. Dependency for whoever touches the world. Data for whoever calculates. Two serious critiques deserve an answer before the close, because both have serious authors. The first attacks this chapter’s villain for going too far. Seemann has argued since 2010 that Service Locator is an anti-pattern, for the three reasons you lived through in the opening: hidden dependency, a lying API, error deferred to runtime. Jimmy Bogard, the creator of MediatR, answered in “Service Locator is not an Anti-Pattern” (jimmybogard.com, 2022) that the conviction generalizes too far: inside composition infrastructure, where a framework needs to resolve types it only learns about at runtime, calling the resolver is legitimate and unavoidable, and the C# listing’s own provider.GetRequiredService is exactly that. Both are right on their own turf, and the synthesis is operational: resolving a service inside the composition root is part of assembling the graph; outside the

root, never. What Friday condemned wasn’t the map’s existence, it was the BUSINESS orchestrator asking the map for a dependency in the middle of a method. The second critique attacks the remedy for ceremony: “you create an interface for every single class just so you can test it, and that’s noise.” I agree with the general diagnosis; the CUPID from chapter 6 already flagged that a single-implementation interface, created by reflex, is dead weight, and Seemann reached the same conclusion by a functional path: he showed that pure dependencies don’t need a contract to be swapped out. FOCUS’s answer is the asymmetry you just saw: an interface only where a real IO boundary exists ( PaymentGateway , TabRepository , and each one earns its rent on the first test with a fake); no interface for use cases, which are pure functions and get tested by calling them with data. No IDiscountCalculator . The critique is right about the reflex and wrong about the target: the problem is an interface with no boundary, not the boundary’s interface. Pitfalls The first pitfall is the locator with a new badge. It rarely introduces itself as ServiceLocator ; it shows up as a context , appState , or services object that the whole codebase receives and that every method reaches into for whatever it wants ( context.gateway , context.repository ). The signature says “I take the context,” which is the same as saying nothing, and all three pains from the opening come back intact. The fix is the one this chapter taught: every class declares in its constructor exactly the dependencies it uses, and the grab-bag object dies. The second is setter injection: build the object empty and hang the dependencies on it later, orchestrator.gateway = ProcessorGateway() . Between the constructor and the setter there’s a half-built object

that compiles, gets passed around, and blows up with a null the moment someone uses it too soon; the constructor’s signature went back to lying, only now with a window of time attached. If a dependency is mandatory, its place is the constructor, no exceptions. The third is the dissolving root. The project starts with the graph in main , and six months later there’s a new ProcessorGateway() inside a screen, another in a helper, a third in a test that turned into production code; each one of those new calls is a clandestine piece of Composition Root, and swapping the processor now takes a hunt through five files. The symptom is easy to measure: if you need more than one place to swap an implementation, the root has dissolved. Pull the creations back together, next to the entry point. Q&A What about get_it, Hilt, or Koin? Isn’t the book going to teach me how to use them? No, on purpose. All three are DI containers, and the criterion from the Pure DI section decides for you: small graph, compose it by hand in main ; graph with dozens of nodes and distinct scopes, adopt whichever container your platform has already blessed. The pattern this chapter taught doesn’t change in either case, and learning a container’s API takes an afternoon once the dependencies already live in the constructors. Doesn’t a constructor with lots of dependencies turn into a monster? It does, and that’s a feature. A constructor asking for eight dependencies is a class confessing it does too much; the locator hid that confession, the constructor prints it. The fix isn’t going back to hiding it, it’s splitting the class, and the SRP from chapter 6 tells you where to cut.

Do only classes get injection? What about functions? Same idea, different clothes: a function that takes the dependency as a parameter (or a closure that captures it at creation) is Constructor Injection without the word class . What matters is the property, visible in the signature, assembled at the root; syntax is just a detail of the language. Quick tip Run grep -rn “ServiceLocator.get|GetIt.I|getIt<” lib/ (adjust the names to your project) and look at every result that isn’t inside main : that list is the map of your code’s hidden dependencies, in order of on-call risk. Tip 9 Explicit dependencies show up in the constructor; hidden dependencies show up on call. Quick reference FOCUS piece Gets DI? What it gets View no the ready state; no business dependency Orchestrator yes the IO boundary’s contracts, through the constructor Use Case no data as

parameters; stays a pure function Repository/Gateway is the endpoint implements the contract; the concrete is born at the root Exercises

  1. The cash closing below hides the locator in exactly two methods. Migrate it to Constructor Injection and write the Composition Root in main ; the exercise is in Dart, and it’s worth translating to your language of choice before you solve it. Dart class CashClosing { CashClosing(); String dailySummary(int totalInCents) { // First hideout: the clock comes from the global map. final clock = ServiceLocator.get(); final day = clock.now();

return “Closing for ${day.day}/${day.month}: " “$${totalInCents / 100}"; } void closeTheDay(int totalInCents) { // Second hideout: the printer, too. final printer = ServiceLocator.get(); printer.print(dailySummary(totalInCents)); } } Check your result against the orchestrator listing: the constructor should declare ShopClock and Printer , both get calls should disappear, and main should create and wire all three pieces. 2. Four classes from Rosie’s Coffee Shop each need a decision: interface and injection, yes or no? Justify each answer using the FOCUS asymmetry before you look at the answer key. The

classes: PaymentGateway , TabRepository , the discount use case from chapter 7, and a ReceiptFormatter that takes a PaymentResult and returns the receipt’s text. Answer key: PaymentGateway and TabRepository get an interface and enter the graph through injection, because both sit at the IO boundary, fail for real, and earn a fake on the first test. The discount use case is a pure function, it takes items and customer as data and gets no interface at all; injecting it would be ceremony. ReceiptFormatter is the trick question: it looks like a “service,” but it takes a value and returns a value, without touching the world; it’s pure logic, tested by calling it, and it also goes without an interface. If you gave it an interface “just in case,” reread the second critique from the asymmetry section. Next chapter: you’re holding the pieces forged since chapter 4: simplicity, contracts, pure functions, failure as a value, and now dependencies composed at the root. Chapter 10 opens Part III by snapping all of them into a design that fits on a single page, the whole FOCUS architecture at once.

Powered by TurnKey Linux.