DEV Community

Cover image for Do We Really Need to Separate the Model from the UI?
Manuel Vila
Manuel Vila

Posted on • Updated on • Originally published at liaison.dev

Do We Really Need to Separate the Model from the UI?

Typically, domain models and UI views are completely separated. A few years ago, we had a good reason to do so because the views were mostly made of imperative code. But now that we have functional UI libraries (e.g., React with hooks), wouldn't it be possible to gather everything together, and implement the views as methods of the models they represent?

Object-Oriented Approach

For example, let's say we have a User class defined as follows:

class User {
  constructor({firstName, lastName}) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's add a React component into this class:

class User {
  // ...

  View = () => {
    return <div>{this.getFullName()}</div>;
  };
}
Enter fullscreen mode Exit fullscreen mode

And create an instance:

const user = new User({firstName: 'Arthur', lastName: 'Rimbaud'});
Enter fullscreen mode Exit fullscreen mode

Then, to render a View for this user, we can do:

<user.View />
Enter fullscreen mode Exit fullscreen mode

This is a perfectly valid JavaScript/JSX code, and I don't think there is something wrong with it. Conceptually, the View() method is no different than the getFullName() method. They are just methods returning a different kind of view: getFullName() returns a string and View() returns a React element.

Functional Approach

In a typical React app, we would not do so though. We would separate the view from the model like the following:

function UserView({user}) {
  return <div>{user.getFullName()}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Then, to render UserView, we would do:

<UserView user={user} />
Enter fullscreen mode Exit fullscreen mode

Does this more verbose approach bring benefits? No matter how much I scratch my head, I don't see any. The code is just more scattered.

Decoupling

It is always good to decouple the pieces of an application as much as possible.

But does the functional approach (React components implemented separately as functions) brings more decoupling than the object-oriented approach (React components implemented as methods of a model)?

It doesn't. Getting the models from a parameter or accessing them through this makes no difference. In both cases, models and views become tightly coupled.

Separation of Concerns

Some might argue that it is good to separate the model from the view because they are two different concerns. I don't get it. Again, how, in the object-oriented approach, the getFullName() method is different than the View() method? Both are returning a representation of the model, so why should we separate them?

It remembers me of the discussion about separating HTML and CSS. Yes, they serve two different purposes. HTML describes the content and CSS describes the presentation. But I don't think there is something wrong about putting them together in a cohesive way.

Sharing One Model with Multiple UIs

Let's imagine we are building an app for several platforms: a web app (with ReactDOM) and an iOS app (with React Native).

In this case, we usually want to share the same model with all platforms, and implement different UIs for each platform. To achieve this, we can implement the model separately and subclass it to implement the different views.

Refactoring our previous example, we define the User model in a separate file:

// shared/user.js

export class User {
  constructor({firstName, lastName}) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, we subclass it to implement the views of the web app:

// web/user.js

import {User as BaseUser} from '../shared/user.js';

class User extends BaseUser {
  View = () => {
    return <div>{this.getFullName()}</div>;
  };
}
Enter fullscreen mode Exit fullscreen mode

And the same goes for the iOS app:

// ios/user.js

import {User as BaseUser} from '../shared/user.js';

class User extends BaseUser {
  View = () => {
    return <Text>{this.getFullName()}</Text>;
  };
}
Enter fullscreen mode Exit fullscreen mode

Physically, the code is then a bit more scattered, but logically, it is not. Whichever the platform, from a User instance, we have access to both the model (user.firstName) and its views (<user.View />).

Composition over Inheritance

« Inheritance is evil. »

« Composition is the way to go. »

I am tired of hearing that all the time about anything and everything.

Yes, single inheritance in static languages (Java, C#, etc.) may not be the right approach for composing the multiple pieces of an application. But it is not true with JavaScript where inheritance is dynamic, and therefore, extremely flexible.

For example, we can use mixins to enable any kind of inheritance: multiple, conditional, parameterized, etc.

There are many ways to implement mixins in JavaScript, but there is only one good way, and it is incredibly simple. Please head over here for a nice explanation.

Conclusion

I tried the object-oriented approach when implementing the RealWorld example with Liaison, and I think it worked pretty well. Encapsulating the views into the models made the code a lot more cohesive than if the views were implemented separately.

If you are skeptical (you should be), please have a look at the code and tell me what you think.

Since most of the models are implemented in the backend, the frontend models are pretty much just composed of views.

Some might think that the classes are a bit crowded. I guess it is a matter of taste. Personally, as long as the content is related, I don't mind large files. If you prefer small files, you can group some views into mixins and assemble them into a single model.

This article was originally published on the Liaison Blog.

Top comments (8)

Collapse
 
mrispoli24 profile image
Mike Rispoli

So to me this is one of those things that seems unnecessary when an app is small and/or new but eventually you code yourself into a corner and realize where the idea of separation of concerns came from.

The best example in recent history is wanting to change your front end framework. If you have good decoupling you likely can leave your models in tact and migrate in a new framework in pieces. These days it seems like react is going to take us to the end of the earth but I remember quite painfully buying into a highly coupled angular 1 app, then buying into another highly coupled ember system only for them to die and having to so revolutionary migrations.

It’s also easier for later maintainers who anticipate models and views living differently as they handle different things. Moving these two together on a larger codebase can leave one feeling like they are looking for the glasses in someone else’s kitchen. Some of these conventions help us reason about things and find our way around an unfamiliar code base.

Good decoupling really does pay dividends over time in my opinion. At the outset it seems like a burden but there usually comes a day where you wish you had effectively decoupled different concerns in your code.

Collapse
 
mvila profile image
Manuel Vila

Thanks, Mike, for your detailed comment.

Everything you said is valid, but from my experience building a lot of small to medium single-page applications, the "model" in the frontend doesn't contain much business logic.

Since the backend is the source of truth, it has to implement all the business logic, and if there is some logic running in the frontend, it is probably backend logic that we duplicate to make our apps feel more reactive (i.e., optimistic updates).

In my case, since I use JavaScript for both the frontend and the backend, when I need to share some business logic, I implement it into a shared package.

So, in the end, my frontend models are mostly composed of views.

Of course, if my backends were not implemented in JavaScript, or if I was working for Google or Facebook, it would be a completely different story.

Collapse
 
johnkazer profile image
John Kazer

If you have a single global state, then for me the separation of concerns is between data manipulation (of the state) and the view (which renders the state).

Collapse
 
mvila profile image
Manuel Vila

Do you think this separation brings some benefits?

Collapse
 
johnkazer profile image
John Kazer

Yes for sure. I work on relatively simple apps. And I also don't mix my html as JSX. I actually use Pug templates with a virtual DOM.

All my app logic is in pure functions which change the state and trigger vdom update. Seems clear and safe.

This approach can be componentised using messages, a bit like the original object oriented concept or actor model. I haven't really used this approach yet, but seems reasonable in theory!

Thread Thread
 
mvila profile image
Manuel Vila

With your approach, don't you feel that your code ends up being unnecessarily scattered, and therefore, a bit difficult to maintain?

Thread Thread
 
johnkazer profile image
John Kazer

I don't create components in the React sense. I create a set of functions which manipulate the global state and a SPA Pug template (with mixins for modularity - but any HTML and CSS template approach would do). The functions can be grouped according to broad type (e.g. immediate actions, async effects, subscriptions to events...).

For me separation of concern is between business logic and appearance. Anything which is 'state' is controlled by functions, separately from HTML or CSS which are just expressions of state.

So I suppose at the end of the day I don't really agree with the implementation of React components. I try to re-use function and logic specifically rather than contained pieces of a website.

Collapse
 
danielrusnok profile image
Daniel Rusnok

In my humble opinion there is no need to separate model with view if model does not contains bussiness (domain) logic.