DLabs.AI 2024年11月26日
Modular Architecture: A Framework For Building Clean, Easy-To-Maintain JavaScript Apps
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文探讨了如何构建易于维护和扩展的JavaScript应用程序。作者介绍了一种名为‘模块化架构’的框架,强调模块化、清晰的代码结构和明确的数据模型。文章详细阐述了模块化架构的组成部分,包括模块、视图、组件、服务、辅助函数、存储等,并通过一个示例项目展示了如何应用这些组件构建一个结构清晰的应用。此外,文章还强调了软件架构和软件设计的重要性,以及如何通过合理的代码结构和组件划分来提高代码的可读性和可测试性。最后,文章提供了包含工作示例的代码库,帮助读者将所学知识付诸实践,构建更优质的JavaScript应用。

🤔 **软件架构与软件设计**: 文章首先区分了软件架构和软件设计,强调软件架构关注项目的整体结构和组件交互,而软件设计则关注代码层面的细节,例如项目结构、数据模型、UI和业务逻辑等。

🧱 **模块化架构核心组件**: 模块化架构的核心组件包括模块(代表应用的单一职责)、视图(代表页面或主要路由)、组件(UI构建块)、服务(包含业务逻辑)、辅助函数(用于重复操作)、存储(数据管理工具)等。

🗂️ **代码结构与模块化**: 文章建议将项目按照模块进行组织,每个模块包含与该模块相关的组件、常量、存储、辅助函数、服务等,例如`/modules/ExampleModule/components`、`/modules/ExampleModule/services`等,这有助于提高代码的可读性和可维护性。

🔗 **组件划分与数据模型**: 在UI设计的基础上,可以通过划分组件边界来确定组件的职责和数据模型,例如根据报表功能的线框图,可以将报表功能划分为`GenericReport`、`SavedReports`等组件,并定义相应的数据模型。

🧪 **单元测试与代码质量**: 模块化架构有助于提高单元测试的覆盖率,因为业务逻辑与UI逻辑分离,更容易进行独立测试,从而保证代码质量。

I, for one, can barely remember a world without the internet. 

In truth, I can scarcely remember life without a smartphone. Looking at the stats, I think it’s fair to say many people are in the same boat.

Given the trajectory we’re on, we’re unlikely to see a drop in the need for developers (of all kinds) any time soon, and the below studies attest to that:

This developer shortage means that now more than ever, we need repeatable ways to build apps that use clean code that’s also easy to maintain.

The following article distills down our experience from multiple projects into a JavaScript framework we now call ‘Modular Architecture’ — and it’s a guideline that both new and more experienced engineers can adopt.

By reading this text, we hope you’ll learn:

At the end of the article, you’ll also find a repository with a working example that uses the guideline, helping you put what you’ve learned to use. And to make sure everyone can follow what we’re talking about…

 We’ll start with a glossary of terms used in this text.

Glossary

Term

Meaning

Business logic

The part of the program that encodes the real-world business rules determining how data can be created, stored, and changed. Prescribes how business objects interact with one another and enforces the routes and methods by which business objects are accessed and updated. In simple terms: this is the meat in your app.

Component

Components are UI building blocks in the app.

Constant

A variable whose value cannot be changed (in the case of a constant object in JS, the reference cannot be changed).

Data flow

Movement of data through an app.

Data mock

Fake data which is artificially inserted into a piece of software, often used for testing in isolation.

Data model

Organizes elements of data and standardizes how they relate to one another (and to the properties of real-world entities).

Data type

A classification that specifies which type of value a variable has — alongside what type of operations can be applied to it without causing an error.

E2E test

A technique that tests the entire software product from beginning to end to ensure the application behaves as expected.

Flux architecture

An application architecture developed by Facebook. It complements composable view components by utilizing a unidirectional data flow.

Helper

Place for small, repeatable operations used across the app.

Module

The fundamental element in Modular Architecture that gathers code elements related to a domain in one place.

Service

The code responsible for business logic within a module.

Software architecture

Defines functional and non-functional requirements and high-level technical dependencies.

Software design

Defines low-level requirements for the software, including aspects like code structure, data models, UI, and business logic composition.

Store

The place for data management tools (e.g., Redux).

UI

User interface.

Unit test

A way of testing a unit (which is the smallest piece of code that can be logically isolated in a system).

