Pergunta

So I'm currently trying to write a project using Clean Architecture. Its a Unity Engine project which doesn't make the task any easier.

The Issue I'm running into however is much more basic and has to do with the fact that all examples for Clean Architecture use cases I have seen appear to be rather simple mostly CRUD and without multiple steps.

Now the (high-level Cockburn-definition) Use Cases I want to implement involve a couple of steps:

  1. User creates an element
  2. User puts information about the element
  3. User creates additional elements and links them to the first one

Now in the traditional use case sense as described by Clean Architecture with an input-port and an output-port I do not know how to model this workflow as a single use case as it spans multiple UI elements with their own presenters and the workflow itself is being triggered from deep within a presenter hierarchy that has nothing at all to do with the execution of this workflow aside form starting it.

I do want to keep the application as loosely coupled as possible so I do not wish to introduce tight-coupling via some god-object that holds references to all needed presenters and implements the output port.

I have a solution that is rather hands-off by simply having most of the UI listen to state changes in the repositories however this as discussed today with colleagues leads to a bit of confusion of how the flow of data works and we have people opting for having use-cases drive UI state changes instead as in:

outputPort.ChangeToInputDetailsState()

which I think completely breaks the principle that use cases should not care about how they are called etc.

If anyone has any example of use-cases in Clean Architecture requesting further information or in general any source of information on using Clean Architecture with more complex use-cases in a UI driven environment I would be quite happy.

Edit: More details as requested

To elaborate a bit on what I mean with spans multiple UI elements since it was asked in the comments. I can't give details on the specific project but I can try to make an analogon.

Use case (Cockburn definition) is something like:

Name: Record and Upload Video

Primary Actor: User

Steps:

  1. Record Video
  2. Annotate and Cut Video
  3. Upload Video to Server

Now these span multiple UI elements in the sense that steps 1, 2 and 3 require different parts of the UI with their own sublogic to complete, which themselves are used from multiple points. I'm not sure that this use-case definition can be mapped 1:1 to a Clean Architecture use-case. If it can be then the Input/Output port would have to be able to orchestrate all these different UI parts which themselves might be used by different use-cases.

Elaborating on it like that I think that my issue also contains a variant of the issue of reusing UI components in a Clean Architecture case where there might be more than one use-case associated with a single controller/presenter.

Edit 2: Elaborating on just using functions on the outport port that return after getting the necessary info.

The issue I see here is that in Clean Architecture Output Ports/Presenters to me always looked dumb (in the sense of barely any logic) and that orchestrating all the business logic falls into the realm of use cases.

If I were to however only call another use-case to get my additional information instead of passing it back into the presentation layer I would not be able to open another user interface as the use cases don't even know about it.

Foi útil?

Solução

OK, I think you you have a few misconceptions about the architecture itself and that this is tripping you up, so let me try to sort that out.

enter image description here

So, domain behavior (the business logic of the application) resides within the combined Use Cases + Entities layer. How you split that up internally is up to you, but the guideline is to put the "application independent" (core) domain logic/rules within the entities (domain model with behavior), or within objects that share that layer with the entities (anemic data structures + functions). However, if you don't actually have several applications that share application independent logic, or if it's just not perfectly clear what that should be, most of the good stuff will probably end up in the Use Cases layer, though - and that's OK.

The issue I see here is that in Clean Architecture Output Ports/Presenters to me always looked dumb (in the sense of barely any logic)

I think you have a slight misconception about what input and output ports are. You have a certain domain behavior: it needs some input, then it does something, and produces some output or some side effect. The input is received through the input port, which is just an interface to this domain code - it's either an actual interface type, or a base class, or just the public interface (public methods and properties) on some object (the "Interactor"). So, the input port is not a dumb component (barely any logic) as it represents an abstract view of the business logic provided by the use case (and, hopefully, the use case involves some logic of value). Yes, the implementation of it resides in the use case, but the methods (the capabilities, the things that tell us what it can do, and thus, if it's a smart component or an anemic data structure, are defined by the interface of the input port). The data structures (or parameters) that pass through it, however, are dumb - as these are just plain data structures.

Similarly, an output port is an abstract interface to some external component (a view, a service, a data store) - and how dumb/smart it is depends on the nature of the component it represents. (When I say "external", I mean external to the Use Cases layer).

