Guide to Frontend Business Code Architecture in MVVM Pattern

Introduction

In modern frontend development, the MVVM (Model-View-ViewModel) pattern has become a crucial design pattern. Unfortunately, there are some misconceptions about it, leading to messy code, difficulties in maintenance and extension, and performance issues in frontend projects. In this article, I will delve into the essence of the MVVM pattern and provide what I believe is the correct understanding of MVVM. I will guide you step by step on how to better organize and build business code using MVVM, explaining in detail why this approach is better, ensuring a complete understanding of the business code architecture in the MVVM pattern.

Understanding MVVM

Screenshot 2023-10-31 16-03-10.png

When searching online for an understanding of MVVM, you will likely come across the above image. However, this image is too abstract, leading to significant differences in interpretation among different people.

Common Misunderstandings of MVVM

The most common mistake in understanding the MVVM pattern is not grasping that its essence is an architectural design pattern. Many people think that data-driven development is MVVM, believing that automatic rendering of the page after changing data is MVVM, and some even think that implementing two-way data binding is MVVM. Data-driven and two-way data binding are patterns designed to solve specific problems, and they cannot be considered architectural design patterns. There is a fundamental difference here. When these concepts are misunderstood as MVVM and applied to architectural design, it becomes challenging to achieve good maintainability and scalability.

Here are two common misunderstandings:

Misunderstanding 1: "View" corresponds to frontend frameworks like React, "Model" corresponds to state management tools like MobX, and "ViewModel" is tools like MobX-React that link state and components. (Reference: Link)
Misunderstanding 2: A Vue component represents the MVVM pattern, where the component's data is the model, the template is the view, and other computed properties and methods are the viewModel.

Clearly, both of these interpretations focus on implementing two-way data binding, failing to address how to structure the code.

My Understanding of MVVM

Typically, the description of each layer in MVVM is as follows:

The view encapsulates the user interface and any user interface logic, the viewModel encapsulates presentation logic and state, and the model encapsulates business logic and data. The view interacts with the viewModel through data binding and function calls. The viewModel observes the model, transforms and aggregates data to display in the view, and coordinates updates to the model.

This description explains the functions of each layer but fails to answer the question of where the code logic should be placed within each model or viewModel. This is crucial.

My understanding of the MVVM pattern is:

A model encapsulates the capabilities and properties of a business entity; a viewModel encapsulates a business function or process; the view encapsulates the user interface, divided based on the business represented by the view.

I believe that the core and starting point of using the MVVM pattern to structure code are not pages but business. Each model or viewModel should only encapsulate the logic related to its own business, leading to a well-defined code separation. Therefore, a model should represent a business entity, and there should be only one model for each business entity. The viewModel represents a business function, and there should be only one viewModel for each business function. As shown in the diagram:

Screenshot 2023-11-02 14-57-33.png

Let me explain in detail below.

Model - Business Entity

An entity is a concrete or abstract object that objectively exists, with properties and capabilities. In an e-commerce business, for example, products, shops, and customers are concrete entities, while orders, reviews, and addresses are abstract entities, all of which should be abstracted into models.

The key point in abstracting models is that a model can only maintain and implement the properties and capabilities of the business entity it represents. It should not interfere in unrelated business entity matters.

For example, a product model needs properties such as name, image, description, color, and inventory, as well as methods representing its capabilities, such as increasing or decreasing inventory or modifying its description. The product model should not contain information about the shop, even if the product details page needs to display shop information. However, the way a product is displayed is not a functionality a product should have. Each entity should have only one model, so we cannot create a product model in the details page to display shop information or create a product model in the order page to display order or customer information. This reflects the principle that MVVM should be based on business rather than pages when structuring code.

It is also essential to note that a business entity does not have the ability to manage similar business entities. For example, a product cannot create another product, delete itself, or view other products. Therefore, a model cannot abstract the management functions of it's business entities.

In this case, where should this logic be placed? It should be placed in the business entity that has these capabilities. For example, the owner and employees of a shop have the ability to manage products, so these functions should be abstracted into the models of the shop owner and employees. Here, you may notice that even though these are all functions related to products, they are all placed in the employee model. It may seem inappropriate and challenging to organize code in this way. The best solution is to abstract a manager model for business entities that need to be managed. If a business entity has this capability, it inherits this manager.

ViewModel - Business Function or Process

A business function is an operation, task, or activity related to a specific business. In e-commerce, for example, purchasing a product is an activity that includes selecting a product, choosing its color and size, placing an order, and making a payment. This business activity should be abstracted into a viewModel. During the execution of this function, the viewModel should call the product model to display product information and select the product's color and size, call the customer model to place an order, and call the customer and order models for selecting a payment method and making payment.

