介绍文章《The Single Responsibility Principle》是从《NodeJS and Good Practices》看到的,继续翻译记录。原文翻译:NodeJSandGoodPracticesOriginMyGitHubText软件总是在不断变化,有助于衡量代码质量的一个方面是更改代码的难易程度。但为什么?...如果你害怕改变某些东西,那显然是设计不当。—MartinFowler关注点和责任分离“收集因相同原因而改变的事物。分离因不同原因而改变的事物。”无论是函数、类还是模块,都遵循单一职责原则和关注点分离。从架构开始,让您的软件设计基于这些原则。架构(Architecture)在软件开发中,责任是共同努力实现的任务,例如:在应用程序中表示产品的概念、处理网络请求、用户在数据库中的持久化等等。请注意这三个职责为何不在同一类别中?这是因为它们属于不同的层次,可以分为不同的概念。根据上面的例子,“数据库中的用户持久化”指的是“用户”的概念,也指与数据通信的层。一般来说,与上述概念相关的架构往往分为四层:领域、应用、基础设施和输入接口。领域层在这一层中,我们可以定义充当实体和业务规则的单元,这些单元与我们的领域有直接关系。例如,在用户和团队的应用程序中,我们可能有一个User实体、一个Team实体和一个JoinTeamPolicy来回答用户是否能够加入给定的团队。这是我们软件中最独立也是最重要的一层,可以被应用层用来定义用例。应用层应用层定义了我们应用程序的实际行为,因此负责执行领域层中各单元之间的交互。例如,我们可以有一个JoinTeam用例,它获取User和Team实例并将它们传递给JoinTeamPolicy;如果用户可以加入,它将持久化的责任委托给基础设施层。应用层也可以作为基础设施层的适配器。假设我们的应用程序可以发送电子邮件;负责与电子邮件服务器直接通信的类(我们称之为MailChimpService)属于基础设施层,但实际发送电子邮件的类(EmailService)属于应用层并使用MailChimpService。因此,我们应用程序的其余部分不知道具体的实现细节——它只知道EmailService能够发送电子邮件。基础设施层这是所有层中的最低层,是我们应用程序所有扩展的边界:数据库、电子邮件服务、队列引擎等。多层应用程序的一个共同特征是使用存储库模式与数据库通信或其他一些外部持久性服务,例如API。存储库对象本质上被视为集合,使用它们的层(领域层和应用层)不需要知道底层有什么样的持久化技术(类似于我们的电子邮件服务示例)。这里的想法是存储库接口属于领域层,而实现属于基础设施层,即领域只知道存储库接受的方法和参数。这使得这两层更加灵活,即使在测试时也是如此!由于JavaScript没有实现接口的概念,我们可以想象自己的接口,并基于基础设施层创建一个具体的实现。输入层该层包含我们应用程序的所有入口点,例如控制器、CLI、websockets、GUI(用于桌面应用程序)等。它不应该有任何关于业务规则、用例、持久性技术甚至其他方面的知识逻辑类型!它应该只接受用户输入(例如URL参数),将其传递给用例,最后将响应返回给用户。NodeJS和关注点分离好吧,在所有这些理论之后,您如何将这个理论应用到Node应用程序中?老实说,多层架构中使用的一些模式非常适合JavaScript世界!NodeJS和领域层Node上的领域层可以由简单的ES6类组成。有许多ES5和ES6+模块可帮助创建领域实体,例如:Structure、AmpersandState、tcomb和ObjectModel。让我们看一个使用结构的简单示例:const{attributes}=require('structure');constUser=attributes({id:Number,name:{type:String,required:true},age:Number})(classUser{isLegal(){returnthis.age>=User.MIN_LEGAL_AGE;}});User.MIN_LEGAL_AGE=21;请注意,我们的列表不包括Backbone.Model或像Sequelize和Mongoose这样的模块,因为它们在基础设施层用于与外界通信。因此,我们代码库的其余部分甚至不需要知道它们的存在。NodeJS和应用层用例属于应用层,不像promises,可能会产生成功和失败以外的结果。对于这种情况,一个好的节点模式是事件发射器。要使用它,我们必须扩展EventEmitter类并为每个可能的结果发出一个事件,从而隐藏存储库在内部使用promises的事实:constEventEmitter=require('events');类CreateUser扩展EventEmitter{constructor({usersRepository}){super();this.usersRepository=usersRepository;}execute(userData){constuser=newUser(userData);this.usersRepository.add(user).then((newUser)=>{this.emit('SUCCESS',newUser);}).catch((error)=>{if(error.message==='ValidationError'){returnthis.emit('VALIDATION_ERROR',error);}this.emit('ERROR',错误);});}}这样,我们的条目就可以执行用例并为每个结果添加一个侦听器,如下所示:createUser.on('SUCCESS',(user)=>{res.status(201).json(user);}).on('VALIDATION_ERROR',(error)=>{res.status(400).json({类型:'ValidationError',详细信息:呃ror.细节});}).on('错误',(错误)=>{res.sendStatus(500);});createUser.execute(req.body.user);}};NodeJS和基础设施层基础设施层的实现应该不难,但要注意不要将其逻辑泄漏到上面的层中!例如,我们可以使用Sequelize模型来实现一个与SQL数据库通信的库,并为其提供不暗示底层SQL层存在的方法名称——例如我们上一个示例中的常见添加方法,我们可以实例化SequelizeUsersRepository和将它作为usersRepository变量传递给它的依赖项,这些依赖项可能只与其接口交互。类SequelizeUsersRepository{添加(用户){常量{有效,错误}=user.validate();if(!valid){consterror=newError('ValidationError');error.details=错误;返回Promise.reject(error);}returnUserModel.create(user.attributes).then((dbUser)=>dbUser.dataValues);NoSQL数据库、电子邮件服务、队列引擎、外部API等也是如此。NodeJS和输入层在Node应用程序中实现这一层有很多选择。对于HTTP请求,Express模块是最常用的,但您也可以使用Hapi或Restify。尽管对此层的更改不应影响其他层,但最终的选择是实现细节。如果从Express迁移到Hapi意味着以某种方式发生变化,那么这就是耦合的标志,您应该密切注意修复它。层通信让一层直接与另一层通信可能是一个糟糕的决定,并且会导致它们耦合。在面向对象的编程中,这个问题的一个常见解决方案是依赖注入(DI)。这种技术使类的依赖项可以作为其构造函数中的参数接收,而不是需要依赖项并在类本身内部实例化它们——创建所谓的控制反转。使用这种技术允许我们以一种非常干净的方式隔离类的依赖关系,使其更加灵活和易于测试,因为清理依赖关系成为一项微不足道的任务。对于Node应用程序,有一个很棒的DI模块Awilix,它允许我们利用DI而不必将我们的代码耦合到DI模块本身——所以我们不想在Angular1中使用奇怪的依赖注入机制。Awilix的作者有一系列解释DependencyInjectionwithNode的文章值得一读,还解释了如何使用Awilix。顺便说一下,如果你打算使用Express或Koa,你还应该看看AwilixExpress或AwilixKoa。一个例子即使有所有这些关于层和概念的例子和解释,我相信没有什么比遵循多层架构的应用程序例子更好的了,而且可以简单地使用!您可以查看此基于节点的样板文件,用于WebAPI生产就绪示例。它采用多层架构,并且已经为您提供了基础知识(包括文档),因此您可以练习甚至将其用作Node应用程序的初始化。更多信息如果您想了解有关多层架构以及如何分离关注点的更多信息,请查看此链接:FourLayerArchitectureArchitecture—TheLostYearsTheCleanArchitectureHexagonalArchitectureDomain-drivendesign感谢ThiagoAraújoSilva。参考资料NodeJSandGoodPractices
