在開發企業級應用程式時,要不要把資料處理邏輯寫在 stored procedure 裡面,也是個常見的問題。最近剛好碰到一個狀況,就順便寫點筆記。
我記得在哪裡看過一個原則(也許是 Dino Esposito 的書):如果要寫 stored procedure,裡面應該只包含資料邏輯,而不要包含商業邏輯。
我認為這建議合理。不過其實我基本上不太寫 stored procedure,也不贊成這麼做--除了一些特殊原因,例如效能。一方面,大概是自己的 SQL 太弱;另一方面,有時候不太容易明確區分資料邏輯與商業邏輯,一不小心就「一個邏輯,各自表述」:同一件工作,團隊中有的人選擇寫在 stored procedure 中,有的人認為應該包在 business class 裡,結果就是商業邏輯散落各處,造成維護上的麻煩。
最近在修改一個既有系統的 bug 時,碰到一個狀況,跟 stored procedure 稍微扯上點關係,就順便記錄一下。
是這樣的:該系統的一個基本開發原則是程式碼裡面不可以寫 SQL 指令,所以任何對資料庫的操作都必須寫成相應的 stored procedure(底下簡稱 SP),供應用程式呼叫。也因為如此,大部分的(也許是全部的)商業邏輯都藏在各 SP 裡面。這種做法有個方便之處:將來要修改商業邏輯時,只要到資料庫裡面就能找到了。然而,實務上會不會變成資料庫裡面有一份商業邏輯,應用程式的商業邏輯層又重複寫一份呢?這也值得考慮。
既然是以 SP 來負責處理商業邏輯,寫程式時很自然就會發展出一個慣例:程式中必定有某個類別會提供一個與該 SP 名稱相同(或雷同)的方法,而且它們的參數也都大致一樣。比如說,假設某 SP 的名稱叫做 sp_CheckSomeRule,在商業邏輯層中的某個服務類別就會有一個對應的方法:CheckSomeRule()。只要寫法一致,問題不大。
然後,在追蹤一個 bug 時,我發現問題根源在 sp_CheckSomeRule 裡面。為了修正該問題,必須為那個 SP 增加一個參數。於是我修改了 SP,然後從用戶端程式碼一路追蹤至伺服器端,找出所有與該 SP 對應的方法,並為它們加上新的參數。
然後我發現我一共修改了下列程式檔案(依後端至前端順序列出):
這些介面和類別裡面都有 CheckSomeRule() 方法,而且那些類別幾乎是一個接著一個串連呼叫其他類別。比如說,當使用者點擊某按鈕,程式某處會呼叫 FooManager.CheckSomeRule(),然後該方法又去呼叫 FooProxy 的 CheckSomeRule(),然後是 FooContractService 或 FooService,最後到 FooDataAccess。
有點瘋狂?
此系統的架構,實體部署的 tier 就有四、五層,所以在設計時將應用程式拆成多個 layers 也不令人意外。目前我還不明白是否一定要這麼多層,但這個部份的設計的確值得思量。
此外,雖然程式寫法一致,修改時毫無困難,但就是覺得....怪怪的。
解決方法?
如果基本架構已經無法改變,層層串連呼叫無可避免,使用 Introduce Parameter Object 技巧把方法所需的參數重構一下,應該就可以省下一些工夫。但這也只是解決一部分的問題,在真實世界裡總是沒有這麼單純....
我記得在哪裡看過一個原則(也許是 Dino Esposito 的書):如果要寫 stored procedure,裡面應該只包含資料邏輯,而不要包含商業邏輯。
我認為這建議合理。不過其實我基本上不太寫 stored procedure,也不贊成這麼做--除了一些特殊原因,例如效能。一方面,大概是自己的 SQL 太弱;另一方面,有時候不太容易明確區分資料邏輯與商業邏輯,一不小心就「一個邏輯,各自表述」:同一件工作,團隊中有的人選擇寫在 stored procedure 中,有的人認為應該包在 business class 裡,結果就是商業邏輯散落各處,造成維護上的麻煩。
最近在修改一個既有系統的 bug 時,碰到一個狀況,跟 stored procedure 稍微扯上點關係,就順便記錄一下。
是這樣的:該系統的一個基本開發原則是程式碼裡面不可以寫 SQL 指令,所以任何對資料庫的操作都必須寫成相應的 stored procedure(底下簡稱 SP),供應用程式呼叫。也因為如此,大部分的(也許是全部的)商業邏輯都藏在各 SP 裡面。這種做法有個方便之處:將來要修改商業邏輯時,只要到資料庫裡面就能找到了。然而,實務上會不會變成資料庫裡面有一份商業邏輯,應用程式的商業邏輯層又重複寫一份呢?這也值得考慮。
既然是以 SP 來負責處理商業邏輯,寫程式時很自然就會發展出一個慣例:程式中必定有某個類別會提供一個與該 SP 名稱相同(或雷同)的方法,而且它們的參數也都大致一樣。比如說,假設某 SP 的名稱叫做 sp_CheckSomeRule,在商業邏輯層中的某個服務類別就會有一個對應的方法:CheckSomeRule()。只要寫法一致,問題不大。
然後,在追蹤一個 bug 時,我發現問題根源在 sp_CheckSomeRule 裡面。為了修正該問題,必須為那個 SP 增加一個參數。於是我修改了 SP,然後從用戶端程式碼一路追蹤至伺服器端,找出所有與該 SP 對應的方法,並為它們加上新的參數。
然後我發現我一共修改了下列程式檔案(依後端至前端順序列出):
- MyApp.AppService.Domain.DataAccess.IFooDataAccess 介面
- MyApp.AppService.Domain.DataAccess.FooDataAccess (上述介面的實作類別)
- MyApp.AppService.Domain.Contracts.IFooContract 介面
- MyApp.AppService.Domain.Services.IFooService 介面
- MyApp.AppService.BusinessLogic.FooService 類別
- MyApp.AppService.Services.FooContractService 類別
- MyApp.Aplpication.Proxy.FooProxy 類別
- MyApp.Application.BusinessLogic.FooManager 類別
這些介面和類別裡面都有 CheckSomeRule() 方法,而且那些類別幾乎是一個接著一個串連呼叫其他類別。比如說,當使用者點擊某按鈕,程式某處會呼叫 FooManager.CheckSomeRule(),然後該方法又去呼叫 FooProxy 的 CheckSomeRule(),然後是 FooContractService 或 FooService,最後到 FooDataAccess。
[碎碎念] 我還看到一個 stored procedure,用途是新增一筆記錄。假設該程序的名稱叫做 sp_AddCategory,它需要兩個參數:name、originalName。第二個參數看起來怪怪的....看了原始碼,才知道如果第二個參數不為空字串,就會將整個資料表中所有分類名稱符合 originalName 參數的資料全替換成 name 參數值。私下猜測,這大概是為了避免寫太多 stored procedure 而臨時起意的省事作法。傷腦筋的是,如果要重構此 stored procedure,將它一分為二,這又得改好多東西...Orz
有點瘋狂?
此系統的架構,實體部署的 tier 就有四、五層,所以在設計時將應用程式拆成多個 layers 也不令人意外。目前我還不明白是否一定要這麼多層,但這個部份的設計的確值得思量。
此外,雖然程式寫法一致,修改時毫無困難,但就是覺得....怪怪的。
解決方法?
如果基本架構已經無法改變,層層串連呼叫無可避免,使用 Introduce Parameter Object 技巧把方法所需的參數重構一下,應該就可以省下一些工夫。但這也只是解決一部分的問題,在真實世界裡總是沒有這麼單純....
小弟也有碰到類似的經驗,一樣的method,由於分了很多層,導至層層都在做修改,每一層都只是改個method傳入的參數,以及呼叫下一層參數的方法,雖然這樣也許在擴展上可能比較有彈性,但重覆的事情要做好多遍,總覺得很沒有效率,且有的工程師一懶,還直接跳過中間許多層,這樣倒不如一開始就不需分那麼多層,還是覺得簡單的三層(Data、Business與UI)比較實用,以上是小弟的經驗,不知Huan有何看法呢?
回覆刪除我想這還是得看個別專案的狀況。由於目前接觸的系統,我還沒有完全了解其背景,不敢妄下定論,只是心中存疑而已。是否有必要這麼多層,值得進一步檢視,而且還得詢問當初設計的人,以便確認。基本上,拆得越多層,除錯和維護難免會多一分麻煩。忘了是誰說的一句挺有意思的話:「分散式架構的一個重要原則,就是不要分散。」哈!有點 kuso,但是從效率和維護的觀點來看,也不是沒道理。
回覆刪除