ViewModel only cares about the events of its business function and does not need to be aware of the existence of the view. After implementing a viewModel, even without a page, you can complete the entire business function just by calling the viewModel's methods.

View - User Interface and Interface Logic

The user interface is the interface where software interacts with users, including the layout of pages, information display, and logical interactions.

In the MVVM pattern, the interface is generally divided into logical components and UI components. Logical components are responsible for communication between UI components and viewModel, while UI components are responsible for displaying information and interactive features. UI components should also be divided based on business, and you should not simply split them into three parts because a page has three sections.

Generally, a PC page is more complex, and one page can complete one business function, so this page contains a logical component responsible for maintaining a viewModel and several UI components, handling communication between them, and usually managing page layout. For many mobile pages, completing one business process may require multiple pages. Therefore, these multiple pages should be in one logical component, and multiple components in multiple pages should communicate with the viewModel through this logical component.

Why This Architecture Is Good

1. Exceptional Maintainability

This architecture achieves maximum decoupling between code modules, making the code highly maintainable. This is mainly evident in the following aspects.

1. Easy Code Reusability

Firstly, model reusability. A software may have many functions and business processes, but these functions and processes revolve around a few business entities. After abstracting these business entities into models, each business function is just calling the methods of these models. Models are not aware of viewModel, making it easy to reuse them. Moreover, because the functionality of business entities is clear, making changes and enhancements to models during reuse is not likely to have side effects.

Secondly, viewModel reusability. A software may support multiple platforms, such as PC, mobile, and tablet, each with an HTML5 version and a client version. Interaction or interface frameworks between platforms may differ, but business processes will remain the same. With this architecture, each platform only needs to implement its interface, then call a unified viewModel to achieve the functionality.

Thirdly, code reuse across software. Many commercial software have client and admin interfaces, but the functionality of both revolves around the same business entities. After abstracting the model, the two ends can also be shared.

Fourthly, UI component reusability. Most UI components are divided based on business entities, not tied to specific business processes or interfaces, making them easy to reuse.

2. Easy Code Modifications

In software development, changing requirements is common, and the majority of changes are related to interface or interaction, with a few related to business processes. For interface changes, even a complete UI redesign, viewModel, and model are not concerned; only the view needs to be modified. For changes in business processes, there is no need to modify the model. Fortunately, most of the logic in this architecture is in the model and viewModel layers!

3. Easy Extension

When adding features to software, simply combine a few existing models to complete the viewModel, then combine or write a few UI components to complete the functionality. If the new feature requires existing business entities to add new capabilities, add functionality to the corresponding models. If new business entities are added, abstract a new model, then continue combining the entire functionality.

2. Easy Unit Testing

Developers can easily create unit tests for viewModel and model without involving the view. Unit tests for viewModel can completely test the functionality used by the view. Developers can also easily write unit tests for UI components in the view layer.

3. Reduced Development and Testing Costs, Increased Efficiency

Due to extensive code reuse, developers will become faster in development over time, accumulating more reusable models with each completed business process. After completing software for a platform(pc,tablet or mobile), when developing another platform, you only need to implement the view layer of the this platform. Similarly, in the testing process, if one platform is tested, testing the other platform will be completed quickly.

Code Organization Structure

Many people nowadays prefer organizing code by file type or pages. For example, there may be a pages or modules directory containing code related to each page or business process, along with several directories named after file types: utils, constants, apis, classes, hooks, models, components, etc., containing code of various types for use by multiple pages.

This approach has significant issues: Firstly, it is challenging to determine whether a piece of code is shared among multiple pages. For instance, developer A develops a business (a) and encapsulates a component Component1, thinking that this component is only used on this page. Developer B, when developing business (b), needs a similar component. He may look for it in the components directory, not finding it, and may develop a new one. He might ask team members or find Component1 by examining the product, or he might find it himself and move it to the components directory, then use it. In either case, this increases development and communication costs.

Secondly, it is difficult to determine which directory a small feature belongs to. For example, developer A needs to process data before displaying it, and this logic is used multiple times in the product. He believes that this must be encapsulated into a common method. He looks for it in the utils directory but doesn't find it. He then checks the classes directory and still doesn't find it. Therefore, he wonders why this common functionality isn't encapsulated in a common method, so he writes it himself and puts it in utils directory. One day, he discovers that this logic already be encapsulated in a common method, but it was in the hooks directory or it had a different name, which he didn't think about.

