CQRS框架(nodejs的DDD開發落地框架)初識感想

CQRS是啥?DDD又是啥?

這兩個概念其實沒什麼神祕的,當然此文章中的這兩個概念以曾老師的課程爲準(關於CQRS和DDD的標準概念,google上已經很多了,不再贅述。)

DDD(Domain Driven Design),領域驅動設計開發。

DDD和OOP有什麼同嗎?其實就我個人經驗來說,沒有任何不同(當然你可以反駁我),DDD就是OOP。這裏以曾老師課上的概念爲準,domain就是世界,包含了當前所有actor的一個域,這個域是一個上帝視角,可以監聽每一個域中發生的事件,並且記錄。

CQRS,既命令和查詢職責分離(Command Query Responsibility Segregation)。

在普通mvc架構中,對於數據庫的CRUD基本都是寫在controller層,這樣一來路由非常臃腫,而且維護起來簡直是噩夢。

CQRS將查詢與職責分離。簡單說來,就是寫操作和讀操作分離,讀操作寫在路由中,寫操作通過面向對象寫入類的業務方法中,這樣路由中的查詢部分薄了,而且對於寫操作的可讀性,重用性和維護性大大提高。

相比較於普通mvc,cqrs分爲核心層Core(及核心層擴展Core Extension)應用層(Application),UI層,看起來3層,其實是四層,但是由於核心層與核心層擴展的伸縮性很強,並且針對項目的大小來決定,所以就我覺得用3.5層來描述比較合適。

取cqrs文檔中的例子
const {Actor} = require("cqrs");

module.exports = class User extends Actor{
  constructor(data){
     const {name} = data;
     super({
       name,
       createTime: Date.now(),
       stars:[], // 被關注明星的 ids
       watchers:[] //  關注者的 ids
     });
  }

  // 關注某位明星
  async follow(starId){
    const service = this.service;
    const star = await service.get("User",starId);
    if(starId !== this.id && star){
      await star.addWatcher(this.id);
      this.$(starId)
    }
  }

  // 取消關注某位明星
  async unFollow(starId){
    const star = await this.service.get("User",starId);
    if(star){
      await star.deleteWatcher(this.id);
      this.$(starId);
    }
  }

  // 加入關注者 watcher
  addWatcher(watcherId){
    if(watcherId !== this.id)
    this.$(watcherId);
  }

  // 取消被關
  deleteWatcher(watcherId){
    this.$(watcherId);
  }

  get updater(){
    return {
      follow(json, event){
        const stars = json.stars;
        stars.push(event.data);
        return {
          stars
        }
      },
      unFollow(json, event){
        const stars = json.stars;

        var set = new Set(stars);
        set.delete(event.data);

        return {
          stars:[...set]
        }
      },
      addWatcher(json,event){
        const watchers = json.watchers;
        watchers.push(event.data);
        return {
          watchers
        }
      },
      deleteWatcher(json,event){
        const watchers = json.watchers;
        const set = new Set(watchers);
        set.delete(event.data);
        return {
          watchers:[...set]
        }
      }
    }
  }
}

以上例子是一個cqrs (傳送門)Actor的實現,通過this.$產生一個事件,事件由updater接收,進行數據的真正修改。

const {Domain} = require("cqrs");
const User = require("./User");
const domain = new Domain();

// 註冊 User Actor 類
domain.register(User);

// 即時異步執行函數
(async function () {

  // 創建用戶1
  let user1 = await domain.create("User",{
    name:"leo"
  });

  // 創建用戶2
  let user2 = await domain.create("User",{
    name:"zengliang"
  })

  // user1 關注 user2
  await user1.follow(user2.id);

  console.log(user1.json.stars); // 打印一下 user1 監聽所有 ids
  console.log(user2.json.watchers);  // 打印一下 user2 追隨者的所有 ids

  user1.unFollow(user2.id);  // user1 取消關注 user2

  // 重新加載 user1 和 user2
  user1 = await domain.get("User",user1.id);
  user2 = await domain.get("User",user2.id);

  console.log(user1.json.stars); // 打印一下 user1 監聽所有 ids
  console.log(user2.json.watchers);  // 打印一下 user2 追隨者的所有 ids

})();

