Repository,我可能不會用你

當我們已經在應用程式中使用 ORM(例如 Entity Framework),還需要 Repository 嗎?這個問題就像「要不要使用 ORM」一樣,沒有標準答案。這類「該不該」的問題,有時真的挺傷腦筋。

更新紀錄
  • 2012-11-30:些微潤飾,增加參考資料。
  • 2014-05-08:補充一點新發現,增加延伸閱讀文章。

何謂 Repository?

按照 Martin Fowler 的定義,「Repository 是領域層與資料對應層之間的媒介,如同存在記憶體中的物件集合。用戶端會建構查詢規格(query specification),並傳遞給 Repository 以執行特定的資料操作.....概念上,Repository 將資料與其相關操作封裝成物件集合,以便用貼近物件導向的方式來存取資料」。

在設計 Repository 時,常見的做法是為每一個「主要的 entity」設計一個 Repository 類別,稱為 aggregate root。例如客戶資料會有對應的 CustomerRepository,產品資料則是 ProductRepository,訂單則是 OrderRepository;這些都是 aggregate roots。至於訂單細項,應該由 OrderRepository 一併負責處理,就不另外設計 OrderItemRepository。

此外,為了減少重複的程式碼(DRY 原則),有些人會使用泛型 Repository

如果你想看看 Repository 或者分層設計的範例,可以參考:

有了 ORM,還需要 Repository 嗎?

在 ORM--或者說 Entity Framework--還沒那麼成熟的年代,使用 Repository 模式的理由很充分,好處也明顯。可是,當 Entity Framework 和 NHibernate 已經提供了同樣、甚至更多的功能,我們還需要再多加一層 Repository  嗎?

網路上可以找到很多文章,介紹如何搭配 Entity Framework 來設計我們的 Repository。所以「怎麼做」並不是問題,問題在於我們是否該這麼做。

使用 Repository 的理由通常有這幾個:
  1. 方便單元測試。
  2. 避免上層服務直接依賴 Entity Framework,以便將來可以將 EF 換掉,換成 NHibernate 或其他 ORM。
  3. 可搭配 Unit of Work 模式來管理交易。

拿掉 Repository 之後,單元測試還是可以做,只是測試對象改成了商業邏輯層、服務層、或其他更上層的服務;單元測試的粒度可能會粗一點,視情況而定。如果連商業邏輯層都沒有,例如直接在 MVC 應用程式的 Controller 中使用 Entity Framework,單元測試也許會更麻煩一些。

其次,可抽換底層的資料存取元件當然很理想。然而,一旦使用了 Entity Framework,就算我們用 Repository 把它包起來,難保不夠周密,讓某些與 EF 相依的東西悄悄從縫隙中溜出去。除了擔心成本效益,我也懷疑真實世界中有多少案子真的需要而且實際達成了「任意切換 persistence layer」的理想。

至於 Unit of Work,其實 EF 的 ObjectContext 和 DbContext 已經提供了類似的功能。

如此說來,ORM 之上再疊一層 Repository,可能會讓我們費好一番工夫寫一堆不那麼有用、而且長得很像的程式碼,可能增加了程式的複雜性,卻得不到相對比例的好處。因此,在不需要切換 persistence layer 的情況下,比較小而單純的應用程式,我可能不會使用 Repository。如果是比較大型或複雜的系統....嗯,目前我也說不準。

誠如開頭說的,這裡沒有標準答案,有的只是個人主觀的想法(而且以後可能會變)。

文後附上一些參考文章,如果你也碰到類似問題,可以自行研究看看,寫點程式感覺一下,再做決定。

2014-05-08 補充

www.asp.net 網站上的教學文章 Getting Started with EF 5 using MVC 4 裡面有一個步驟是 Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application,可是到了 EF 6 和 MVC 5,新版本的文章 Getting Started with EF 6 using MVC 5 裡面卻把實作 Repository 的步驟拿掉了,如下圖。不知道是不是有什麼原因。



延伸閱讀

17 則留言:

  1. Repository的設計概念,主要是做為領域層與資料對應層之間的媒介。
    這個「媒介」的意思如果單純看做資料查詢的物件,在設計的時候會顯得綁手綁腳,因為很多功能都跟EF等等框架重疊了。

    但是換個角度把Repository看做一個IoC的實做,相依的方向是資料對應層相依領域層。
    也就是說Repository是作為系統邊界的存在,而這個系統邊界是存在領域層與資料對應層之間。
    套用Repository最主要的工作,讓整個領域層更加的凝聚。

    以這樣的角度去思考「泛型 Repository」、「aggregate root」就會發現,這些做法是以資料對應層的角度去實做,而不是以領域層的角度去設計。
    基本上從本質就是有問題的設計,因為資料對應層的工作可以由EF、NHibernate來提供不需要Repository。
    Repository應該只需要設計出領域層需要的介面就夠了。

    簡單說Repository的用意是隔離相依,讓領域層不相依任何其他的層,提高整個領域層的重用性、可測試性。 ^^

    回覆刪除
  2. 感謝 Clark 的分享!
    我發現你有一篇文章詳細說明了你在留言中提到的設計方式,還提供了範例程式,對於評估是否採用 Repository 也很有幫助,所以我將你的那篇文章加入本文的延伸閱讀清單了。
    我的觀察是,BLL 裡面將會有一層薄薄的 Repository(例如 UserRepository、ProductRepoitory....等等),在 DAL 裡面對應的 Repository 類別才提供了對應的實作,例如你的範例中的 SqlUserRepositoryProvider。然後再透過相依性注入的方式將這兩層黏起來。
    如果實際專案的需求會要切換後端資料來源,這樣的設計我想是個不錯的選擇。如果沒有這樣的需求,而且已經決定採用 ORM 的情況下,就得斟酌是否值得引進這個抽象層,畢竟它會增加初期開發的 coding 成本,以及為應用程式加入不少類別(意味著複雜性也可能升高)。
    Thanks :)

    回覆刪除
  3. 採用ORM不牴觸的說,例如EFUserRepositoryProvider。:D

    回覆刪除
  4. 嗯,我想是的。只是有個小小疑問:EFUserRepositoryProvider 如果叫做 EFUserProvider 會不會比較恰當?因為這些 XyzRepositoryProvider 類別都是提供 Xyz 物件,而不是提供特定實作的 Repository。
    關於 Provider 的設計模式,也許改天我會再整理一篇筆記,不過目前腦袋還很混沌,想法還很模糊...隨緣吧 ^^

    回覆刪除
  5. IUserRepository
    -EFUserRepository
    -SqlUserRepository
    目前的專案都是採用上列這樣的命名規則,因為採用EFUserProvider少了代表邊界物件的語意。

    至於先前文章中會撰寫為UserRepositoryProvider這樣落落長的名稱,
    是因為考量UserRepository中有時會封裝一些計算的功能,
    為了跟DI進來的邊界物件作區隔,
    所以額外加上了Provider尾字再隔離出去一層。

    但是現在回頭看,覺得自己有點畫蛇添足阿。XD

    回覆刪除
  6. 嗯,了解。Thanks ^^
    順便一提,為了平衡報導,我正在整理另一篇支持 Repository 的筆記。

    回覆刪除
  7. 其實大多數所謂的要切換 persistence layer 的終極目標,大多只是為了 "要能夠用讓系統配合不同的 RDBMS"。

    專案不太需要,產品才有可能。
    只要搞清楚,persistence layer 的更換真正意義是什麼,要真的是換 RDBMS 的話。

    只要設計得宜,EF / NHibernate 這些東西甚至不需要出現。(有當然更好,節省開發時間)。

    回覆刪除
  8. IT Player, 我昨天才從一位老朋友那裏聽到一件正在發生的事情:原本用得好好的 Oracle,客戶嫌她「太慢」,而開始慎重考慮把資料庫換成 Sybase。他們做的是專案,客戶說了算。或許真的世事難料,原本以為不可能替換的東西,還是有那麼一絲機率會發生 XD
    至於把 Oracle 換成 Sybase 的原因是嫌 Oracle 太慢,這又是另一則故事(笑話)了 Orz

    回覆刪除
  9. Repository一開始的用意,的確只是隔離BLL與DAL,讓領域邏輯不相依DB。系統演進的過程中就會發現,Repository除了隔離DB、讓BLL層更加的凝聚之外,也封裝的資料來源這個概念。

    也就是說系統之外的資料來源,都可以用Repository去隔離。例如說:別人的系統、公司內部的子系統、硬體設備,這些都可以當作DAL層的來源。

    回覆刪除
  10. 嗯,Repository 作為隔離之用,合理。

    回覆刪除
  11. 對於系統之外的各種資料來源....老實說這個部分我還有點疑問:是只定義一個 IRepository 介面,然後有多種 Repository 實作,還是每碰到一種一種資料來源,就會有不同的 Repository 介面?
    也就是說,是將 DRY 原則發揮到極致(也許是採用泛型 Repository?),還是只是要讓程式撰寫模型盡量趨於一致,這部分我欠缺實務經驗,只憑空想像,覺得似乎是後者的成分居多。
    還看過一種說法:如果是關聯式資料庫,就使用 ORM;如果是非關聯式資料庫,則針對那類資料來源實作一個 DAO。其基本理念是:尊重底層儲存體的結構。
    寫得有點多了....這些原本打算整理到下一篇筆記的。不過還是很高興聽到各種實戰經驗分享,很難得的。Thanks!!

    回覆刪除
  12. 在設計上會是一個 IRepository 介面,然後有多種 Repository 實作。
    這裡面了設計思路,是IRepository是為BLL服務,只提供BLL服務需要的功能。
    以這樣的角度去思考,就能讓IRepository的設計範圍不發散。

    至於泛型Repository,我是比較少用到的設計,畢竟這樣的設計是以DAL層的角度來看待Repository。
    通常會用的場合是在使用EF的場合下,EF已經幫忙建好提供CRUD的物件,
    繼承這個物件並且讓這個物件實做IRepository,就能注入到BLL層,這樣是比較有可能會用到泛型Repository的情景。

    回覆刪除
  13. 我猜測MVC5+EF6抽掉Repository的原因,是因為先前EF5的版本需要Repository做切換測試,但是EF6有了DbSet,,所以不需要利用Reposity也可以直接在記憶體測試??

    回覆刪除
  14. EF5 有 DbSet 耶。如果我沒理解錯,DbSet 應是從 EF 4.1 開始出現(DbContext 和 DbQuery 也是)。
    MVC5+EF5 教學範例之所以略過 Repository,背後的原因或許只是單純因為想要簡化練習步驟,或者是因為 Repository 的相關文章到處都找得到,不需要再重複了。不過猜想畢竟只是猜想。實務上,還是依個別專案的實際需要來決定是否採用。我的看法是若沒有需要、沒有為專案帶來明顯的好處,就不要加上 Repository 層。

    回覆刪除
    回覆
    1. 抱歉打錯字,上面的 "MVC5+EF5" 應是 "MVC5+EF6",

      刪除

技術提供:Blogger.
回頂端⬆️