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

实战系列来自个人开发以及运维 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. nmap top N端口获取

    使用nmap 扫描时可能会扫描tcp top100 top1000 端口, 有时需要去配置文件提取,配置文件路径/usr/share/nmap/nmap-services, 具体根据实际安装情况调整: ...

  2. pikachu SQL-inject insert/update注入

    insert 注入 (修改信息处是update注入,和此处同理) 注册页面,用户处输入 1' 发现报错信息 You have an error in your SQL syntax; check th ...

  3. 混合类Mixins介绍

    介绍 混合类是封装了一些通用行为的基类,旨在重用代码.通常,混合类本身并没有什么用,仅扩展这种类也行不通 因为在大多数情况下,它都依赖于其它类中定义的方法和属性.通过多继承,可将混合类与其它类一起使用 ...

  4. 利用wiile双层循环打印各种星星---day06

    # 十行十列小星星 j = 0 #定义行数 while j<10: #当行数小于10的时候 i=0 #定义列 while i <10: #当列小于10的时候 print('*',end=' ...

  5. Flex 弹性盒子布局

    可以少去理解一些不必要的概念,而多去思考为什么会有这样的东西,它解决了什么问题,或者它的运行机制是什么? 1. 弹性盒子布局概念 Flex 是 Flexible Box 的缩写,意为"弹性布 ...

  6. VMware虚拟机Ubuntu系统连接网络过程

    网络和Internet设置--高级网络设置--更多网络适配器选项--WLAN. 右键选择属性--共享,勾选允许连接,选择VMnet8.(若勾选了其它,之后再想换回来,可以先取消勾选,点确定,再进入勾选 ...

  7. macOS Monterey 与以下电脑兼容下载操作流程解析

    有时在开发iOS应用时我们时常遇到各种情况,比如手机升级了Xcode不支持这时候需要安装xcode但是xcode需要依奈相应系统本人小编整理了这种情况无法解决问题. 首相打开苹果标志进入到下面界面 进 ...

  8. 第121篇: DOM常用类型(Document、Element)

    好家伙,本篇为<JS高级程序设计>第十四章"DOM编程"学习笔记   1.Document类型 Document 类型是 JavaScript 中表示文档节点的类型. ...

  9. Geospatial Data 在 Nebula Graph 中的实践

    本文首发于 Nebula Graph Community 公众号 本文主要介绍了地理空间数据(Geospatial Data)以及它在 Nebula Graph 中的具体实践. Geospatial ...

  10. linux的内置crontab(定时任务)的简单介绍

    编写定时任务的基本命令: #查看当前用户的crontab crontab -l:   #编辑crontab任务 crontab -e:   #删除crontab任务 crontab -r   定时任务 ...