Wireframe

A skeletal blueprint or framework that outlines the basic design and functions of a user interface.

View

The UI component that represents a single page or major route in the application.

What is Software Architecture? And Why Does it Matter?

Let’s kick things off by reviewing two fundamental aspects of engineering: software architecture and software design.

First up, software architecture.

Software architecture

People often confuse software architecture for software design. But these two terms are not interchangeable.

Software architecture helps us see the big picture of a project. It covers the high-level components of the design and how they’ll interact within a project, answering questions like ‘where’ and ‘how’ by guiding:

Deciding on the above helps us determine the functional and non-functional requirements — alongside the high-level technical dependencies. And with these aspects defined, we have a clearer path forward, which will help us keep the overall quality high.

“In general, software architecture helps us monitor performance, scalability, and reliability.”

— Iryna Deremuk, Modern Web Application Architecture Explained

Software design

On the other hand, software design answers the question of ‘how’ — but it does this at the code level.

In place of considering the big picture, this is where we handle the details, deciding how to build the individual parts of the system, including the project structure, data models, UI, and business logic composition, among other aspects.

— ‘Why is that important?’ you ask. Why can’t we just write code? Well, let’s get some perspective. If you’ve ever tried to assemble a Lego set without instructions, you’ll know how important that piece of the puzzle is. 

The instructions are the part of the design that gives us a universal language to follow: a language that enables multiple parties to work together and create well-defined solutions for known problems.

With a clear architecture and code design, development is much more straightforward. Now, let’s dive into the guidelines for building clean, easy-to-maintain JavaScript apps.

 

Note: Even though you can apply these guidelines to backend Node applications, in this article, we’ll only focus on web and mobile apps that we can create in React and React Native, respectively.

Modular Architecture: Components

First up, we’ll look at the core components.

Module

As the ‘Modular Architecture’ name suggests, a module is fundamental to the framework. A module represents a single responsibility within an application (say, authentication, reporting, or user account management). 

A module often corresponds to an epic. And to allow a module to represent a responsibility, it collects all related items in one place.

View

A view is a simple UI component representing a single page or major route in the application. It serves as a place to bootstrap underlying components, rarely holding any UI logic.

Component

Components are the UI building blocks in the app. They’re small pieces of code representing UI elements, mostly holding the majority of the UI logic.

Service

This is the place for business logic. Each service should serve a specific purpose, and the name of each service should reflect said purpose (the service that handles reports = ReportsService; auth = AuthService; you get the point!).

Helper

Here’s where to put small, repeatable operations used across the app. Something that’s not exactly business logic or UI logic, but that might help them.

Store

We keep all things related to data management in the store. Most of the time, we stick to Redux or ContextAPI (only when building with React) and use it for reducers, actions, and store slice.

The store is optional: you might not need Flux architecture in every app.

Now, let’s move on to the code structure and learn how you’ll put each of these components to good use.

Code Structure

A module collects all related items in one place. That said, it still manages to emphasize the division of responsibility within the module.

After all, we want to keep the UI, business logic, and data management separate. And the element that helps us achieve this separation is the code structure, so let’s now take a look at a project that benefits from this modular design.

Structure of an example project grouped by modules

/Project root
|-ui (global UI components)
|-[...] (other directories)
|-modules
 |-ExampleModule
   |-components (UI components related only to the module)
   |-constants
   |-store (place for data management tools)
   |-helpers (place for minor, reusable functions)
   |-mocks (place for data mocks)
   |-services (module’s business logic)
   |-types (module’s type declarations)
   |-views

Structuring a project as above improves the semantics, making it easier to position new elements and look for existing code pieces. 

In our experience, it’s also offered better unit test coverage. That’s because the business logic was well organized and separated from the UI logic.

Components Split

The approach to use depends on whether we have UI designs available.

Assuming we do, we can use them as a base to draw boundaries between components and decide what belongs where (which is also helpful when verifying if we should split the underlying user story into correct pieces and define data models).

Let’s take the wireframe of the reports feature as an example.

Knowing that a module should gather related items and the expected code structure, we could draw certain boundaries.

 

This yields in the following module:

