引子
文章《The Single Responsibility Principle》是从《NodeJS and Good Practices》外面看到的,持续翻译记录。
翻译原文:NodeJS and Good Practices
- Origin
- My GitHub
注释
软件总是处于随时变动中,而有助于掂量代码品质的一个方面是改变代码的容易水平。但为什么会这样呢?
… 如果你胆怯扭转某些货色,那么它显然设计得很蹩脚。
— Martin Fowler
关注点和责任拆散(Separation of concerns and responsibilities)
“将因同样起因而扭转的事件集中起来。将因不同起因而扭转的事件离开。”
无论是函数、类还是模块,它们都能够利用繁多责任准则和关注点拆散。从架构开始,基于这些准则进行软件设计。
架构(Architecture)
在软件开发中,责任 是独特致力于实现的工作,例如:在应用程序中示意产品的概念、解决网络申请、数据库中用户长久化等等。
留神到这三项责任不属于同一类别吗?这是因为它们属于不同的档次,这些档次能够划分为不同的概念。根据上述示例,“在数据库中用户长久化”波及“用户”概念,也波及与数据通信的层。
一般来说,与上述概念相干的架构往往分为四层:定义域(domain)、利用(application)、基础设施(infrastructure)和输出(input interfaces)。
定义域层
在这一层中,咱们能够定义有实体和业务规定作用的单元,它们与咱们的定义域有间接的关系。例如,在用户和团队的应用程序中,咱们可能有一个 User
实体、一个 Team
实体和一个 JoinTeamPolicy
来答复用户是否可能退出给定的团队。
这是咱们软件中最独立和最重要的一层,应用层能够应用它来定义 用例。
应用层
应用层定义了咱们应用程序的理论行为,因而负责执行定义域层中各单元之间的交互。例如,咱们能够有一个 JoinTeam
用例,它接管 User
和 Team
的实例并把它们传递给JoinTeamPolicy
;如果用户能够退出,它将长久化的责任委托给基础设施层。
应用层还能够用作基础设施层的 适配器 。假如咱们的应用程序能够发送电子邮件;负责与电子邮件服务器间接通信的类(让咱们称它为 MailChimpService
)属于 基础设施层 ,但理论发送电子邮件的类(EmailService
)属于 应用层,并且在外部应用 MailChimpService
。因而,咱们应用程序的其余部分不晓得具体实现细节——它只晓得 EmailService
可能发送电子邮件。
基础设施层
这是所有层中最底层,是咱们应用程序所有延长的边界:数据库、电子邮件服务、队列引擎等。
多层 应用程序的一个常见个性,是应用存储库模式与数据库或其它一些内部长久化服务(如 API)通信。存储库对象实质上被视为汇合,应用它们的层( 定义域层 和 应用层)不须要晓得底层是那种长久化技术(相似于咱们的电子邮件服务示例)。
这里的想法是,存储库接口属于定义域层,而实现则属于基础设施层,即定义域只晓得存储库承受的办法和参数。这使得这两个层更加灵便,即便在测试方面也是如此!因为 JavaScript 没有实现接口的概念,咱们能够设想本人的接口,并基于基础设施层创立一个具体的实现。
输出层
这个层蕴含咱们应用程序的所有入口,比方控制器、CLI、websockets、图形用户界面(对于桌面应用程序)等等。
它不应该对业务规定、用例、长久化技术有任何理解,甚至不应该对其它类型的逻辑有任何理解!它应该只接管用户输出(比方 URL 参数),将其传递给用例,最初返回响应给用户。
NodeJS 与关注点拆散
好了,在所有这些实践之后,如何将这些实践利用到 Node 应用程序?诚实说,多层架构中应用的一些模式非常适合 JavaScript 世界!
NodeJS 与定义域层
Node 上的定义域层能够由简略的 ES6 类 组成。有许多 ES5 和 ES6+ 模块有助于创立域实体,例如:Structure、Ampersand State、tcomb 和 ObjectModel。
让咱们看应用 Structure 的简略示例:
const {attributes} = require('structure');
const User = attributes({
id: Number,
name: {
type: String,
required: true
},
age: Number
})(class User {isLegal() {return this.age >= User.MIN_LEGAL_AGE;}
});
User.MIN_LEGAL_AGE = 21;
请留神,咱们列出的不蕴含 Backbone.Model 或像 Sequelize 和 Mongoose 这样的模块,因为它们在基础设施层,是用来与内部世界进行通信的。因而,咱们代码库的其余部分甚至不须要晓得它们的存在。
NodeJS 与应用层
用例 属于应用层,与 promise 不同的是,它们可能会产生 胜利 和 失败 之外的后果。对于这种状况,一个好的 Node 模式是 event emitter。要应用它,咱们必须扩大 EventEmitter
类,并为每个可能的后果收回一个事件,从而暗藏存储库在外部应用 promise 的事实:
const EventEmitter = require('events');
class CreateUser extends EventEmitter {constructor({ usersRepository}) {super();
this.usersRepository = usersRepository;
}
execute(userData) {const user = new User(userData);
this.usersRepository
.add(user)
.then((newUser) => {this.emit('SUCCESS', newUser);
})
.catch((error) => {if(error.message === 'ValidationError') {return this.emit('VALIDATION_ERROR', error);
}
this.emit('ERROR', error);
});
}
}
这样,咱们的入口就能够执行 用例 并为每个后果增加一个侦听器,就像这样:
const UsersController = {create(req, res) {const createUser = new CreateUser({ usersRepository});
createUser
.on('SUCCESS', (user) => {res.status(201).json(user);
})
.on('VALIDATION_ERROR', (error) => {res.status(400).json({
type: 'ValidationError',
details: error.details
});
})
.on('ERROR', (error) => {res.sendStatus(500);
});
createUser.execute(req.body.user);
}
};
NodeJS 与基础设施层
基础设施层的实现不应很艰难,但要留神其逻辑不要透露到以上的层!
例如,咱们能够应用 Sequelize 模型来实现一个与 SQL 数据库通信的库,并为其提供办法名称,这些名称并不暗示上面存在的 SQL 层——例如咱们上一个示例中的常见的 add
办法。
咱们能够实例化 SequelizeUsersRepository
并将其作为 usersRepository
变量传递给它的依赖项,这些依赖项可能只是与它的接口交互。
class SequelizeUsersRepository {add(user) {const { valid, errors} = user.validate();
if(!valid) {const error = new Error('ValidationError');
error.details = errors;
return Promise.reject(error);
}
return UserModel
.create(user.attributes)
.then((dbUser) => dbUser.dataValues);
}
}
同样的情理也实用于 NoSQL 数据库、电子邮件服务、队列引擎、内部 api 等等。
NodeJS 和输出层
在 Node 应用程序中实现这一层有很多抉择。对于 HTTP 申请,Express 模块是最罕用的,然而你也能够应用 Hapi 或 Restify。只管对这一层的更改不应影响其它层,也要依据实现细节做出最初的抉择。如果从 Express 迁徙到 Hapi 意味着要进行某种程度变更时,那么这就是耦合的迹象,你应该密切注意要修复它。
层的通信
让一个层间接与另一个层间接通信可能是一个槽糕的决定,并会导致它们耦合。在面向对象编程中,这个问题的一个常见解决方案是 依赖注入 (DI)。这种技术使类的依赖项在其构造函数中作为参数接管,而不是要求依赖项并在类自身外部实例化它们——从而创立所谓的 管制反转(inversion of control)。
应用这种技术使咱们可能以一种十分简洁的形式隔离一个类的依赖关系,从而使它更加灵便和易于测试,因为革除依赖关系成为一项琐碎的工作。
对于 Node 应用程序,有一个很好的 DI 模块 Awilix,它容许咱们利用 DI,而不用将咱们的代码与 DI 模块自身耦合——因而咱们不想应用 Angular 1 中 奇怪 的依赖注入机制。Awilix 的作者有一系列文章解释应用 Node 进行依赖注入,这些文章值得一读,同时也介绍了如何应用 Awilix。顺便说一下,如果你打算应用 Express 或 Koa,还应该看看 Awilix Express 或 Awilix Koa。
一个实例
即便有了所有这些对于层和概念的例子和解释,我置信没有什么比一个遵循 多层架构 的应用程序实例更好的了,它能够简略地应用!
你能够看看这个基于 Node 生产就绪的实例 boilerplate for web APIs。它利用了 多层 架构,并且曾经为你提供了根底设置(包含文档),因而你能够练习甚至将其用作 Node 应用程序的初始化。
更多信息
如果你想理解无关多层架构以及如何拆散关注点的更多信息,请查看以下链接:
- FourLayerArchitecture
- Architecture — The Lost Years
- The Clean Architecture
- Hexagonal Architecture
- Domain-driven design
感激 Thiago Araújo Silva。
参考资料
- NodeJS and Good Practices