Reputation: 1531
Do "Application Detail(s)" specific adapters, gateways, and the like, belong in a Use Case Interactor? Or; are Interactors meant to be clean from all adapter abstractions and only interact with Entities and return a POJO?
I have one question and one question only, the one above. All else is just me trying to prove my point or back up my question
Note: What Uncle Bob calls "Application Details" are things like Networking, Databases, API Calls, and methods of delivery (in general)
So, basically...
// This... ? (Includes a Network and a DB "Application Detail")
public function completeSale(saleDTO, networkAdapter, dbAdapter) {
sale = transformSaleDTOToSaleEntity(saleDTO);
transaction = null;
// This is all based on there being "SOME KIND OF" implementation
// of a "network" and a "database" concept... meaning we're tightly coupled
// to a concept even though its implementation is hidden.
if (netowrkAdapter && dbAdapter) {
networkResults = networkAdapter.completeTransaction(sale);
if (networkResults) {
transaction = new Transaction(sale.id);
transaction.timePurchased = sale.timeCreated;
transaction.isCompleted = networkResults.completed;
dbAdapter.save(transaction);
}
}
return transformTransactionToTransactionDTO(transaction);
}
// Or this... ? (No adapters or "Application Details")
public function completeSale(saleDTO) {
sale = transformSaleDTOToSaleEntity(saleDTO);
transaction = new Transaction(sale.id);
transaction.timePurchased = sale.timeCreated;
transaction.isCompleted = true;
return transformTransactionToTransactionDTO(transaction);
}
I have looked and looked. I cannot find any reference to what EXACTLY constitutes as a "use case", within Uncle Bob's Clean Code Architecture. There is lots of information about how they orchestrate things, how they abstract and hide implementation, sure. But, the more common conversation(s) I tend to see about "Clean Code Use Case Functions" usually talk about stuff that leaves me with more questions than answers. Typically, I read about how the use cases are interactors and how they alone interact with the data layer. When I read about what a use case is (at a high level) as per Uncle Bob's definition and examples, the "use cases" in these interactors are basically just a UML concept of "use cases". Or at least all of his blog posts and live talks seems to go over the use cases like abstract UML use cases and he never gets into the real code or concepts other than just words as a black-boxed user would know. These use cases describe an abstract set of steps that are SO abstract, one cannot really use them to just start coding right away. You have to interpret his concepts and I believe I might be failing to do so.
I know how to use dependency injection with adapters or gateways in the use case function itself. This hides the implementation from the use case, and instead just calls the function name from an interface. But, technically the use case in the interactor function now always must check for the adapter in question and then fire the interface function. So... the use case kinda "knows" about the "application details"... doesn't it? It doesn't know the implementation but it still needs to ask, "are you there, adapter? If so, do x".
I have heard talk of "Uncle Bob's Use Cases" reading like a written sentence (almost) due to the high level of abstraction. But yet, I don't know how you could do that if you had to include "Application Details" in your use case interactor method. I have yet to see anyone mention what level or scope of "Application Detail" specific code, lands in these use case interactor methods. Do we use adapters via dep-injection here and make fetch calls that return from the API calling function as a POJO? Or is it highly inappropriate to call "Application Detail" specific functionality in the interactor methods?
I am trying not to be specific by saying that the adapters are injected via "constructor" or "method injection" since all languages can use Clean Code, supposedly. But, all languages do not contain interfaces/classes. Anyway...
I noticed that Uncle Bob's UML use cases always include things like "Update cart" or "Complete transaction". So my assumption was that he is injecting the adapter into the use case and simply calling a function like "completeTransaction". To this, I could imagine him saying,
"we don't care that there is a network call to be made, we just ask if there is a network adapter present in the parameter list and if so, call the function we require via the interface. A function called
completeTransaction
".
It doesn't matter what the implementation of the completeTransaction
function is to the use case function, it just calls the function name inside the adapter and lets the adapter worry about the implementation and the requirement of the function name.
That all being said... now I am tightly coupled to needing "at least 1 Network Adapter", which means my use case function both "knows about the network adapter" but "doesn't care about what it's up to".
Uncle Bob says:
"You’ll be able to run your tests without delay. You’ll be able to defer decisions about the UI. You’ll be able to test business rules without the UI present. That kind of flexibility and decoupling always speeds you up."
However, if I include adapters or gateways that use "Application Detail" specific libraries and or classes, then my tests are immediately blocked by needing to start mocking that service. If I started with a use case that used 4 different "Application Detail" specific service(s) (i.e. network call, DB, notification system, and state update) then I would start by immediately being blocked by 4 mocked services...
How does this speed us up and let us use TDD? It's pretty hard to drive your development with tests when each test starts by being blocked multiple ways.
I should not be using anything related to any "Application Detail" (such as Network calls, Databases, etc) within a use case no matter how abstract the function seems to be. I should only have 100% business logic in my use case interactors and when I return a POJO from my use case function, the controller that called the use case should then do "Application Detail" specific functionality. I think perhaps Uncle Bob is wanting us to make the domain-level use case functions only interact with Entities and Business Objects in the next layer down (Data), rather than doing anything with injected adapters and controllers. If you remove all non-business-logic-related concepts from your use case, then you are freed up to test without mocking and use these Use Case Function(s) more throughout your codebase, or other codebases in the future (with similar domain knowledge)
UML Use Cases always depict an abstract wording of a concept that requires an Application Detail to accomplish (it seems). Example:
To me, the 5th step 100% requires a network connection, models, transformation abilities, and so on. So, how is it helpful or easy to test (as in TDD) if my fifth step of this use case is to do something like networkAdapter.completeTransaction(transactionDTO)
. Again, as I said, I would have to them mock the network calls and worry about the following conditions: "is there a network connection?", or "is the network on an expensive connection?".
Also, doesn't it change the use case to include the Network Adapter into a use case function? For example, the following could change:
to now be:
From this example, you can see that it's really unclear how to handle the edge case "I have a network connection, but no DB present".
If I include "Application Detail" specific invocations in my use case functions, and I want to use this business logic on multiple front-ends, then suddenly things become too crazy. Let's say I am using a language (like C#) that can code to use several different viewing devices (i.e. Windows Tablet and Windows Desktop). In this example, let's say you're on the road and have access to your local DB but no network on the tablet. Well, if you baked "Application Specific" invocations into your use case function, then you can't really always use that function now on all device types since desktop doesn't really care about the same level of "connectedness". You can't "code once, use everywhere" this way. You have to now make 2 different use case functions, one that cares about network connection a lot more than the other.
Upvotes: 3
Views: 2825
Reputation: 365
I believe another blogpost answers your question: A Little Architecture, where he gives a small code example of an architecture created with the Dependency Inversion Principle.
This way your Use Case code does not depend on networks or databases, only on the interfaces that abstract them away. In your unit tests you can then give your own (in memory) implementations of these interfaces, without any need for external APIs.
Upvotes: 1
Reputation: 166
You are missing one layer which Interactor (or Usecase) will call for CRUD. As you might guess the name ... Repository layer! The interface of Repository is defined in Interactor (Domain) layer and implementation is injected in application level. So, Interactor doesn't know anything about network calls, local data persistent, but it is able to call them.
Upvotes: 2
Reputation: 335
First of all, I dislike the way common concepts get renamed and recycled in Uncle Bob's book. What we see here is a somewhat recycled concept of Ports and Adapters (Hexagon) pattern unnecessary made more complex and confusing.
Use case interactor is what most other people call Application Service or Command Handlers (if you do CQRS). They do not deal with concrete implementations of any network or database access. Instead, they have references to interfaces (Ports) and send messages to those ports to make the internal domain model interact with the "outside world". That means:
Basically, the 'Entities' layer deals with POJOs while the "Interactor" gets and sends away those POJOs through Ports.
Upvotes: 4