iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
Software Development

一個好的系統之好維護基本篇 ( 馬克版 )系列 第 27

Day-27: 如何降低 Query 複雜性的探索

  • 分享至 

  • xImage
  •  

就我現在的認知,我覺得 Query 情境比 Command 的情境難處理多,在 Command 的情況下我們用 Domain-Driven Design 或是只單純用 Domain Model 模式我覺得都可以將複雜度降低不少,但在 Query 情況下,目前我到現在還沒看出比較好的模式可以來解決它的複雜度,就算是 GraphQL 我自已也覺得只是解了部份,然下來我就來談談我覺得難題在那。

首先下面這些是我在實務上,我自已比較覺得給我帶來複雜、難以理解、難以維護的情況 :

  1. 相同欄位,但計算方式不一致,導致難理解、難維護 : 例如 UI A 的課程時長是 10 小時 40 分鐘,但是 UI B 的課程時長是 10 小時 42 分鐘。
  2. 相同欄位,但是不同情境就不同定義,導致難維護: 同一個 Query 可能支援的情境不同,例如一個是 UI,一個是內部使用,所以可能為了一個情境改了然後另一個就炸了。
  3. 每個欄位定義不明確 : 例如 Reade Model 的欄位意思,它是如何產生的,命名是否合理。
  4. 很多重複的東西,導致難以維護 : 每一個 Query 請求都要在重寫一個嗎 ?
  5. 權限導致我不能做共同 Query。
  6. 要想辦法 Peak Query 的性能。
  7. 多條件查詢。
  8. 一個方法回傳的物件,被多個地方使用,然後每個地方都有可能新增欄位、修改欄位,最後根本不知道那個欄位被改成什麼樣子了 ( js 的原罪,但工程師也有鍋 )。

然後接下來我一步一步的說,首先我們最開始使用 GraphQL 可以解決的東西。

Query 的複雜性來源與嘗試解

1. 使用了 GraphQL

首先我們用這 GraphQL 這東西,應該有不少了有用了,然後用了它我們的確解決了上述的幾個點 :

  • 4 . 很多重複的東西,導致難以維護 : 每一個 Query 請求都要在重寫一個嗎 ?
  • 6 . 要想辦法 Peak Query 的性能。

GraphQL 它有辦法做到以下這些事情,所以它的確解決了我們第 4、5 點的難題。

  • 可以指定需要的欄位,所以我們就算是不同情境需要不用欄位,那我們只要指定需要的欄位就好。
  • DataLoader 解決 N+1 問題,所以我們節省了不少性能。
  • 有 Cache 機制,所以也幫我們節省了不少性能問題。

2. 嘗試解決常見的 Query 情境 - 權限、分頁、排序、多條件查詢、

但是我們現在有個需求就是:

不同的角色,只能拿到用戶可以管的資料,並且要排序、分頁與多條件查詢

所以為了這個需求,我們在 GraphQL 到 QueryService 中間有進行了,根據角色然後取得到它可以控制的資源,然後在組合 Query 丟到 Service 取得資料。

但這裡有個東西要來說說,那就是分頁。

我們已經有差了一個平台用的 GraphQL 了,那接下來的問題就在分頁,然後我們應該都知道要能用分頁的條件在於 :

資料在同一個資料庫

所以這裡有分兩種情境,假設我們現在已經有 3 個 Bounded Context,那我們現在的架構有兩個選項 :

  1. 每個 Bounded Context 就有一個 GraphQL。
  2. 有一個以平台為單位的 GraphQL BFF 服務,然後去後面的各 BC 抓資料。

第一種架構的情況下,表面上沒毛病,但實際上還是可能會有問題發生,因為它 GraphQL 所提供的 Scheam 就一定是該服務的,但第二種就有可能會出問題,因為它可能會去抓多個 BC 來整合資料。

那第二種怎麼辦 ? 就只能判斷是用 Sub Resolver 的欄位是沒辦法分頁吧。

3. 分平台

然後發現不同平台有不同的欄位意思,所以將 GraphQL 那裡分成,每一個平台都有一個 GraphQL Schema。
但是內部使用的怎辦呢 ? 答案就是我們將平台可取得的邏輯放在 GraphQL 那一層,然後 QueryService 那一裡我們就是做比較接近通用型的。

也就是假設有個需求為 :

在創作者後台上,創作者只能取得到他自已管理的課程

那這樣我們就是:

  • 平台 GraphQL 那裡我們取出他可以管理的 courseIds
  • 然後用這個 courseIds 去 QueryService 取得到資料。

所以這個地方我們解決了這個點 :

    1. 相同欄位,但計算方式不一致,導致難理解、難維護 : 例如 UI A 的課程時長是 10 小時 40 分鐘,但是 UI B 的課程時長是 10 小時 42 分鐘。
    1. 相同欄位,但是不同情境就不同定義,導致難維護: 同一個 Query 可能支援的情境不同,例如一個是 UI,一個是內部使用,所以可能為了一個情境改了然後另一個就炸了。

4. 如何定義好每個欄位呢 ?

我覺得重點在於

  1. 要分的清楚那些是共用
  2. 然後注解要寫清楚 + 有辦法反追回這個欄位是如何產生的。

然後關於第 1 點,我覺得可以想成 QueryService 那的設計比較算是不考慮情境的情況下設計,但準確的說那個欄位是所有情境都適用 ( 共用 ),而 GraphQL 那就是比較可以根據平台考慮情境。