Note that this is a generalized view; in some cases, you may decide to merge the two ports into a single one (e.g., if you don't need anything to inherit from the Output Port, or if you do all the output through data structures returned by the Interactor interface).

BTW, if you are using the Observer pattern or events, then the abstract Observer interface, or, in the case of events, the signature of the event handler, is a concrete example of an Output Port.

Not a UI Pattern

I'm not sure that this use-case definition can be mapped 1:1 to a Clean Architecture use-case. If it can be then the Input/Output port would have to be able to orchestrate all these different UI parts which themselves might be used by different use-cases.

Clean architecture is not a UI pattern; it does not demand that each UI element has a Controller, a Presenter, and an Interactor with an input and output port. That's not what it's about at all. Strictly speaking, the Controller and the Presenter aren't really a part of the architecture (aren't really prescribed by it), the relationships across the boundary are. The Controller and the Presenter are there because that's how UI-related logic happens to be structured in the example (MVC/MVP being a common pattern). And the Interactor has nothing conceptually to do with UI. It just handles how the "outside world" interacts with the domain logic and vice versa. A use case is implemented by one or more objects in the Use Cases layer that work together, the Interactor being an entry point - a Facade if you will.

The input and output ports don't orchestrate UI - they don't know about the UI, and they certainly don't know about the fact that it's composed of parts. That's presentation logic. A use case might have several input and output ports, for different purposes - e.g, one for user input, one for input by an automated system, one that outputs to the screen, one that outputs to a database, or a web server. And these ports may be connected to actual components, or to a test case (say, in TDD), or a test double.

So, if you have UI elements that are (to some extent) generic, so that they can appear on multiple screens, in the context of different use cases - that is absolutely fine. The inset to the right does not say that this is a structure for every UI element - it's just an example of the flow initiated from the UI side.

In terms of the presentation logic, any orchestration the UI needs happens in the the presentation layer. But the actual work on the objects that are of business interest to the users is delegated by the UI to an Interactor. Often, a screen will use a single Interactor, but there's no reason why it can't reference two or three of them to support different but related use cases, or why several screens that represent a UX-related breakdown of the same use case can't share the same Interactor.

The Architectural Boundary

