我们从一个最简单的需求开始,来探索我们应该从哪些方面思考模块设计,以及如何将不同的文件分类。之所以说“思考”,是因为我在这篇文章里更多的是提供一类解决问题的范式,而非统一的标准答案,能够为你提供一丁点的启发就好

实战系列来自个人开发以及运维 site2share 网站过程中的经验

需求很简单:当用户访问列表页面时,我们需要展示列表数据。为了便于接下来的演示,我们不如把这里的“数据”具象化,以 site2share 网站为例,列表即代表了每用户创建的文件夹下的内容,如下图所示

Service 层很重要

在获取数据的过程中我们至少需要做这么几件事:

  1. 获取列表数据
  2. 获取列表文件夹元数据,如创建人、创建时间
  3. 判断访问者是否有权限修改列表

我们可以想象这三类数据如果使用 SQL 进行存储的话,它们会属于不同的数据库表,所以在这次数据访问中我们至少要对三张表进行读操作。但是应该直接把三者的数据库读操作语句糅合在一个函数里吗?缺少封装问题不是最大的问题,这么做会导致代码的维护性会大大增加,因为可能有多个功能用例都需要“获取文件夹元数据”,这样一来同样的一组代码就难免会散落在代码库各个地方(可能是多行代码),如果有一天表结构发生了更改需要修改语句,你会面临散弹式修改的风险。

所以你至少应该以 service 视角将同类型操作封装起来。比如把所有和用户有关的操作都归类到 UserService.js 文件中

什么是 service?如上图所示,如果我们把最外层大圆圈定义为正在被开发的应用的话,那么 service 层处于应用的最外层边界上,与它接壤的是各种类型访问方式,例如网络请求可以访问这个应用,通过 CLI 也可以访问这个应用。无论访问方式是什么,外部永远无法跨越 service 层调用应用内部的接口。service 封装了常见的应用业务逻辑,你甚至可以理解为用户用例

在 service 层之下你还看到有 domain 层和 data source 层,在 NodeJS 中可以不追求一模一样的抽象,但 service 层是至少需要保证的。

注意 service 与 model 并非一一对应

例如我们有一个关于文件夹的服务层模块 FolderService.js,“Folder”意味着的是它服务于“Folder”,而非只能访问和 "Folder" 有关的数据。在模块内部可以有一个方法名为 GetFolderByUserId,这个时候它可能为了在结果中带上用户信息,而需要访问用户数据库。甚至它还需要请求应用外的数据,比如用户数据存储在 Auth0 的话

优先按照功能给文件分类

除了 FolderService.js 以外,我们可能还有数据层访问模块 FolderModel.js,以及专门响应与文件有关请求的路由 FolderRouter.js。关于如何组织这些文件这里有两个思路

按照功能(与文件或者用户有关)将这些文件组织在一起(前者),和按照角色(与路由或者服务有关)将文件组织在一起(后者)

├── Folder
| ├── FolerService.js
| ├── FolderModel.js
├── User
| ├── UserService.js
| ├── UserModel.js
├── Services
| ├── FolerService.js
| └── UserService.js
├── Models
| ├── FolderModel.js
| └── UserModel.js

后者是我们最常见的组织文件方式,但实际上前者更有利于我们开发

日常开发工作都是围绕某个特定的功能展开的,例如需求“新增 API 用于前端获取访问量最多的前十文件夹 id“,在开发这个功能的过程中我们修改的路由、在 service 和 model 中新增的方法,都只和 Folder 有关,我们需要反复在有关文件之间切换并且做出修改。按照功能组织文件,便于我们聚焦我们开发的功能

这其实和前端开发中使用 duck 文件分类法 是类似。

甚至我建议你的测试文件也应该和功能放在一起(大部分项目的测试文件都会单独放在测试文件夹中)。当然是否应该这么做部分取决于你是如何制定你的测试策略的。在拥有 service 模块的前提下,我建议大部分精力应该对 service 进行测试,对 service 测试一方面间接测试了 service 以下的其他模块代码,另一方面也直接对用户用例进行了测试。测试还有一个颇为重要的功能是,它可以作为被测试模块的说明书,通过测试用例我们可以知道模块能做什么以及不能做什么

最小知识原则

项目中还有相当多的文件是无法与具体业务相关联的技术代码。例如我们通常会把常量,公共函数提取到单独的文件中。代码不是为了分类而被分类,所有的行为背后都是为提高代码维护性服务的。在我的经验看来代码分类的一个重要原则是确保程序员以最小的成本展开工作。

文件的命名和划分应该符合程序员的默契。例如我们看到 src 文件夹就应该知道它是用于存放源码文件的,如果你把源码文件命名为 src 的全称 source 会让不少人要理解好一会。

之所以说是默契,是因为这类规则并不是明文写在什么开发制度里的,而是当我们经历了10个代码库并且每个代码库都是这么做的时候,我们也就会认为这么做是对的,并且之后的编码工作把相同的规则传递下去。例如以下关键词常常被作为默契使用:

  • util/utility: 公共代码
  • libs:第三方类库
  • common:公共文件目录
  • constant: 常量
  • test/ut(united test):测试相关
  • client/provider:第三方服务访问类
  • docs:文档相关
  • dist/build:打包目录
  • public/assets: 静态文件
  • *.md:markdown 格式的文件通常为文档
  • *.spec:带有 spec 字样的文件通常为测试文件
  • tool/scripts:与代码无关的工具文件,比如创建数据库的 shell 脚本

这些还只是文件命名上的潜规则。默契可以提升我们的开发效率,例如当你第一次接触某个代码库想看看它说明文档时,你想当然的会去搜寻 markdown 类型的文件;又或者你创建了一个函数觉得后期对其他团队成员也有帮助时,你下意识的会存储在 util.js 的文件中

“最小知识”的另一个方面是,当需要维护某个模块时,我们应该尽可能避免其他模块带来的干扰,这也是模块划分的本质意义。行数巨大的文件或者函数虽然是一个经典反例,但是在当下可能已经不常见。有一些其他类型的陷阱在这里分享出来。

package.json 就是一个例子,目前 jest, babel, nodemon 等等都允许将配置信息编写在 package.json 中,甚至一些开发工具还允许我们将 eslint 的配置也放置其内。但我不认为这是一个好的实践。例如当我们需要修改 jest 配置时,只专注于 jest.config.js 的修改能够让我们更快定位到文件和代码,以降低对其他配置破坏的可能性。

下面截图是 site2share 项目中所需要的各类配置文件,想象一下如果把它们所有内容都塞入 package.json 其中,诞生的超级配置文件是怎样一种灾难。

就像我在文章开头说的,所有的问题无法一概而论。例如对于多个团队共用一个代码库的 monorepo 类型项目来说,全局共享的单一常量文件注定是无法解决问题的。对于类似问题我无法一一作出解答,但是把你自己放到将来的项目中去,围绕可维护性去思考一定是正确的方向


你可能也会喜欢:

