This is the 24th day of my participation in Gwen Challenge

  • For writing business code, a lot of front-end development is considered boring and easy to reach technical bottlenecks. This is not true. Almost all development of what we call “technical requirements” or “technical tools” comes from business needs, as do frameworks like Angular, React, and Vue.

  • On the front end, is business development really just about tweaking styles, stitching together templates, binding events, interface requests, updating pages? We can improve the programming experience of writing business code through better code design.

Here I’ll show you how to design modular and componentized applications.

  • In fact, when we start to write repetitive code or do a lot of copy and paste, we probably need to think about abstracting the application properly, so let’s take a look at that.

How to implement modular design of applications

  • When we get a designed application, in order to avoid problems such as excessive file content and serious coupling between functions, and improve the usability and maintainability of the project code, we need to split it into modules.

Application modules and hierarchies

  • Everyone has a different idea of what a module is, so there are many ways to break it up.

    • For simple management applications, a structure similar to MVC can be used to split modules, such as view modules, data modules, and logic control modules.

    • For applications with rich page content, you can split modules and components into core modules, function modules, and common component modules based on services.

    • For the application with complex interaction and logic, the application can be divided into modules and layers according to the system architecture, such as the rendering layer, data layer, network layer, etc.

  • For large applications, we often need to divide modules from top to bottom based on the granularity of modules. The partitioning rules may be the above rules or may be related to the application business scenarios.

  • For example, an online collaboration application with complex interactions such as online documentation may require hierarchical or secondary module partitioning after modules are separated into core modules, functional modules, and common component modules. For example, the core module can be divided into rendering layer, data layer, network layer, functional module can be divided into function calculation module, copy and paste module, and so on. Common component modules can be split into avatar modules, toolbar modules, and so on.

Module division and design principles

  • In the field of general programming design, there are also many design concepts and principles for architectural design. Here, I will introduce two.

  • Domain-driven Design (DDD) : Domain division and modeling of the system from the perspective of business Domain.

  • Responsibility-driven Design (RDD) : Responsibility division, module split, and collaboration from the perspective of the internal system.

  • Among them, domain-driven design (DDD) is used to divide business domains, which is more practical in the design of complex business system architecture. For example, the division of commodities, buyers/sellers, orders, coupons, risk control and other fields in the field of e-commerce.

  • However, in the front-end domain, different business domains usually appear through different pages and components, such as commodity page, order page, coupon page and so on. Therefore, domain-driven design (DDD) is rarely used in front-end development, or it can be said that the application in front-end domain is similar to the idea of front-end componentization.

  • As for responsibility-driven design (RDD), it tends to define and divide modules in terms of roles and responsibilities, similar to common components on the front end, tool libraries, MCV/MVVM design, functional module partitioning, and so on. Responsibility Driven Design (RDD) can bring a lot of help in the design of the system architecture with complex functions. For example, in the design of each functional module in the online document mentioned above, the functions of modules can be clearly defined by dividing the responsibilities of the system and defining the boundaries and cooperation between modules.

  • These two design patterns are not mutually exclusive and can be used together, for example:

  • We can divide and model the functions closely related to business logic according to business domain, such as shopping cart component and commodity component in e-commerce website.

  • For the functions related to the front-end implementation (view rendering logic, interaction logic with the server, and interaction logic with the user), we can assign responsibility and module division to these functions during the concrete system building process.

  • When we divide the modules, we also need to consider the design of modules, dependencies between modules and communication. Among them, the most common is how to solve the problem of dependency coupling between modules.

How to decouple dependencies between modules

  • You’ve heard the terms low coupling and high cohesion used to describe modular dependencies in system design, where:

    • Low coupling is based on abstraction, making our system more modular, and unrelated things should not depend on each other;

    • High cohesion means that the object is focused on a single responsibility.

    • Low coupling and high cohesion are the goals of every well-designed system, and there are many books and courses devoted to specific design patterns. Here I will focus on dependency decoupling, which is commonly used in complex front-end domains.

  • First, you can use dependency inversion for dependency decoupling. There are two principles of dependency inversion, including:

  • High-level modules should not depend on low-level modules, but both should depend on abstract interfaces;

Abstract interfaces should not depend on concrete implementations, and concrete implementations should depend on abstract interfaces.

  • For example, the DataManager module relies on the sendData() interface of the NetworkManager module and the updateView interface of the RenderManager module.

We can define interfaces in Typescript, so we can express them as:

Copy the code

interface INetworkManagerDependency {
  sendData: (data: string) = > void;
}
interface IRenderManagerDependency {
  updateView: () = > Promise<void>;
}
class DataManager {
  constructor(networkManagerDependency: INetworkManagerDependency, renderManagerDependency: IRenderManagerDependency) {
    // Dependencies can be saved and used as needed}}Copy the code

In this way, we rely only on abstract interfaces to implement function calls by convention, not on concrete modules and details.

If your project has a well-established dependency injection framework, you can use a dependency injection framework in your project, such as the Angular framework, which comes with dependency injection. Dependency injection is common in large projects and is useful for managing dependencies between modules. For example, VsCode uses dependency injection.

In addition to using dependency injection frameworks, the more common approach to dependency decoupling in the front end involves using events to communicate.

Event-driven is often used in various system designs to decouple the target object from its dependent objects. The target simply notifies its dependent objects, and the dependent objects decide what to do.

  • Using an event-driven approach can quickly and easily decouple modules, but it often introduces more problems, such as:

  • Global events fly around, not knowing where an event is coming from and how many places it is being listened to;

  • Unable to manage the destruction of event subscriptions, easy to have the problem of memory leakage;

  • Event maintenance is difficult. Adding and adjusting parameters are widely influential, and bugs are easily triggered.

In addition to the methods described above, there are many design patterns and concepts to refer to in the process of code programming, many of which are very helpful for decoupling the dependencies between modules, such as interface isolation principle, minimum knowledge principle, Demeter principle, etc.

Here, I’ve introduced module partitioning, design and decoupling in front-end applications, architectures and systems that are more common in large and complex projects. In the front-end daily development process, more will involve business logic and interface development content, so we often say to carry out componentized design.

How to componentize the application design

First, let’s define what a component is.

In simple terms, components extend HTML elements and encapsulate reusable code, such as:

<! A wrapped component might look like this --><my-component></my-component>
Copy the code

It looks like there is nothing in this component, because we have all the logic wrapped in the component. Components have their own presentation, state data, and functional logic, and a cat can be a component

  • In general, component partitioning can be done from two perspectives.

    • Partition by code reuse. When we write code, we observe that some code is actually reusable in structure and function, and we can encapsulate it to reduce the number of duplicate code.

    • By visual and interactive partitioning. Generally speaking, the division of components is closely related to visual, interactive, etc., and we can judge whether it is suitable as a component by function and independence.

Once we have determined which functions to divide into components, we need to define the responsibilities of the components and then implement component encapsulation.

How are components encapsulated

  • The process of component encapsulation is similar to the responsibility definition of a module. We first need to define the responsibility of this component.

  • A competent component that provides these capabilities:

    • Maintain its own data and state within the component;

    • Events (methods) that maintain themselves within a component;

    • External configuration interface to control display and specific functions;

    • By providing external query interfaces, you can obtain component status and data.