以上是在運行中對User實例對象的操作,關注與取關的操作。

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. -- 某位大牛

以上的例子很好的詮釋了可讀性還有重用性。對於寫操作來說,完全用業務方法來實現,那麼路由中可以僅包含cqrs中Q的部分,這樣做到了業務和查詢分離,那麼迷惑也開始解開了。

  • 寫操作,用業務方法來完成,屬於核心層
  • query,既查詢操作,寫在router中,是應用層變薄

在使用普通mvc的時候,邏輯和查詢通常都會放在路由當中,這樣造成的高耦合性(coupling)讓代碼的重用性,可讀性,可伸縮性很差。維護起來簡直噩夢連連。我的第一個項目是用標準mvc完成,後期加新需求的時候基本山就是牽一髮動全身,也是我的經驗確實不夠對於很多地方沒有對代碼進行可重用的封裝。

現在淺談一下 Auxo(傳送門)

Auxo框架集成了Nuxt(Vue),Vuex,Express,cqrs四個重要框架。這樣在開發時就不用再辛苦搭建開發環境了,直截了當。Auxo是約定式的框架,關於文件結構是根據Nuxt(傳送門)的,所以有必要讀一讀Nuxt的文檔,對Nuxt有一定了解之後就可以用了而且上手很快,因爲基本上不需要配置什麼東西。

在Auxo框架中,數據遵循Event Sourcing原則,分兩個collection。

  • 一個是事件數據庫記錄在domain中發生的所有事件,讓事件回溯、長故事(saga)和事件鎖(lock)成爲可能;
  • 另外一個是查詢數據庫,記錄普通數據,我自己的理解就是面向數據庫開發的那種最基本的數據庫。

eventstore

記錄事件對象的數據庫,可以通過該數據庫的數據進行數據回溯。

snap

事件快照。domain中的事件的一個snapshot,我暫且理解爲一個log

server/index.js 中的 req.dbs req.$domain

這兩個屬性已經在框架中直接掛載在了req對象上,歸功於曾老師。在server/index.js中,已經定義好了,這個文件相當於express的app.js,只是文件名不一樣。
req.dbs就是上述的查詢數據庫,可以使用mongojs來query。
req.$domain就是domain,即上帝視角,可以用以下語句

req.$domain.get('User', uid); // 獲取User對象
req.$domain.create('User', {username: 'ephraimguo', password:'*******'}); // 創建user對象

等domain對象的方法進行數據操作。

在Vue組件中的axiosdomain

這兩個對象已經寫在plugins/文件夾裏面,可以直接在Vue組件中引用如下

<template>
    <!-- Vue Template -->
</template>
<script>
    import axios from '@/plugins/aixos'
    import domain from '@/plugins/domain'
    // ... codes ...
</script>
<style>
    /* some style sheet */
</style>

Listener 核心層擴展 (有個小坑)

起初看到曾老師用listener但是不明白怎麼監聽,而且去看epxress-cqrs的源碼的時候,看到listener的路徑是作參數與傳入了的。

截取一段express-cqrs的源碼
// Register Actors Class from actors folder
ActorList.filter(Actor => /.*\.js$/.test(Actor)).
    forEach(Actor => domain.register(require(path.join(actorPath, Actor))));
    
// Get Listener from listener folder
listeners.filter(listener => /.*\.js$/.test(listener)).
    forEach(listener => require(path.join(listenerPath, listener))(domain));
  • 第一步,在根目錄下添加listener文件夾
  • 第二部,創建新的監聽js文件,
Listener 內部寫法,如下(個人經驗)

module.exports = function(domain){ 
    // Utilise domain.on(...) to make onAction listening
}

這次先暫時聊這麼多,cqrs還有很多好用的方法和思想可以慢慢琢磨,而且這種編程思想易實踐,並且對全局的把控更精準,心有猛虎細嗅薔薇,當然這篇文章也是針對上過曾老師課的童鞋們,不算是掃盲,過後會繼續寫一些關於cqrs框架應用的文章,也歡迎大家提問,並且一起討論。如果有錯誤,也請大家指正,我會馬上修改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章