NodeJS 实战系列:模块设计与文件分类的更多相关文章

  1. NodeJS 实战系列:个人开发者应该如何选购云服务

    这文章至少值一千元,因为这是我保守估计花出去的冤枉钱(请自行脑补一个苦笑的 emoji) 文章中会穿插选择云服务的一些建议,当然也会提供一些"薅羊毛"的技巧.不过在此之前我们要想清 ...

  2. NodeJS 实战系列:DevOps 尚未解决的问题

    本文将通过展示 NodeJS 应用里环境变量的提取过程,来一窥 DevOps 技术是如何应用在现在云平台上的运维工作中的.同时我也想让大家在这里看到 DevOps 的另外一面,即它并非全能,从本地开发 ...

  3. NodeJS 实战系列:如何设计 try catch

    本文将通过一个 NodeJS 程序里无效的错误捕获示例,来讲解错误捕获里常见的陷阱.错误捕获不是凭感觉添加 try catch 语句,它的首要目的是提供有效的错误排查信息,只有精心设计的错误捕获才有可 ...

  4. Nodejs实战系列:数据加密与crypto模块

    博客地址:<NodeJS模块研究 - crypto> Github :https://github.com/dongyuanxin/blog nodejs 中的 crypto 模块提供了各 ...

  5. [.NET领域驱动设计实战系列]专题一:前期准备之EF CodeFirst

    一.前言 从去年已经接触领域驱动设计(Domain-Driven Design)了,当时就想自己搭建一个DDD框架,所以当时看了很多DDD方面的书,例如领域驱动模式与实战,领域驱动设计:软件核心复杂性 ...

  6. NET 领域驱动设计实战系列总结

    NET 领域驱动设计实战系列总结 一.引用 其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计.领域驱动设计C# 2008实现.领域驱动设计:软件核 ...

  7. Java秒杀系统实战系列~构建SpringBoot多模块项目

    摘要:本篇博文是“Java秒杀系统实战系列文章”的第二篇,主要分享介绍如何采用IDEA,基于SpringBoot+SpringMVC+Mybatis+分布式中间件构建一个多模块的项目,即“秒杀系统”! ...

  8. [开源]OSharpNS 步步为营系列 - 1. 业务模块设计

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  9. react实战系列 —— 起步(mockjs、第一个模块、docusaurus)

    其他章节请看: react实战 系列 起步 本篇我们首先引入 mockjs ,然后进入 spug 系统,接着模仿"任务计划"模块实现一个类似的一级导航页面("My任务计划 ...

  10. GitHub实战系列~1.环境部署+创建第一个文件 2015-12-9

    GitHub实战系列汇总:http://www.cnblogs.com/dunitian/p/5038719.html ———————————————————————————————————————— ...

随机推荐

  1. Ansible的基本配置

    目录 定义主机和组 主机的定义 主机组的定于 主机组的嵌套 选择主机和组 匹配主机 使用通配符匹配 配置文件优先级 配置文件详解 配置文件段 配置文件参数说明 配置案例 1. 在节点上创建一个普通用户 ...

  2. contextmanager装饰器

    虽然上下文管理器很好用,但定义一个符合协议的管理器对象其实挺麻烦的 得首先创建一个类,然后实现好几个魔法方法.为了简化这部分工作,python 提供了一个非常好用的工具:@contextmanager ...

  3. .net core6 Log4记录日志

    一.引言 .net core6在文件方面是精简了,所以配置方面也发生了部分变化:下面记录下.net core6中怎么配置Log4 进行日志记录. 二.项目创建 1).首先引用两个包:在Nuget中引入 ...

  4. React 组件通信方式

    人生的游戏不在于拿了一副好牌,而在于怎样去打好坏牌,世上没有常胜将军,勇于超越自我者才能得到最后的奖杯. 1. 父子组件通信方式 1.1 父组件传递到子组件 直接通过属性进行传递,数据的传递可以提高组 ...

  5. 【Azure App Service for Linux】NodeJS镜像应用启动失败,遇见 RangeError: Incorrect locale information provided

    问题描述 在App Service For Linux 中,部署NodeJS应用,应用启动失败. 报错信息为: 2023-08-29T11:21:36.329731566Z RangeError: I ...

  6. 【Azure Redis 缓存】Azure Redis读写比较慢/卡的问题排查

    问题描述 在使用Azure Redis的过程中发现读写比较慢,非常卡,执行扩容6-->13GB后,过一段时间也满了.在通过门户Console连接到Reids,通过info Memory名称查看到 ...

  7. 【Azure Redis 缓存】Azure Cache for Redis 专用终结点, 虚拟网络, 公网访问链路

    问题描述 为优化Redis访问链路,对下面三种方案进行对比: 1.Redis添加到虚拟网络 2.Redis添加专用终结点 3.Redis默认公共链路 问题分析 第三种:Redis默认公共链路,顾名思义 ...

  8. linux系统信息命令笔记

    1,时间和日期 2,磁盘信息 4,进程概念介绍 4.1,ps 基本命令使用 ps aux 显示内容太多了.一般用ps a 或 ps au 4.2, top命令的基本使用 top 可以动态的显示运行中的 ...

  9. 【自动化】使用PlayWright+代理IP实现多环境隔离

    Playwright是由微软公司2020年初发布的新一代自动化测试工具,相较于目前最常用的Selenium,它仅用一个API即可自动执行Chromium.Firefox.WebKit等主流浏览器自动化 ...

  10. 16. Class字节码结构

    1. 相关概念 1.1字节码文件的跨平台性 Java 语言是跨平台的(write once, run anywhere) 当 Java 源代码成功编译成字节码后,如果想在不同的平台上面运行, 则无须再 ...