OK, so you have some business logic, a use case, that requires some input data to do something. The data that you pass in is, in a generalized way, referred to as "The Input Model" ("model" just means a "representation" - as in, you've studied the problem and understood it in a certain way, and came up with all these concepts, ideas, data structures, etc., that you are going to use to represent that problem in code). The input model could be an actual data structure, or just a parameter list on a method. It's a dumb thing; no behavior, just data that you can toss across the architectural boundary. It's really a part of the Input Port interface - it's "owned" by it, and it's defined in the same layer (Use Cases).

Note that both the Input Port and the Output Port are just interfaces that capture how the Interactor and some other component "talk" - so they will likely contain a few methods for different aspects of that interaction, and each method will have it's own Input and/or Output Model (basically, what parameters they take, and what they return, or pass to other components as parameters). The thing to remember is that all those methods and any data structures and types that get passed through them are on the boundary of the layer; the insides of the layer are encapsulated behind them. Any code in the outer layers that needs to interact with an inner layer is programmed in terms of those elements that are on the boundary. It's basically the API of the layer.

Multiple Screens per Use Case

When there's a multi-step process involved, with several screens, one viable option is to use a standalone data structure for the Input Model, because then you can pass it between multiple views (but read on, there are cases where you might want to do it in a different way).

Now, collecting input is really a part of the presentation logic. The presentation layer depends on the application business rules layer, in the sense that it will make use of the interfaces defined there. (Again, I'm using the more general notion of "interface"). Specifically, it is going to use the Input Model to pass data to the Interactor. You'd give both the Input Model and the Interactor a more meaningful name, BTW. When implementing presentation layer code, you can pass the input model from screen to screen, and collect user input along the way. Or just could inject it in all the relevant screens at the start, when they are assembled. On the final screen, invoke a method on the Interactor and pass in the data structure containing the complete input for the use case.

Sometimes, it's good enough to save the data locally (to prevent loss of work in case of a crash); in other scenarios, you may want to upload it somewhere at certain steps (or after every step). Either way, don't make assumptions about this; talk to your users, the domain experts, to find out what's actually necessary, what's acceptable, and what's not needed. Don't make things more complicated then they need to be.

The process may involve keeping track of some strictly UI/UX-related stuff. E.g. you may need to track if some panel is collapsed or not, or if some data is hidden - but this happens to be just for user convenience (say, it helps them focus on something when they work), and isn't due to business logic. In that case, it doesn't make sense to place that kind of data in the Input Model, especially if you are at a stage in the project where the idea of what the UI should be hasn't stabilized enough - because then whenever you need to introduce a change, the Input Model would get in the way. The idea is confine the UI/UX-specific stuff within the presentation layer. Several options there. You could create a composite structure that contains the Input Model and the extra data. You could derive from the input model if the structure and the language allow it. Or if the language makes it easier that way, you could use a different, specialized data structure to collect the data, and copy the relevant fields to the Input Model in the last step (or if the input model is just a parameter list, pass the relevant properties as parameters). Do the simplest thing that works given the tools at your disposal.

If you have business logic in a middle step that's different from the final use case (when all the data is ready), then create a separate use case for it, and call that. On the other hand, if, for example, you need to upload at some middle step, but there's really no domain logic involved, and it's all "logistics" or CRUD-like, you can bypass the domain-related core of the application completely, and call a (possibly special-purpose) Gateway directly. Note that this doesn't break the dependency rule - coupling within the layer is allowed, but it should be minimized/controlled, as always. If future changes do introduce business logic, restructure and go through the Use Cases layer; if not, just leave it as is.

The Flow of Control

If I were to however only call another use-case to get my additional information instead of passing it back into the presentation layer I would not be able to open another user interface as the use cases don't even know about it.

I suspect there may be some design problems; e.g., you may have business logic intermingled with presentation logic in your presenters. If so, try to untangle that, but not everywhere at once, but incrementally - start at some small part of the codebase that's actively being developed, and then fix the others as needed.

In any case, think about the flow through the application. Also think about the flow of your screens. Sometimes, there will be side effects that need to be shown immediately on the same screen - in that case, push them through the (presentation) Output Port, or use the observer pattern (events). Other times, you just show a completely new screen; you can use the same strategy there, but there's also an option to pull the data by calling a method on the Interactor.

If several screens are sharing a use case, then the use case doesn't need to know anything about the screens being changed (from its perspective, all that is happening is that methods are being called on it by something, at different times). If each screen calls a different Interactor, then the flow is as follows. You invoke a method on an Interactor, it does something, and possibly returns something in one way or another; the UI handles that, and switches to another screen, and then the process repeats, but with a different screen and a different Interactor. Think through these flows, decide what's presentation logic, what's business logic, what the use case is really about, and try to find balanced design that's not too complicated.

Outras dicas

While it's of course technically possible to make the Clean Architecture work in any scenario, it actually breaks down quite quickly when the application grows.

It kind-of works in CRUD applications, as you've mentioned, but it's not an accident there are no other examples to be found. Also, if you take a look at Uncle Bob's own project, you'll see it's quite cumbersome (i.e. non-maintainable) even on smaller scale.

What I'm saying is, it is the wrong design to be using, especially in a UI-heavy scenario. The Clean Architecture says the UI is a detail, when in this case it is obviously not (and I would argue neither in most other cases).

In clean architecture, a use case consists of three components: a request, a handler and a response.

A request contains user input. Note that the request itself is also input, so it’s not impossible to have an empty request, although typically requests contain at least an id for a resource.

The handler belongs to this specific request and nothing else. The handler is like the glue in the data flow:

  • It validates the request (you might want to delegate this to separate request validators)
  • it gathers necessary data from the database or external API
  • it creates (initializes) domain objects with this data and the data in the request
  • it invokes the domain functionality associated with the request
  • finally, if all goes well, it persists the new domain state, sends out events, notifies API, etc.
  • returns a response, if necessary

In your question you mention three steps. You have to ask yourself if these are three separate requests, or is it perhaps a single request? Maybe you can do these three steps within the client side UI layer, before creating a single request containing all the data to invoke the domain logic / business rules.

In my experience this clean architecture data flow works best for commands.

For queries this flow quickly becomes tedious because you typically have many more of them not to mention paging and sorting etc. If we assume commands persist valid state, we can also assume all queries contain valid state.

Licenciado em: CC-BY-SA com atribuição
scroll top