Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thoughts about the ViewModel #29

Open
ahmedk92 opened this issue Feb 10, 2023 · 0 comments
Open

Thoughts about the ViewModel #29

ahmedk92 opened this issue Feb 10, 2023 · 0 comments

Comments

@ahmedk92
Copy link
Owner

ahmedk92 commented Feb 10, 2023

The following is a typical view model in an MVVM setup:

class PostsViewModel {
  @Published var posts: [Post] = []
  
  func viewDidLoad() {
    // 1. Get an array of Post objects from the model layer...
    // 2. Update the observable exposed `posts` property with it
  }
}

It acts as a mediator between the view and the model. It exposes observable “output” properties to which the view can bind, and exposes “input” methods (or observable properties) that respond to user actions. No problems so far, however, two things about that I can’t get over:

  1. Who defines the view model’s API?
  2. How much logic can the view model contain?

Who defines the view model’s API?

In a codebase where there are no hard boundaries between layers, this can go unnoticed. However, in codebases that follow an architecture that defines hard boundaries between layers, one can spot such nuisance easier. Let’s take the Clean Architecture as an example. A typical codebase that follows this architecture will probably define the following modules/packages for each layer (either for the whole project or per feature):

  • Domain
  • Data
  • Presentation
  • UI

The hard dependencies will be like this:

pako_eNpdjjEOwjAQBL9ibZ18wAWVGzokoLvmFB_EAp8j51ygKH_HUCG61e5otBumEgUe98rL7C6B1LnAxm4cDy6UzElJyU5VVlFjS0X_puvxW_wSGJCldiB28_ZREmyWLATfY-T6IJDuneNm5fzSCd5qkwFtiWwSEvdDGf7Gz1X2N-nBN_M

You can notice here the UI module depends on the Presentation module where the view model is defined. So, this is a clear decision that the view has no say in defining the API of the view model. However, I believe that for the view model API to make sense, i.e. convenient to the needs of the dependent view, then it needs to closely resemble how the view is actually structured and how it behaves. And, I4 think this is what really happens in a codebase where such hard module dependencies do not exist. The view is envisioned first, and the view model is modeled after it for convenience. If the view model is designed truly without the view being in mind, I can see the complexity creeping into the view to adapt to the view model API, which kinds of defeats the point of the view model being a convenience to the view.

My proposal here is that maybe the view model can have its protocol defined in UI while its implementation still living in Presentation. The dependency graph above may look like this:

pako_eNqFjrEOwjAQQ38l8tz-QAamLGxIwJbl1Bw0glyq9DJUVf-dQ3wAniz7SfaOqSaGx7PRMrtbiOJMgZTcOJ5cqIWy_MJL45VFSXOVf6W19zMGFG7GJBvYv1yEzlw4wptN1F4RUQ7jqGu9bjLBa-s8oC-JlEMm-1XgH_Re-fgAU9g3wA

But we now have a regression. Presentation can now see irrelevant public UI components such as view controllers. To fix this, maybe the view model protocols can have their own module:

pako_eNp1T7sKwzAM_BWjOfkBD528dCgE-pi8iFhtTGMrODKhhPx71XYMvUmc7o67FXoOBBYeBafBXJzPRuFQ0LTtwThOGPOP7ArNlAUlct49r8cvdYu0nDRx7AoL9zzOf7x7ITSQqGhi0Drrx-ZBBkrkweoZsDw9-LypDqvw-ZV7sFIqNVCngEIuoq5IYO84zrS9ARo7SFc

Overkill? Maybe. But I think the intents are clearer that way.

How much logic can the view model contain?

Now, regardless of where the view model (or its protocol) should be defined, I also have an issue with the responsibilities of the view model implementation. I think I won’t be so wrong if I claim that 90% of the code in the view model has to do with interaction logic and UI state management. Things like calling the right application service/model/use case/repository/whatever when the relevant UI event happens, or saving what items did the user selected on a UI list. A clever developer may offload/delegate as much of such logic to separate components, especially when context-independent logic is added to the mix like pagination or input validation. However, the view model will still be the gateway to such components.

All this feels to me way more than a “view model” should handle. In fact, I like to think about the view model being…just a model of the view; a pure data structure where the brain of the app can interact with it as if it is interacting with the actual user-visible view without all its framework baggage. Similarly, the view can bind to it without having to do non-trivial conversions.

So, the proposal here is:

  1. Reduce the view model to a DTO-like structure.
  2. Move the interaction logic, state management, and view model update to a separate component (be it called an interactor, logic controller, …etc).
  3. View models are injected to both the view and the interactor.

I made a simple project demonstrating this idea here. You may notice the view model in this example might be breaking some good practices in class design like exposed mutability. However, I don’t see the required complexity to work around that worth the effort. Even more, I think sometimes we get too obsessed applying such practices that it reversed some gains made possible by Swift’s features, but hopefully I will discuss this separately in a later post and link it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant