NodeJs 与最佳实践(译)

译者:最近在研究前端架构分层,在 medium 看到了这篇关于 node.js 架构分层的文章,以为很不错,特意翻译过来分享给你们,其中不少思想也能够应用到前端项目中。html

原文连接 blog.codeminer42.com/nodejs-and-…前端

文章首发于个人博客 github.com/mcuking/blo…node

软件随时可能更改,而定义代码质量的一个方面就是更改代码的难易程度。可是是什么使它是这样的?git

...若是您惧怕改变某些东西,显然是设计得很差。 —马丁·福勒github

关注点和职责分离

“将因为相同缘由而发生变化的事物汇集在一块儿。分开那些因不一样缘由而改变的事物。”

不管是功能,类仍是模块,它们均可以应用于单一职责原则和关注点分离 the single responsibility principle and the separation of concerns。基于这些原理进行设计软件架构。web

架构

在软件开发中,职责是团结一致要实现的任务,例如:在应用程序中表示产品的概念,处理网络请求,将用户保存在数据库中等等。数据库

您是否注意到这三个职责不在同一类别中?这是因为它们属于不一样的层,所以又能够分为概念。根据上面的示例,“在数据库中保存用户”与“用户”概念有关,也与数据库进行通讯的层有关。npm

一般,与上述概念相关的体系结构倾向于分为四层:domain, application, infrastructure, input interfaces编程

Domain 层

在这一层中,咱们能够定义充当实体和业务规则的角色并与咱们的 domain 有直接关系的单元。例如,在用户和团队的应用程序中,咱们可能会有一个 User 实体,一个 Team 实体和一个 JoinTeamPolicy 来回答用户是否可以加入给定的团队。json

这是咱们软件中最孤立最重要的层,Application 层可使用它来定义用例。

Application 层

Application 层定义了咱们应用程序的实际行为,所以负责执行 domain 层各单元之间的交互。例如,咱们能够有一个 JoinTeam 用例,该用例接收 User 和 Team 的实例,并将它们传递给 JoinTeamPolicy。若是用户能够加入,它将持久化职责委托给 infrastructure 层。

Application 层也能够用做 infrastructure 层的适配器。假设咱们的应用程序能够发送电子邮件;直接负责与电子邮件服务器通讯的类(称为 MailChimpService)属于 infrastructure 层,可是实际发送电子邮件的电子邮件(EmailService)属于 application 层,并在内部使用 MailChimpService。所以,咱们的应用程序的其他部分不知道有关特定实现的详细信息-它仅知道 EmailService 可以发送电子邮件。

Infrastructure 层

这是全部层中的最低层,它是应用程序外部的边界:数据库,电子邮件服务,队列引擎等。

多层应用程序的一个共同特征是使用 repository pattern 与数据库或其余一些外部持久化服务(例如 API)进行通讯。Repository 对象本质上被视为集合,使用它们的层(domain 和 application)不须要知道底层的持久化技术(相似于咱们的电子邮件服务示例)。

这里的想法是,repository 接口属于 domain 层,而实现又属于 infrastructure 层,即 domain 层仅知道 repository 接受的方法和参数。即便在测试方面,这也使两层都更加灵活!因为 JavaScript 并未实现接口的概念,所以咱们能够想象本身的接口,并以此为基础在 infrastructure 层上建立具体的实现。

Input interfaces 层

该层包含应用程序的全部入口点,例如控制器,CLI,websocket,图形用户界面(若是是桌面应用程序)等等。

它应该不具备有关业务规则、用例、持久化技术的知识,甚至不具有其余逻辑的知识!它应该只接收用户输入(如 URL 参数),将其传递给用例,最后将响应返回给用户。

NodeJS 与关注点分离

好了,通过全部这些理论以后,它如何在 Node 应用程序上工做?说实话,多层体系结构中使用的某些模式很是适合 JavaScript 世界!

NodeJS 和 domain 层

Node 上的 domain 层能够由简单的 ES6 classes 组成。有许多 ES5 和 ES6 +模块可帮助建立实体,例如:Structure, Ampersand State, tcombObjectModel

让咱们看一个使用 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.ModelSequelizeMongoose 之类的模块,由于它们打算在 infrastructure 层中用于与外部世界进行通讯。所以,咱们代码库的其他部分甚至不须要了解它们的存在。

NodeJS 与 application 层

用例属于 application 层,与 promises 不一样,用例可能会带来成功与失败以外的结果。对于这种状况,比较好的 Node 模式是 event emitter。要使用它,咱们必须扩展 EventEmitter 类并为每一个可能的结果发出一个事件,从而隐藏了咱们的 repository 在内部使用了 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 与 infrastructure 层

infrastructure 层的实现不该很困难,但要注意其逻辑不要泄漏到以上各层!例如咱们可使用 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 和 input interfaces 层

在 Node 应用程序上实现此层有不少种方式。对于 HTTP 请求,Express 模块是使用最多的模块,但您也可使用 Hapi 或 Restify。最终选择取决于实现细节,尽管对此层所作的更改不该影响其余细节。若是从 Express 迁移到 Hapi 某种程度上意味着在要更改某些代码时,则表示已耦合,而且您应密切注意对其进行修复。

链接这些层

直接与另外一层进行通讯多是一个错误的决定,并致使它们之间的耦合。在面向对象的编程中,解决此问题的常见方法是依赖注入 dependency injection(DI)。这种技术包括使类的依赖项在其构造函数中做为参数接收,而不是引入依赖项并将其实例化到类自己内部,从而建立了所谓的控制反转。

使用这种技术使咱们可以以一种很是简洁的方式隔离一个类的依赖关系,使其更加灵活且易于测试,由于解决依赖关系成为一项琐碎的任务

对于 Node 应用程序,有一个很好的 DI 模块,称为 Awilix,它使咱们可以在不将代码耦合到 DI 模块自己的状况下利用 DI,所以咱们不但愿使用 Angular 1 那种奇怪的依赖注入机制。Awilix 的做者有一系列的文章,它们解释了 Node 的依赖注入,值得一读,而且还介绍了如何使用 Awilix。顺便说一句,若是您打算使用 Express 或 Koa,还应该看看 Awilix-Express 或 Awilix-Koa。

一个实践的例子

即便有了全部这些有关层和概念的示例和说明,我相信没有什么比遵循多层架构的应用程序的实际示例更好的了,这足以使您确信使用起来很简单!

你能够查看可用在生产环境的 boilerplate for web APIs with Node。它采用了多层架构,并已经为您设置了基础配置(包括文档),所以您能够练习甚至将其用做 Node 应用程序的开始模板。

额外信息

若是您想了解有关多层架构以及如何分离关注点的更多信息,请查看如下连接:

相关文章
相关标签/搜索