|-ReportsModule
  |-components
  | |-GenericReport
  | | |-Controls
  | | |-Settings
  | | | |-Filters
  | | | |-Sorting
  | | | |-Metrics
  | | |-ReportTable
  | |   |-Entry
  | |-SavedReports
  |   |-[...]
  |-constant
  |-store
  |-helpers
  |-mocks
  |-services
  | |-ReportsService
  |-types
  |-views
    |-ReportsView

 

We can use the boundaries drawn on the wireframe and a view of what the data might look to determine an initial data model for a future contract between the backend and the frontend.

The below is the data model created from the boundaries drawn on the wireframe.

interface Settings {
 filters: string[];
 sorting: string[];
 metrics: string[];
}

interface Item {
 id: number;
 name: string;
 property: string;
}

interface Report {
 settings: Settings;
 items: Item[];
}

 

Where UI designs aren’t available (or applicable), we can define similar boundaries, structure, and data models using business needs as expressed in a user story.

Data Flows

We’ve touched on data models; now’s the time to talk about them.

I’ve seen the UI manipulate the data in many projects I’ve worked on. This often introduces unnecessary complexity to a project with multiple negative consequences. 

Having UI manipulate data calls for business logic inside the UI, breaking the separation of responsibilities. It also introduces another source of truth within the application and makes the logic inside the UI barely testable. 

You might say, ‘So what?’ — but just wait till you have to debug, test, or maintain an app built in this way.

So please trust me when I say: Business logic that lives in the services should never be mixed with the UI logic. UI simply takes care of rendering the data to the user. Which is why when using modules, we keep the data flow unidirectional (see Flux architecture). 

Views and components can only request an action to be executed by a service, and the process is entirely transparent to the UI. 

Services and helpers take care of the operation (API communication, data processing, pushing data to a store), then a store manager provides new data to the UI.

In the above image, the solid line shows the data flow.

Thanks to this approach, we gain a single source of truth and solid separation of concerns between the UI, business logic, and data handling. 

The UI only ever consumes data, never processing it. And if the code doesn’t need flux (say, in a tiny web app or backend service), you don’t have to use a store management tool at all until the business logic lives only within the services.

Naming Conventions

As you’ve probably noted, we like to have our code easily readable and semantically meaningful, which is why we agreed on a set of naming conventions. 

But what you see below is only a baseline. And it’s not written in stone. Every team can adapt it to fit their purpose. And once you have the changes documented, your team should stick to the established rules.

It’s worth noting: we apply these conventions to our React, React Native and Node apps. That said, they’re not a mandatory aspect of the Modular Architecture approach.

While if you have your own conventions, they’ll also work.

Cons of Modular Architecture

Right, so we’ve got this far, and you’re probably thinking, ‘What’s the catch?’ 

I can openly admit: there are some disadvantages to the modular approach, which I have summarized below:

Pros of Modular Architecture

Despite the cons, we’ve built multiple apps using our Modular Architecture guidelines, and while it takes additional effort and has some disadvantages, the positives easily outweigh the negatives.

Just take a look and see what you’ll get:

And to top it all off — the separation of the business logic means you can share the app across mobile and web applications (assuming they both use JavaScript).

How to Test Modular Architecture

If you felt like this detail was missing, rest assured: I deliberately left testing to last. 

That’s because the subject is way too important to sit hidden amidst the other content; it requires its own section.

In fact, I’m going to write an in-depth article on our testing approach alone. But for now, I’ll simply share a quick summary. Basically, we write two types of tests: unit tests — and end-to-end tests. 

No great architecture can thrive without a robust testing structure, which is why we invested time in building one for Modular Architecture.

See Modular Architecture In Action

There you have it: this is how DLabs.AI codes JavaScript. 

To help you better understand the principles, you can experience our working example, which is a slice of our Kalkulator WBT app.

I even challenged myself to reuse the business logic across mobile and web, so have fun exploring the repo, and please — do share your thoughts. And if you have an idea on how we can improve the framework or use it in other scenarios.

Drop me a message.


P.S. If you are curious about the photo at the top of the article – yes, it’s me on the right. In DLabs.AI, we have no dress code, so we can work even in pyjamas. If this suits you, check out the open positions and join us.
P.P.S. And more seriously – this is a photo from our company integration

Artykuł Modular Architecture: A Framework For Building Clean, Easy-To-Maintain JavaScript Apps pochodzi z serwisu DLabs.AI.

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

JavaScript 模块化架构 软件架构 软件设计 代码结构
相关文章