bespoyasov / frontend-clean-architecture

React + TypeScript app built using the clean architecture principles in a more functional way.
https://bespoyasov.me/blog/clean-architecture-on-frontend/
2.35k stars 258 forks source link

Is the domain level independent of the UI layer? #9

Closed denvifer closed 2 years ago

denvifer commented 2 years ago

First of all, thanks for the interesting example, Alex.

But... I'd like to ask one question.

I've seen many attempts to implement clean architecture when working on web interfaces, but actually, I face the same problem every time.

The specific of the web development is that we want to update UI pointwise, instead of re-rendering the whole page. And usually, we use tools that allow us to do it effectively. I mean React, Redux, and MobX are the tools that allow us to do local UI updates, right? But we pay for that by using the API provided by these tools.

Let me clarify what I mean on the example of your application.

Why do you create a new cart object with a new product array here instead of mutating this? It's because React forces you to do this, isn't it? If you don't create a new copy, React will not re-render your Cart component. But it means that the logic inside the domain layer depends on the UI layer. If you decide to replace React with VueJS, or for example if you decide just to add MobX to your React application, approaches in the domain namespace will not work anymore or at least, they will not work effectively.

That's why from my point of view, clean architecture actually is not as perfect for the front-end apps as on paper.

What do you think about this?

bespoyasov commented 2 years ago

First of all, thanks for the interesting example, Alex.

Thanks for reading! Glad you found it interesting!

Why do you create a new cart object with a new product array here instead of mutating this? It's because React forces you to do this, isn't it?

It isn't.

The point of creating a new object is in treating the code and data as immutable. React doesn't force me to do it, functional approach does.

I prefer modelling the core of an application in functional style. This implies immutability, stateless data transformations, and pure functions. This style allows to model the domain of an app in a simplest possible way—as a chain of pure data transformations.

The chain of transformations naturally requires taking data and returning the transformed result. The example you're asking about is exactly this—a result of a transformation.

(More about this approach you can find in “Domain Modelling Made Functional” by Scott Wlaschin.)

In general, I tend to first model the domain and only after that go to the application layer and the UI. By that point, I can present the core of all use cases as a set of domain functions. And to do that, it's more convenient for me to return data rather than use side effects on an object.

The use cases then can be modelled as “Functional Core in Imperative Shell”, which is more comfortable to reason about than composing side effects.

Again, this approach implies that the main logic of an application (the domain layer) is organized as pure data transformations.

The UI then only connects to the application layer. It so happens that this example uses “immutability” but it isn't related to the UI frameworks.

If you decide to replace React with VueJS, or for example if you decide just to add MobX to your React application, approaches in the domain namespace will not work anymore or at least, they will not work effectively.

I perceive this as a more general matter—composition.

MobX basically composes side effects. That means creating new object as a result of an action “is not applicable anymore”. So, yeah “functional style” might not work with it, but it isn't a concern of the domain, application, or UI.

If I were to design an app composing side effects, I also would start with the domain:

class UserCart implements Cart {
  protected products: Product[] = []

  public addProduct(product) {
    this.products.push(product)
  }
}

No notion of a UI framework in the example above. I still would model the application core this way first and connect MobX in the end. To connect MobX I'd need something like makeObservable inside constructors or maybe use a factory + decorators to keep the domain untouched.

The idea is the same though: model the domain, model use cases, connect the UI. Use cases can be modelled in different ways (as functions, or classes with DI, or whatever) but the conceptual splitting between different “layers” is the same.

Ideas aren't different, the composition is.

I'm not really fond of composing side effects. I prefer composing functions (or transformations) rather than effects because it's more testable and easier to understand. That's why in my example the code treats data as immutable and models the domain in a “functional way”.

That's why from my point of view, clean architecture actually is not as perfect for the front-end apps as on paper.

  1. Final decisions depend on the project's needs and constraints; there's no approach suitable for every project.
  2. Never claimed it to be perfect, just described and showed the concept.

The problems we discuss, of course, are related to the architecture in a broad sense. But the concept in the post is flexible. For example, in the post, I mention that from all of it you might need only a couple of ideas:

So if I can't use the concept along with a tool “as written” I'd try to “bend” concept so it “serves enough under circumstances” and adapt the tool to the project needs. It might not work, so I'd weigh pros and cons of using either beforehand.


P.S. I didn't consider mobx-state-tree in the answer above because it's kinda opinionated. With it, yes, the library would need to become “a part of the domain” just for the ability to model the domain objects and transformations. (I'm not a fan of this but because the JS world lacks better native tooling to reactive updates it might be a compromise.)

If, by this issue, you meant this case, then I agree—the concept of clean architecture “as written” wouldn't work. But it still would help “bend” the concept and get useful ideas from it even though constrained by the tooling.

denvifer commented 2 years ago

Thanks for such a detailed answer!

In general, yes, that's what I meant. If you use the current approach, it will not work with MobX (or will not work well). If you start with domain classes, they will not work with Redux. It's a problem because you still put some concepts that are motivated by the UI layer to the Domain layer in the end.

And one more comment. You said

The point of creating a new object is in treating the code and data as immutable. React doesn't force me to do it, the functional approach does.

I'd say, React forces as well. If you provide mutable models, it will be impossible to organize proper well-performant re-rendering logic in the components tree.

bespoyasov commented 2 years ago

In general, yes, that's what I meant. If you use the current approach, it will not work with MobX (or will not work well). If you start with domain classes, they will not work with Redux.

I still think it's a matter of composition and a paradigm rather than the UI affecting the domain. (I mean, the “functionalness-or-object-orientedness” affects the architecture in general, but it's a more fundamental issue.)

I'd say, React forces as well. If you provide mutable models, it will be impossible to organise proper well-performant re-rendering logic in the components tree.

I might be wrong but I can spot a contradiction: if React forces against mutable models then MobX (a counterexample) shouldn't be a thing.

I don't know about MobX's performance though (maybe it's terrible at rendering big trees). But I'd guess, since it's production-ready, it's high enough 😃


Anyway, I can agree that the UI leaking into the domain isn't a good thing but at the same time lots of tools on the frontend do that. I'd suppose it's because of a narrow native tooling and a multi-paradigm nature of JS but I don't see how it can (or even should) be fixed right now.

My default course of action is to avoid this as far as I can. That is think business-logic first, choose tooling that supports this idea. When it isn't possible—try to minimise the coupling and consider the dependency direction.

Hope this answers the question!