然後第 2 點,我覺得只要能做到以下 2 點,那我覺得應該不算難事 :

  • 熟悉架構,並且架構是正常的情況下,要反追都不是難事。但重點就在於架構與架構規則要正常。

以我們家為例,很多很難追的欄位就在於,我們在很多地方都會 populate 欄位,又或是加個欄位、移除個欄位,然後還有修改欄位的,並且方法往下追可能有 4 ~ 5 層,那就真的很靠北。

然後根據以上的規則,我們應該就已經解決了 :

    1. 每個欄位定義不明確 : 例如 Reade Model 的欄位意思,它是如何產生的,命名是否合理。
    1. 一個方法回傳的物件,被多個地方使用,然後每個地方都有可能新增欄位、修改欄位,最後根本不知道那個欄位被改成什麼樣子了 ( js 的原罪,但工程師也有鍋 )。

好的 Query 架構的特性

那什麼樣的 Query 架構是好的呢 ? 下面是我列出的特點 :

  1. 符合 DRY,不需要太多轉換成本。
  2. 不會一個情境變動,然後欄位的意思就變了,通常就是欄位的意思是符合這個 Read Model 所要支援的情境。
  3. 可以明確知知道每個 Read Model 的欄位意思。
  4. 支援 UI、權限、分頁。
  5. 可以只抓需要的欄位,減少 query 的資源消耗。
  6. 不會影響到 Domain Model ( Write Model ) 那的東西。
  7. 高性能。

例如前台要的『 可顯示課程 』 就會不一樣

1. 符合 DRY,不需要太多轉換成本

就如果要獨立、並且越解耦,那事實上最簡單的方式就是,所有的 Query 都重新寫一次,並且一個回傳的 scheam 就是獨立的,對吧 ?

但是以成本、時間考慮,我們不太可能這樣做對吧 ? 這樣做只會拉高我們的開發時間,與後來要維護的時間,所以我們這裡一定會需要一部份是可做到 DRY。

2. 不會一個情境變動,然後欄位的意思就變了,通常就是欄位的意思是符合這個 Read Model 所要支援的情境

這個是和上面的 TradeOff,例如我很常在 code 中到看到 course.isPublic,但是那個 public 根本就是一中各表,每個地方的 public 的定義都不一樣,不同平台都不一樣,然後接下來這個又在內部用,但內部又沒 public 的概念。

3. 可以明確知知道每個 Read Model 的欄位意思

主要分兩個:

  1. UI 層,通常這是給前端們看的
  2. 內部使用,通常就是我們後端內部拿來計算或什麼用的。

但是這裡我覺得的比較難的是 :

  • 如何 DRY,例如 UI 用的和內部使用的共同業務邏輯。
  • 每個欄位的意思是一樣的。
  • 如何維護下去。

4. 支援 UI、權限、分頁。

我們很多使用情境都是需要考慮 UI + 權限 + 分頁,例如我們就有很多是限制那些畫面那個角色可以看,或是看多少。

5. 可以只抓需要的欄位,減少 query 的資源消耗

如果每一次都像 domain model ( aggregate ) 一樣全部抓出來,但是我們可能只需要 1 個欄位,那真的很好費資源。

6. 不會影響到 Domain Model ( Write Model ) 那的東西

不能因為 query 需要的東西,而修變了 domain model。

7. 高性能

大部份的情況下,Query 情況下高性能是不能避免,你想想你每一次看一個東西都要等一段時間,會有多火。但也不是說 Command 情境下不用,只是它比較重點在於一致性、好維護、好理解。


Q & A

1. 如果 Command 後,我們需要回傳資料模式,要怎麼辦呢 ?

我會建議就去 Query 那取得 ReadModel 資料。如果只是在新增情況下,那種性能消耗不會影響太大,大部份我們需要處理性能的情況是在 Query。

2. 可不可以統一使用 Repository,然後在 Query 情境下也用他 ?

某些方面我覺得可以,但比較建議還是至少分兩個 repository,例如 course-write 和 coures-read 之類的,然後 write 是專門用來更新 domain model,而另一個就是支援 query 用,然後這 2 個 repository 都可以在 command 與 query 使用。

我也看過有人推薦 repository 是只能給 domain model 那用,但是我們在使用時,通常很常需要先去查一下資料才能在進行運算,然後這時很常會有那種實際上可以在 command 與 query 情境下都可以共用的,我就會傾向寫在 course-read repository。

但這樣 coure-read repository 是不是就可能同時回傳 domain model 與 read model ?

3. 在 Query 的架構下,什麼地方是高階呢 ? 什麼地方是低階 ?

根據 Clean Architecture 的定義:

離 I/O 越近的越低階

它的本意是不要讓 I/O 的操作與實作,影響到高階的業務邏輯那,所以正常來說,我們 GraphQL 那的應該算是低階。然後別忘了高階模組不依賴低階模組,雙方都要依賴 Interface。

所以用我們這裡的範例來看 QueryService 就算高階。


小結

老實說這篇寫的不太好,因為很多東西都只是概念上的東西,但在實際在腦袋產出程式碼,又覺得實作上很麻煩,這個主題我可能還要在花時間想一下……


上一篇
Day-26: CQRS ( Command Query Responsibility Segregation )
下一篇
Day-28: 高品質的特性與指標探索 - 維護性
系列文
一個好的系統之好維護基本篇 ( 馬克版 )30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言