Hello Stefan,
I had a brief look at the example application and the articles you linked to. Personally, I find the example application complex and difficult to understand. It is not something I would do myself.
If you strip down the clean architecture (and all the other architectures mentioned in Uncle Bob’s blog post) to its core principles, it’s about separation of concerns and source code dependencies. The inner parts should not depend on the outer parts and preferably be agnostic of the frameworks used by the outer parts. This is a good principle in my opinion and one that I try to follow in my own designs.
However, exactly what the layers are, or what the building blocks are, are not as important; they depend on what you are trying to build. Sometimes, representing individual use cases as their own classes and having the user interface call them is a good approach. Other times, it makes more sense to have the user interface call application services that encompass multiple use cases.
As for having separate input and output ports for the use cases (or application services), I would say it depends. If you are only going to have one implementation of a use case or a service, then it does not need to implement an input port interface - the user interface can access the implementation class directly. In other words, the class is its own input port. For the output port, there are more options:
- If the use case is stateful and is going to be called multiple times by the user interface, then it may make sense to have a separate output port interface that is passed as a constructor parameter.
- If the use case consists of one method only and will return either a result or an error, you don’t need a separate output port interface: you can just return the output or throw an exception, or return a result object that contains either the output or the error.
- If the use case consist of one method only and asynchronously produces output, you may need an output port interface that you pass in as a callback parameter to the method itself.
If you would have to communicate with a database, you’d define an interface that the use case (or application service) can use to communicate with the database. Today, this is often a repository but you can call it whatever you like. The use case (or application service) would get an instance of this repository as a constructor parameter. You’d then create an adapter class that implements this interface and talks to the database, for example using jOOQ.
As for the user interface design patterns, again, it depends on what you are trying to achieve. If you want to reuse code across different user interface implementations (such as, say, Vaadin Flow, Android and JavaFX), then you have to make some parts of the user interface framework agnostic. However, in practice, different user interface frameworks have different patterns and paradigms that makes it almost impossible to truly reuse user interface logic. You’d end up with a compromise that maybe gets the job done, but doesn’t really work nicely with any of the different user interface implementations.
If you know you are only going to build a Flow user interface, there is no inherent benefit in making some parts of your user interface unaware of Flow. It only makes things more complicated. Besides, your use cases/application services are already unaware of Flow, which should be enough separation.
Vaadin Flow does not have a proper data binding framework at the moment. I’m guessing this is the reason why you find a Binder
in the view model in the example application you are referring to. I agree with you that the binder should not exist in the view model. It should not exist at all in this case. Instead, the view model should expose observable properties that one or more views can subscribe to. This does not exist at the moment (unless you build it yourself), but hopefully will in the not too distant future.
Having the view model construct the view seems wrong to me. I’ve always thought of the view model as being shared with and observed by multiple views. It should be passed as a constructor parameter to the views who need it. However, this is also not black and white - it depends on what you consider a view here.
If you think of a view as a user interface component, then the view model should be passed as a constructor parameter. On the other hand, if you think of a view as a Vaadin route, it makes sense to let the view create the view model and pass it on to the other components. Sharing data between views should not happen through a shared view model, but through some other means, such as URL parameters or immutable data objects stored in the session.
The role of the controller in the example application I don’t fully understand, nor do I understand why it implements a RouterLayout
. I guess its role is to wire together the view and everything else, but I fail to see the benefits compared to having the view/route wire itself together (with a little help from Spring).
The presenter in this case implements the output interface of the use case, which kind of makes sense. However, in this particular case, is it really needed? What bad things would happen if the view model itself implemented this interface, and also called the use case?
With a proper data binding framework in Flow, I think you could get rid of both the controller and the presenter while still following the principles of the clean architecture and having a well-structured user interface whose code is easy to read and understand.
I’m not sure I answered your questions but these are my thoughts on the subject. I’d be happy to continue the discussion.
-Petter-