Thirdly, when there are too many files in a common directory, the problem of organizing subdirectories arises. As the project grows, there will be a lot of code that can be reused across multiple pages. Each common directory will have many files. How to organize these files becomes a problem.

I believe a better code organization structure is as follows:

Screenshot 2023-11-04 16-10-20.png

1. objects directory

This is the core of the code structure and should contain the majority of the project's code. Each subdirectory in the objects directory represents code related to a specific business entity. It includes a model.js for the model of the current business entity and a manager.js for the entity's manager (if needed). The components directory contains components related to the current business entity, such as display components, configuration components, and functional components. Similarly, directories like utils, apis, constants, etc., are specific to the current business entity.

2. businesses directory

Each subdirectory in the businesses directory represents a business feature. It includes a vmodel.js for the viewModel of the current business feature and a page.js (or a name representing the business feature). This serves as the logical component for the business feature. If implementing the business feature requires multiple pages, a components directory can be added to store components for each page. If there are components related to multiple business entities, they can also be placed here, but it's advisable to avoid writing such components.

3. Other common code directories

Apart from the above two directories, the src directory should also include common directories like components, utils, constants, etc., to store code unrelated to specific business entities. Since the majority of the project's code is related to business entities, the files in these directories should be minimal.

This approach makes the code structure clear, making it easy to determine where a file should be placed. For example, a component for selecting a product model should be placed in objects/commodity/components, and code for calculating coupon stacking logic should be in objects/coupon/utils. When writing code that uses a component or logic, developers can easily locate where it should be. This method encourages developers to adhere strictly to the MVVM pattern unconsciously.

Issues to Note

  1. Can UI components use models and viewModels?

    • This article describes MVVM as an architectural design pattern, not limiting it to just an architectural design pattern. It can be used for specific feature implementations. Since we have encapsulated many models, UI components can certainly use these models to accomplish specific functionalities. Even a single UI component can be divided into model, view, and viewModel layers. However, for individual UI components, which are often business-oriented and not very complex, they usually only use one model. Therefore, the viewModel layer is generally not complex, and it's common to combine viewModel and view together, leading to the perception of only two layers.
  2. Be cautious about using models from props.

    • Reusability is a crucial indicator of whether a UI component is well designed. When a component relies on models passed from props, it requires the person using the component to be familiar not only with the component but also with the models it depends on. Without this familiarity, it becomes challenging to set up the component correctly by providing the necessary data from the model. This significantly reduces the reusability of the UI component. The recommended practice is to pass the necessary data to the component through props and let the component initialize the model based on this data.
  3. What to do when a model needs functionality from multiple other business entitie's manager?

    • Semantically, when a model has the manager functionality of another entity, inheriting the manager model of that entity is natural. However, having an instance of the manager model is also acceptable. The main reasons are that JavaScript natively does not support multiple inheritance, and it helps prevent naming conflicts.
  4. Can this pattern be applied to businesses which are mainly in the frontend?

    • For most software where the majority of the business logic is in the frontend (such as editors, web-based games, etc.), MVVM pattern architecture can also be used for code organization. In these businesses, the business entities are generally not very concrete. Therefore, developers need to be careful when abstracting and distinguishing business entities to avoid functional overlap between models or the abstraction of multiple business entities into one model.
  5. Does MVVM pattern strongly depend on object-oriented programming?

    • Although abstracting MVVM layers as classes is easy to understand and code, MVVM is an architectural pattern and should not limit the project to only use object-oriented programming. The current trend in frontend development is towards functional programming. It is speculated that implementing MVVM layers using hooks should be possible, with business entities abstracted into various model hooks and business processes abstracted into hooks that use these model hooks. The functionality and structure should be similar to what was discussed, but it remains to be confirmed through practical implementation.
  6. Will MVVM pattern affect division of labor?

    • Since models and viewModels do not depend on views, there may be a new way of dividing tasks during development using this architecture. For developers familiar with business logic, they may focus on developing model and viewModel layers, while those skilled in styling and effects may develop the view layer and interact with the other two layers. Whether this approach will improve efficiency or not is unclear and can be considered an interesting question for further exploration.

Conclusion

If you still don't know how to structure bussinesses code, there's a demo.
The above is my understanding of using the MVVM pattern for code architecture. I hope it is helpful to everyone. Since everyone's understanding of code architecture is different, and my knowledge is limited, many contents in this article are likely to be controversial. I hope readers can share their views in the comments, and we can have friendly discussions to progress together.