譯者:最近在研究前端架構分層,在 medium 看到了這篇關於 node.js 架構分層的文章,以爲很不錯,特意翻譯過來分享給你們,其中不少思想也能夠應用到前端項目中。html
文章首發於個人博客 github.com/mcuking/blo…node
軟件隨時可能更改,而定義代碼質量的一個方面就是更改代碼的難易程度。可是是什麼使它是這樣的?git
...若是您懼怕改變某些東西,顯然是設計得很差。 —馬丁·福勒github
「將因爲相同緣由而發生變化的事物彙集在一塊兒。分開那些因不一樣緣由而改變的事物。」 |
不管是功能,類仍是模塊,它們均可以應用於單一職責原則和關注點分離 the single responsibility principle and the separation of concerns。基於這些原理進行設計軟件架構。web
在軟件開發中,職責是團結一致要實現的任務,例如:在應用程序中表示產品的概念,處理網絡請求,將用戶保存在數據庫中等等。數據庫
您是否注意到這三個職責不在同一類別中?這是因爲它們屬於不一樣的層,所以又能夠分爲概念。根據上面的示例,「在數據庫中保存用戶」與「用戶」概念有關,也與數據庫進行通訊的層有關。npm
一般,與上述概念相關的體系結構傾向於分爲四層:domain
, application
, infrastructure
, input interfaces
。編程
在這一層中,咱們能夠定義充當實體和業務規則的角色並與咱們的 domain 有直接關係的單元。例如,在用戶和團隊的應用程序中,咱們可能會有一個 User 實體,一個 Team 實體和一個 JoinTeamPolicy 來回答用戶是否可以加入給定的團隊。json
這是咱們軟件中最孤立最重要的層,Application 層可使用它來定義用例。
Application 層定義了咱們應用程序的實際行爲,所以負責執行 domain 層各單元之間的交互。例如,咱們能夠有一個 JoinTeam 用例,該用例接收 User 和 Team 的實例,並將它們傳遞給 JoinTeamPolicy。若是用戶能夠加入,它將持久化職責委託給 infrastructure 層。
Application 層也能夠用做 infrastructure 層的適配器。假設咱們的應用程序能夠發送電子郵件;直接負責與電子郵件服務器通訊的類(稱爲 MailChimpService)屬於 infrastructure 層,可是實際發送電子郵件的電子郵件(EmailService)屬於 application 層,並在內部使用 MailChimpService。所以,咱們的應用程序的其他部分不知道有關特定實現的詳細信息-它僅知道 EmailService 可以發送電子郵件。
這是全部層中的最低層,它是應用程序外部的邊界:數據庫,電子郵件服務,隊列引擎等。
多層應用程序的一個共同特徵是使用 repository pattern 與數據庫或其餘一些外部持久化服務(例如 API)進行通訊。Repository 對象本質上被視爲集合,使用它們的層(domain 和 application)不須要知道底層的持久化技術(相似於咱們的電子郵件服務示例)。
這裏的想法是,repository 接口屬於 domain 層,而實現又屬於 infrastructure 層,即 domain 層僅知道 repository 接受的方法和參數。即便在測試方面,這也使兩層都更加靈活!因爲 JavaScript 並未實現接口的概念,所以咱們能夠想象本身的接口,並以此爲基礎在 infrastructure 層上建立具體的實現。
該層包含應用程序的全部入口點,例如控制器,CLI,websocket,圖形用戶界面(若是是桌面應用程序)等等。
它應該不具備有關業務規則、用例、持久化技術的知識,甚至不具有其餘邏輯的知識!它應該只接收用戶輸入(如 URL 參數),將其傳遞給用例,最後將響應返回給用戶。
好了,通過全部這些理論以後,它如何在 Node 應用程序上工做?說實話,多層體系結構中使用的某些模式很是適合 JavaScript 世界!
Node 上的 domain 層能夠由簡單的 ES6 classes 組成。有許多 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
之類的模塊,由於它們打算在 infrastructure 層中用於與外部世界進行通訊。所以,咱們代碼庫的其他部分甚至不須要了解它們的存在。
用例屬於 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);
}
};
複製代碼
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 等,也是如此。
在 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 應用程序的開始模板。
若是您想了解有關多層架構以及如何分離關注點的更多信息,請查看如下連接: