Repository,我可能不會用你 (2) - 範例

上一篇筆記只模糊提了點個人想法,少了程式碼,總覺得有些不著邊際。這次補上一點範例程式碼....

我直接把先前的 LayeredAppDemo 拿來修改,去掉 *.Domain、*.DataAccess 專案(* 號代表 NorthwindApp),以求盡量簡化。於是最終只剩下三個專案,分別是 *.ConsoleClient、*.Infrastructure、和 *.Core。其實 *.Infrastructure 也只是擺好看的,目前沒有任何程式碼,純粹預留給未來。參考下圖:



*.Core 裡面包含兩個子命名空間:Models 和 Services。如有必要,這兩個命名空間也可以分成兩個獨立的 Class Library 專案。

Models 裡面放的是 Entity Framework 產生的模型和類別檔案,Services 裡面則是展現層使用的一些服務類別,例如 CustomerService。各組件的相依關係如下圖所示:


這個簡化的架構並未使用 Repository,設計相對單純:
  • 資料存取的部分就直接交給 Entity Framework 負責。
  • 商業邏輯的部分則寫在 *.Core.Services 類別中。(有一點把 Repository 換成 Service 的味道)

這樣的切分架構,對於小型的、偏重 CRUD 操作的資料庫應用程式來說大致堪用,開發速度也快。如果將 Services 那層拿掉,直接在展現層或者 ASP.NET MVC 的 Controller 類別中直接使用 EF 的 DbContext 來操作資料,那又更快、更省事。可是當程式規模不斷成長,許多類別變得太過複雜、包含太多責任時,該進一步細切就還是得再切出來。一個切分準則是自己寫單元測試的時候感覺一下好不好寫。

程式碼的部分,主要就是先產生 entity model,然後撰寫上一層的服務,例如 CustomerService:

namespace NorthwindApp.Core.Services
{
    public class CustomerService : ServiceBase
    {
        private NorthwindEntities _context = new NorthwindEntities;

        public Customer GetCustomerByID(string id)
        {
            return _context.Customers.Find(id);
        }

        public IList<Customer> GetAllCustomers(int maxCount)
        {
            return _context.Customers.Take(maxCount).ToList();
        }

        public void AddCustomer(Customer customer)
        {
            _context.Customers.Add(customer);
            _context.SaveChanges();
        }
    }
}

最後是展現層的部分,只是很簡單的去呼叫 CustomerService 的方法:

class Program
{
    static void Main(string[] args)
    {
        CustomerService customerService = new CustomerService();
        IList<customer> customers = customerService.GetAllCustomers(10);

        foreach (var customer in customers) 
        {
            Console.WriteLine(customer.CompanyName);
        }
    }
}

這種簡化的架構,中間就只有一層服務層,再加上 Entity Framework 的協助,開發速度應該會蠻快的,而且容易入手。此外,中間的服務層可做為單元測試的重點區域,以及切分應用程式邏輯、商業邏輯、資料存取的一個緩衝地帶。

附帶說明:
  • 這裡沒有使用 ViewModel,所以展現層也是直接使用 *.Core.Models 中的 entity classes。比較複雜的狀況可能會需要加上 ViewModel,專供展現層使用。此時就會需要處理 entity 和 ViewModel 的屬性對應,這個部分可用 AutoMapper 或其他類似的工具來輔助。
  • 這裡也沒有使用 Query Objects 或 Query Specification 模式來設計資料查詢的條件,所以你會看到一堆 Get*** 方法,例如:GetFooByID、GetAllFoos、GetAllFoosWithMaxCount、GetFoosWithSomeConditions....。

這不是肯德基 Domain-Driven!

若將 NorthwindApp.Core 組件視為領域層,Entity Framework 視為基礎建設( infrastructure),那麼這種簡化的分層設計就很難稱得上嚴謹的洋蔥架構或領域驅動架構了。這是因為,以領域為中心的設計方式會儘量避免外界那些容易變動的東西「汙染」領域層;依賴的方向通常是基礎建設依賴領域層,而不是反過來。

若很在意這個問題,領域層 + Repository + 資料存取層(DAL)的設計方式可能會更適合。

從另一個角度來看--也許有點詭辯--如果我們把 Entity Framework 看成跟其他 CLR 類別一樣,亦即相對穩定,不太會變動,也不會想要把 EF 替換成其他 ORM,那麼這種設計倒也還過得去。

8 則留言:

  1. 您好,想請教一下,
    我在 EF 跟 Repository模式相關國外文章看到不少關於Dispose的文章,想請問EF的Context直接使用,不用Using包住真的沒有問題嗎?如您的例子也是直接
    private NorthwindEntities _context = new NorthwindEntities();
    對這感到困惑,不知它是否真的會完全釋放資源,謝謝您。

    回覆刪除
  2. Hi HeartMoon,
    你的顧慮是正確的。不好意思,我在貼範例程式碼的時候,沒有很仔細,漏掉了一些東西。你從範例程式中可以看到,CustomerService 是繼承自 ServiceBase。其實 ServiceBase 類別會負責建立 context 物件,然後有實作 Dispose 方法,並在其中 dispose context 物件。只是我在貼程式碼時,為了省版面,不把 ServiceBase 貼上來,於是在 CustomerService 中補了一行建立 context 的程式碼,卻漏掉 Dispose 方法了。

    至於是否要使用 using,其實有實作 IDisposable 介面就是安全的,只要用戶端記得呼叫 CustomerService 的 Dispose 方法。以 ASP.NET 程式來說,我會以 by request 的方式來建立 CustomerService(或者 DbContet 物件),亦即在 request 開始時建立,request 結束時釋放。這種做法,當然就不會在各個 methond 中使用 using。其好處是可以在整個 request 生命週期中享受 DbContext 的各種好處,如 caching、變更追蹤、交易管理等等。如果是 by method(使用 using),那麼 context 的生命週期就非常短--不是不能用,而是我通常會建議採用 by request 的方式來使用 DbContext。

    最後,"是否會釋放資源?",這個問題,若要精確的說,其實呼叫 context 物件的 Dispose 只是確保該物件本身的 unmanaged resource 獲得釋放(你可以視為 connection 會關閉),至於真正的資源釋放,還是得等到 GC 發生時才會執行回收。
    以上,希望有回答道你的問題。我稍晚也會把範例程式碼補完整,以免引起誤會。Thanks!

    回覆刪除
  3. 您好,感謝您的回覆,大致上了解了。
    不過不太懂您提到的建議採用 by request 的方式來使用DbContext,這個的具體做法大概是怎樣呢?我好像沒看到相關的技術文章,不知道要怎麼實做,因您有提到,所以蠻感興趣想要試試這樣的方式,不知可否提點一下呢?謝謝您。

    回覆刪除
  4. 不好意思,晚了點回覆。
    我把範例程式整理在另一篇文章了:
    http://goo.gl/bSNnu

    回覆刪除
  5. Hi,

    要不要手動 Dispose 的問題也困擾我很久,之前有看過一些文章中說明為了要使用 EF 的延遲加載,所以不要手動 Dispose,而讓 EF 自動去控制 Dispose。

    我自己也有實際測試過,先將 Connection Pool 關掉
    private NorthwindEntities _context = new NorthwindEntities();
    不去寫 context.Dispose();   
    EF 也會自動把 connection close 

    回覆刪除
  6. 是的,我先前看過一篇老外的文章,他的實驗觀察是 EF 的 DbContext 已經會盡快把連線關閉。因此,DbContext 的壽命該多長,就不單單只是考量連線是否盡快釋放的而已了。

    回覆刪除
  7. 那我知道了,要依專案的架構來選擇

    沒有一定那種做法比較好

    我之前有開發一個瀑布流的分享圖文網站,就是交給 EF 去自動 Dispose,它的每天的發文量有一定的數量,也沒有出現過 SQL Server Connection 爆掉的問題,看起來它還是值得信賴的 XD

    謝謝囉

    回覆刪除
  8. 是的,依應用程式的類型與架構來選擇合適的生命週期模型。也謝謝你的分享 :)

    回覆刪除

技術提供:Blogger.