應用程式的分層設計 (2) - 一點改進

延續上集,這次要繼續修改第一版的很粗略的範例程式,再加一點東西進去。

概觀

先看一下整個範例的專案結構。這次的版本比上一版多了一個商業邏輯層,總共五個組件:
  • NorthwindApp.ConsoleClient:Console Application 專案,角色屬於展現層(presentation layer)。
  • NorthwindApp.Service:Class Library 專案。服務層,或應用程式層。撰寫應用程式邏輯的地方。這應該是薄薄的一層,不包含商業邏輯,而只是利用下一層的領域物件來提供展現層所需的服務。
  • NorthwindApp.BusinessLayer:Class Library 專案。商業邏輯層
  • NorthwindApp.Domain :Class Library 專案。領域層,用來放領域模型。上一版是將商業邏輯也放在這裡,這次進一步切開了。
  • NorthwindApp.DataAccess:Class Library 專案。資料存取層。這次底層還是使用 Enterprise Library 的 DAAB 來存取資料庫。

在 Solution Explorer 裡面看起來像這樣:


這次新增加的 NorthwindApp.BusinessLogic 組件中只有一個類別:OrderManager,它的責任是處理與訂單有關的商業邏輯。這裡我採用 *Manager  的方式來命名商業邏輯管理員(business managers),所以未來可能會有一堆 managers,例如 ProductManager、CustomerManager。

底下這片白板顯示了各層模組之間的關係,以及類別之間的互動:


其中紅色的文字是各組件中的類別(除了 UI 層的 Main 方法)。注意右邊的 Domain Model 在左邊四層當中都有用到。

用 Visual Studio 產生的組件相依圖可能會更清楚些:



從相依關係圖可以明顯看出 NorthwindApp.Domain 是由所有組件共用,也就是說,從資料存取層開始就會用到其中定義的 entity 類別,並且把它們當作 DTO,一路傳遞至商業邏輯層、應用程式服務層、以及展現層(也會由上層往下層傳遞)。接著進一步解釋為什麼要這樣設計。

Note:服務層(或者說 Application Layer)不是絕對必要;如果實際的需求並不那麼複雜,架構沒那麼大,也可以讓 UI 層直接使用商業邏輯層。

領域層

這次的 NorthwindApp.Domain 組件中只單純放領域類別(domain classes),其作用等同於 DTO(Data Transfer Objects),主要是用來傳遞資料(entities)。請注意:當文中提到「domain class」和「entity class」時,它們指的是同樣東西。domain model 在這裡也是跟 entity model 同樣意思。嗯,這的確不是那麼「領域驅動」,至少現在還不是。
Note: 如果你對領域類別的解讀是偏向包含商業邏輯的 business class,可能就會覺得 NorthwindApp.Domain 的設計有點怪,應該要把它跟 BusinessLogic 擺在一起才對。我想這是對 "domain" 一詞的觀點不同所致。如果有這種情形,請暫時記住它們只是 DTO 或 entity class 而已。

問題:為什麼要將 entity classes 拆出來放在單獨的組件呢?

反過來問,如果不這樣的話,那些 entity classes 要放在哪一層?

如果將 entity classes 放在 BLL(商業邏輯層),那麼你的 DAL(資料存取層)就必須參考 BLL 組件。可是由於 BLL 必定得參考 DAL 組件來存取資料,這就有了組件循環參考的問題--Visual Studio 不會讓你這麼做。

如果將 entity classes 放在 DAL 呢?組件循環參考的問題是解決了,可是你的 UI 層卻得直接參考 DAL 組件,這又不大好了。如果朝這條路繼續想辦法,大概會得出一個結論:我們必須在 UI 層和 BLL 之間再插入一層(可能叫 ViewModel),用來放 UI 所需的 entity 類別,然後再寫一些程式碼來將 DAL 的 entity 屬性對應(複製)到 UI 層的 entity 類別(也就是要寫一些 object to object 的程式碼)。這樣的設計聽起來也合理,但是層次分得越細,要花的工當然就越多,程式也越複雜。就目前而言,我們並沒有明顯感受到增加這些額外的複雜性能夠帶來多大的好處,所以就還是先採用比較簡單的作法吧。

所謂比較簡單的做法,就是把所有的 entity 類別放在一個單獨的組件裡,例如 NorthwindApp.Domain。如此一來,UI 層、商業邏輯層、資料存取層便都會參考這個共用的 Domain 組件。在目前這個版本的範例中,我傾向讓領域類別保持單純,亦即 POCO,主要當 DTO 用,不放任何商業邏輯。但有時候可能會需要在這裡寫一點點資料驗證的程式碼,我認為這倒還好。資料驗證的程式碼有可能散在各層,包括 UI、DAL、BLL,以及大家共用的 Domain Model,但還是應該以 DAL 和 BLL 為主。

商業邏輯管理員

前一版的服務層是直接使用 Repository 類別來存取資料,現在有了商業邏輯層,UI 或服務層就不用再直接碰觸資料存取層,而是由商業邏輯層裡面的 Manager 類別代勞了;Manager 類別會再去呼叫 Repository 類別來處理資料。參考下方 OrderManager 類別的實作:

namespace NorthwindApp.BusinessLogic
{
    public class OrderManager
    {
        private IOrderRepository orderRepository;

        public OrderManager()
        {
            // TODO: Use IoC container to get the repository instance, 
            //       so that we can easily replace the implementation in the future.
            orderRepository = new OrderRepository();            
        }

        public Order GetByID(int id)
        {
            return orderRepository.GetByID(id);
        }

        public IEnumerable<Order> GetOrders()
        {
            return orderRepository.GetAll();
        }
    }
}

服務層的 OrderService 類別會使用 OrderManager:

namespace NorthwindApp.Service
{
    public class OrderService
    {
        private OrderManager orderManager;

        public OrderService()
        {
            orderManager = new OrderManager();
        }

        public Order GetByID(int id)
        {
            return orderManager.GetByID(id);
        }

        public IEnumerable<Order> GetAll()
        {
            return orderManager.GetOrders();
        }
    }
}

展現層的部分與前一個版本相同,這裡就不列出來了。

利用 ADO.NET Entity Data Model 來產生領域類別

上次曾介紹一個工具,可以幫你產生這些 entity classes,省得自己手工打造每個領域類別。另一個方法,是利用 T4 來產生這些類別。這部分可以參考 91 的文章:透過 T4 產生對應 DB table 的 entity

這裡偷懶一下,直接利用 ADO.NET Entity Data Model 來產生領域類別--不使用 DbContext 或 Entity Framework 的其他功能,就只是單純利用它產生的 entity classes 而已。我不知道實務上有沒有人這樣用,感覺上有點奢侈,但的確方便。

首先,開啟 Visual Studio 2012,在 NorthwindApp.Domain 專案中建立一個資料夾:Model。我打算把所有 entity classes 放在這個資料夾裡面。於是,將來存取這些類別時,使用的命名空間就會是 NorthwindApp.Domain.Model.*。

接著在 Model 資料夾中加入一個 ADO.NET Entity Data Model,命名為 NorthwindModel.edmx。建立此資料模型時,選擇「Generate from database」,如下圖所示。


節省版面起見,其他操作步驟的畫面就不貼上來了。若對這部分的操作不太熟悉,可以參考MSDN 網站上的教學影片:Database First

產生 entity model 之後,我另外手動將所有的 Order_Detail(s) 類別與集合屬性名稱改為 OrderDetail(s)。這只是個人偏好,不是必要步驟。最終的 entity model 如下圖所示:



不過,這個模型在這裡並不重要。就如前面提過的,我們只是利用它來產生領域類別而已。

領域類別可以從 Solution Explorer 中的 NorthwindApp.Domain 專案的 Model \ NorthwindModel.edmx \ NorthwindMode.tt 底下找到。如下圖所示:


在建立 NorthwindModel.edmx 時,會產生兩個 T4 樣板檔案(.tt):NorthwindModel.Context.tt 主要是用來產生 DbContext 類別,NorthwindModel.tt 則是用來產生領域類別。底下列出其中一個領域類別 Customer.cs 的原始碼:

public partial class Customer
{
    public Customer()
    {
        this.Orders = new HashSet<Order>();
        this.CustomerDemographics = new HashSet<CustomerDemographic>();
    }

    public string CustomerID { get; set; }
    public string CompanyName { get; set; }
    public string ContactName { get; set; }
    public string ContactTitle { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }
    public string Phone { get; set; }
    public string Fax { get; set; }

    public virtual ICollection<Order> Orders { get; set; }
    public virtual ICollection<CustomerDemographic> CustomerDemographics { get; set; }
}

你可以看到,Entity Framework 5 所產生的 POCO 類別還蠻簡潔的,應該不用改太多就可以跟 Enterprise Library 的 row mapper 一起搭配使用。

到目前為止,跟前一版的範例程式比較起來,只是組件的安排略有調動(領域模型獨立出來成為單獨的組件)以及領域類別改由 EF 產生,所以程式碼的部分並沒有改太多,整個 solution 就可以順利通過編譯。不過,執行時仍會出現錯誤:

Unhandled Exception: System.InvalidOperationException: The column Customer was not found on the IDataRecord being evaluated. This might indicate that the access or was created with the wrong mappings.

這是因為 EF 幫我們產生的 Order 類別定義中包含了與其關聯的 Customer、Employee、Shipper 物件,導致原本的 OrderRepository 在利用 Enterprise Library 來對應物件屬性與資料欄位時找不到匹配的欄位名稱。底下是稍微修剪過的 Order 類別定義:

public partial class Order
{
    public Order()
    {
        this.Order_Details = new HashSet<Order_Detail>();
    }

    public int OrderID { get; set; }
    public string CustomerID { get; set; }
    public Nullable<int> EmployeeID { get; set; }
    ....(略)
    public virtual Customer Customer { get; set; }
    public virtual Employee Employee { get; set; }
    public virtual ICollection<OrderDetail> OrderDetails { get; set; }
    public virtual Shipper Shipper { get; set; }
}

暫且將其中的 Customer、Employee、和 Shipper 這幾行屬性宣告註解掉,程式就可以順利執行了。執行結果跟前一版的範例一樣:


加入物件工廠

前一版的 OrderRepository 類別已經有實作一個 GetByID() 方法,可以取得特定編號的訂單。其他方法則尚未實作,例如 GetAll()。這次把它加上,與 GetByID() 一併列出來:

public Order GetByID(int id)
{
    string sql = String.Format("select * from Orders where OrderID={0}", id);
    IDataReader rdr = database.ExecuteReader(CommandType.Text, sql);
    if (rdr.Read())
    {
        IRowMapper<Order> mapper = MapBuilder<Order>.BuildAllProperties();
        Order order = mapper.MapRow(rdr);
        return order;
    }
    return null;
}

public IEnumerable<Order> GetAll()
{
    var orders = new List<Order>();
    string sql = "select * from Orders";
    IDataReader rdr = database.ExecuteReader(CommandType.Text, sql);
    while (rdr.Read())
    {
        IRowMapper<Order> mapper = MapBuilder<Order>.BuildAllProperties();
        Order order = mapper.MapRow(rdr);
        orders.Add(order);
    }
    return orders.AsEnumerable<Order>();
}

這兩個方法都有重複的程式碼:建立 Order 物件和對應屬性的部分。將來,我們很可能需要以不同的方式來建立 Order 物件,比如說,在建構函式中傳入一個 Customer 物件來指名該訂單隸屬某位客戶,或者傳入一個 Order 物件來複製一個新物件,諸如此類的。

既然建立物件的方式可能有很多種,而這裡又出現了重複的程式碼,這似乎在暗示:把建立 Order 物件的責任放在 OrderRepository 裡面已不太恰當,有違反 SRP(單一責任原則)的疑慮。也許我們可以將這部分獨立出去,由另一個類別來專門負責建立 Order 物件--就叫它 OrderFactory 好了。參考下圖:


我將這個工廠類別也放在資料存取層,跟 OrderRepository 一起。程式碼如下:

public static class OrderFactory
{
    public static Order CreateOrder(IDataRecord record) 
    {
        IRowMapper<Order> mapper = MapBuilder<Order>.BuildAllProperties();
        Order order = mapper.MapRow(record);
        return order;
    }

    public static IList<Order> CreateOrderList(IDataReader reader)
    {
        var orders = new List<Order>();
        while (reader.Read())
        {
            orders.Add(CreateOrder(reader));
        }
        return orders;
    }

    public static Order CloneOrder(Order order)
    {
        throw new NotImplementedException();
    }
}

除了 CreateOrder 方法,我還加了一個 CloneOrder,表示該方法是要從既有的 Order 物件複製成一個新的。由此可見使用 OrderFactory 的一個好處是,你可以取個恰當的方法名稱,而不用不像建構函式那樣只能用 new 運算子來建立物件。物件工廠的寫法讓程式碼更直覺、容易理解。

有了 OrderFactory,原先 OrderRepository 的程式碼就可以改成這樣(變動的部分已標註 *** ):

public Order GetByID(int id)
{
    string sql = String.Format("select * from Orders where OrderID={0}", id);
    IDataReader rdr = database.ExecuteReader(CommandType.Text, sql);
    if (rdr.Read())
    {
        return OrderFactory.CreateOrder(rdr); // ***
    }
    return null;
}

public IEnumerable<Order> GetAll()
{
    string sql = "select * from Orders";
    IDataReader rdr = database.ExecuteReader(CommandType.Text, sql);
    return OrderFactory.CreateOrderList(rdr); // ***
}

集合物件型別的選擇:在傳回集合物件時,這裡同時使用了 IList 和 IEnumerable 泛型介面。一般來說,可盡量採用輕巧的 IEnumerable 泛型介面。若呼叫端需要使用 IList 來操作集合,只要呼叫 ToList() 方法就能轉成串列。有時我也會用 IList 當作回傳型別,並且在這類方法的名稱後面加上 "List",例如這裡的 CreateOrderList()。另一個選擇是傳回 IQueryable 泛型介面,它具有延遲執行(deferred execution)的特性,在某些情況下有比較好的查詢效能

也許你已經知道,這裡的 OrderFactory 其實是運用了 Domain-Driven Design 的工廠模式(factory pattern)。值得一提的是,這裡的工廠模式與 GoF 的工廠模式並不相同。DDD 的工廠模式偏向領域層次的概念,GoF 的工廠模式則屬於「技術層次」...呃,有點抽象。這麼說吧:我們可能會在 DDD 的工廠類別裡面運用 GoF 的工廠模式(例如 Abstract Factory 或 Factory Method),但不太可能反過來做。

問題:我們需要為所有的領域類別提供物件工廠嗎?例如:CustomerFactory、CategoryFactory、ProductFactory、OrderDetailFactory....等等。這樣看起來好像比較一致,但其實很容易弄出一堆不是那麼必要的類別。當你覺得增加某個工廠類別能夠讓程式碼更簡潔、更容易閱讀,以及最重要的--更容易維護,只有在這個時候才去寫它,否則只是徒增應用程式的複雜性而已。

實作查詢條件

目前我只為 OrderRepository 實作了兩個查詢方法:GetByID() 是查詢特定編號的訂單,GetAll() 則是傳回全部的訂單。實務上通常會有更多種查詢條件,例如:

  • GetByCustomer:查詢特定客戶的訂單。
  • GetByDate:查詢特定日期的訂單。
  • GetByTotalAmount:查詢總金額落在特定範圍內的訂單。

為每一種查詢條件寫一個方法,看起來好像有點老土。比較「高級」的作法有查詢物件(Query Object)和規格模式(Specification Pattern)。

然而就目前的進展來看,還是先用「土方法」就夠了。況且,這種寫法具有直觀、易讀的優點(只要查詢方法的名稱取得恰當),也不至於太差。

小結

跟上一版比起來,這次主要是把層次切得比較清楚了。修改的部分包括:
  • 把商業邏輯層和領域層分別放在不同的組件:NorthwindApp.BusinessLogic.dll 和 NorthwindApp.Domain.dll。
  • 原先服務層是直接使用 Repository 類別來存取資料,現在已不直接依賴資料存取層,而是透過商業邏輯層的 Manager 類別(例如 OrderManager),再由 Manager 類別透過 Repository 取得資料。
  • 領域層裡面的類別基本上只包含 POCO 類別,作為 DTO,供 UI 層、商業邏輯層、和資料存取層共用。
  • 領域類別改由 ADO.NET Entity Data Model 產生(純粹為了節省時間)。
  • 加入工廠類別(OrderFactory)。

沒有改變的部分,是依然使用 Repository 模式來作為商業邏輯層與資料存取層之間的媒介,且實作時是將 repository 類別放在資料存取層。此外,資料存取層也還是繼續使用 Enterprise Library 的 DAAB 來處理關聯式資料模型與領域物件模型的對應。

資料存取的增、刪、改、查,目前只碰觸到「查」而已。其他三種操作,也許將來有時間寫續集時再補上。不過,我想 Repository + EL DAAB 的實作範例大概就到此打住,下次可能會把 Repository 裡面的 DAAB 換成 Entity Framework 吧。

各種作法都體驗一下也不錯!Happy coding :)

下載範例程式


  • 開發環境:Visual Studio 2012、SQL Server 2008、Northwind 資料庫。
  • 執行之前記得要修改 ConsoleApp 應用程式專案的 app.config 裡面的連線字串參數。

延伸閱讀

24 則留言:

  1. entity classes比較建議封裝在BLL內,提高BLL的凝聚力,減少設計出貧血的Entity。

    實做細節可以參考一下這篇 http://www.dotblogs.com.tw/clark/archive/2012/07/30/73721.aspx

    回覆刪除
  2. 嗯,這篇文章提到的解決方法是:「將Object物件歸類進BLL裡,並且在BLL與DAL之間套用IoC,反轉Control物件與ObjectReposository物件之間的相依性。這樣的設計避免BLL、DAL之間的循環相依、提供了彈性讓BLL可以抽換DAL實作,並且加強了BLL的內聚。」
    我反覆琢磨這句話,仍不太確定實作程式碼是否就是我想像的那個樣子。印象中,我看過其他文章也有類似的解法,但是就如我在本文中提到的,這還是得多插入一層,才有辦法解決互相循環參考的問題;其利弊得失,還是得進一步討論分析。就此範例目前的版本,我覺得還不需要引進這種複雜性,而且我的確是希望我的 domain classes 是單純的 POCO(貧血物件?)。謝謝您的意見,歡迎進一步討論 :)

    回覆刪除
  3. 我想一樓應該是DDDer, DDD專注在domain entity的設計上, 按照DDD的設計方式, domain entity會歸在domain layer (以三層式設計來說就是business layer),repository的介面也會放在domain layer裡

    而DDD又認為domain model不應該只有data而沒有behavior,如果是只有data的話就是anemic domail model了,而無法展現domain object之間溝通的好處(http://martinfowler.com/bliki/AnemicDomainModel.html)

    不過我認為一般的三層式設計entity當純DTO在三層間往返就很足夠了, DDD需要些門檻才能理解

    回覆刪除
  4. 多謝補充!
    我昨天也看了一下 Fowler 的<貧血領域模型>文章內容。可以理解,但不是完全贊同。我認為貧血物件--或者說被動物件--仍有適用的地方,例如 Book 不會自己翻頁,不會自己借出。如果硬要為 Book 提供行為,總覺得不自然。嗯...我還是比較資料驅動,而非領域驅動;使用 EF 時,我也偏向 model first,而非 code first。
    Thanks ^^

    回覆刪除
  5. 我想,我會在下一個版本當中,把 NorthwindApp.Domain 的名稱改一下。也許改為 NorthwindApp.Persistence,所以本文中的 Order 類別全名就會是 NorthwindApp.Persistence.Model.Order。這樣應該可以避免跟 DDD 的術語混淆,減少閱讀和理解上的困擾。
    再次謝謝樓上兩位提供的資訊!

    回覆刪除
  6. 請問版主,對於 EF 作為 ORM 想請教個問題,就是 EF 對應的是實際資料庫 Table 的時間欄位名稱與型別,但是萬一資料欄位變動或是欄位型別改變就很麻煩了,試用過後,似乎使用更新 model 的方式,也無法根治?還請版主指教一下

    回覆刪除
  7. 好文,尤其裡面版主對於 Anemic Domain Model 的論述,我覺得才是核心。

    有好多 developers 強調所謂的 true DDD 的做法頂不實際的,在大多數的情況下,以 Transaction script 為藍本(Transaction Script 一詞請參見 Martin Fowler)的做法引入 anemic domain model as DTO 配合3 layer 即可解決很多所謂的架構性需求(本文的作法雷同,也可能我會錯意還請版主指正)。

    又如果可以按照規矩來,上面這樣的組合再變形成 3 Tier 也輕鬆得多。

    我很好奇,檯面上(in Taiwan)老強調 true DDD 精神多好架構美好的大神們,有沒有真正做到或是做過 DDD 為本的 3 Tier 系統,並且沒有遭逢 Production performance and maintained suffer....

    古有云: To say is one thing and to do is ANOTHER. 誠不我欺也。

    回覆刪除
  8. 另外版主在留言中提到改用 Persistence.Model 的做法來進行 Namespace 的區隔避免跟 DDD 的意念混淆,深表認同,我自己也是這樣做。不過為了未來 ViewModel 的引入,我還是會在 Namespace 上面加上 Domain,也就是變成 XYZ.Domain.Persistence.Model

    未來有 ViewModel 引入的時候
    XYZ.Domain.View.Model

    這樣也不用後來糾結於命名問題以及溝通問題。
    野人獻曝,還請不吝討論。

    回覆刪除
  9. 其實設計的重點,是在領域層物件的定義上。
    領域層是以完成某個功能來做設計,這些功能職責散落在領域層的物件上。

    採用DDD方式也會設計出貧血的物件,這不是錯誤的。
    因為貧血物件有存在的價值,有些物件就只是單純的資料。

    但是如果整個系統設計下來,全部都是貧血物件,
    那就要檢查看看,
    實際完成系統功能的職責都落在哪邊,這些職責是不是應該有物件來歸納。
    透過這樣的反覆的設計,就能將系統應該完成的功能,封裝成完整的領域層。

    回覆刪除
  10. When it comes to Tier architecture, pure DDD doesn't work well.

    Martin Folwer 所提到的 DDD 裡面有一個件事情是...有了 Hibernate 這等相關的 ORM 技術開始出現後,使得 DDD 這種觀念得以更好(容易)實現。

    但是用過這樣類似的 ORM Framework 以後其實會發現,你的 Domain logic/DataAccess 被放在同一個機器(App Domain/Execution Context)。

    如果要拆解這樣的狀況(開始要擴充機器,有所謂的 Physical Tier 產稱),Business Layer(Tier) 跟 Data Acccess Layer(Tier) 之間又得要透過另外一種 Remote 的方式來存取,這時候 DDD 的精神會隨著所謂的實體架構複雜開始進行妥協,增加的 Overhead 更大。

    非常有血的 Domain Object 也要開始截肢或是轉生(clone to DTO or ViewModel) 最後都四不像。

    DDD 不是不可使,只是很多時候不太好使。

    很多範例或是說明,鮮少觸及這些議題,針對一個中小規模的系統來說、的確沒什麼可挑剔的。但是大規模的分散式系統,DDD 用到底大概只是提前瓦解整個時程以及團隊。

    洋蔥式的架構可能比較符合大多數場合,也比較沒這麼憋扭。
    http://jeffreypalermo.com/blog/the-onion-architecture-part-1/

    這話題十幾年了,還是可以討論的很熱烈,不過經歷過一兩次後,私以為 DDD 大概就是一種理想大於實務說。

    再者,此門檻頗高,市場上有這麼多人才可以流通嗎?也是另外一大議題,就講 3 layer 的基本架構就好了,有辦法從頭到尾實作出來的 developer 現在都很難面試撈到囉。

    回覆刪除
  11. To 6樓:指教不敢,但我不太明白你說的「無法根治」是什麼情形,可否描述一下?
    To 7樓、8樓、和 Clark: Good point! 多謝你們的分享! 總歸一句話:Context is king 囉 :)

    P.S. 算第幾樓有點麻煩,不麻煩的話,留言時輸入一個名稱或代號,會比較方便討論。

    回覆刪除
  12. To 10樓:我猜您也是 7 樓和 8 樓的主人(猜錯莫怪)?看到這句:「非常有血的 Domain Object 也要開始截肢或是轉生(clone to DTO or ViewModel) 」我笑了。
    這話題確實由來已久,就像 to ORM or not,很多年以前就有人說,ORM 就像是越南戰爭一樣(有興趣的朋友可以搜一下 orm vietnam)。許多設計理念或想法在當時的 context 之下或許大致適用,但隨著軟體技術不斷翻新,也需要重新檢視某些設計的合理性。

    回覆刪除
  13. 另外,您提到洋蔥式架構,順著這個線索去找,又可以發現六角架構(Hexagonal Architecture)、Screaming Architecture 等文章,挺有意思。

    在本系列的第一篇文章裡,我提到希望能拋磚引玉。現在看來是有起到一些作用了 ^^
    Thanks!!

    回覆刪除
  14. Hi Huanlin 我是 7, 8, 10 的主人。

    我是有感而發,講了很久 DDD 如果還是只抱著 Martin Fowler 的那本書不放,怕是有點守舊了,不然講 DDD 最起碼也要搬出 Eric Evans 的那一本也比較命中核心。

    實際上有些觀念雖好,但值得檢視其合理性,時空與技術的變遷有很多做法都可以不一樣(觀念可能還是差不多),實務上(解決問題優先),先定義問題才可以決定討論的 context,就可以導引出要用什麼架構,遵循什麼學派。
    如果 problem domain 是在好維護、以及可擴充的範疇,那麼 DDD 就像殺雞用牛刀。
    而我所見識到的,目前台灣的業界,缺乏的就是好維護以及可擴充的議題居多,只要能好好堅守 layered architecture,引用一兩個 infrastruture framework 就可以解決百分之八十以上。

    探討到 DDD 還是要先回歸祖師爺 Eric Evans 開宗明義說的,Project facing complex(or vjavascript:void(0)ery complex) problem domain. 這件事情,如果沒有這麼複雜的業務邏輯,拿DDD大刀最後都是傷到自己的腳囉。

    如果要取得 DDD 在實務中的平衡,這有一本是挺推薦的,我想您博覽群書,應該也已經看過,就獻醜了。

    Applying Domain-Driven Design and Patterns by Jimmy Nilsson

    題外話,大多搞 DDD 的,好像都沒認真讀過 Eric Evans 的這本書就是了。

    貼貼想法,感謝您海涵給個空間讓我可以說。 :D


    回覆刪除
  15. 您拋的不是磚,就算是磚,也是建材裡面的高級磚。:D

    我獻也不是玉,不過是借花獻佛而已啦 :D

    不過您這系列的文章,深入淺出,是要準備集結出書嗎? 應該滿有意思的,我看這些內容,頗有從 MANNING 幾本書中領略的融會,說真的挺不容易,質量兼具。

    其實沒有 DDD 的這個議題的時候,也有好多值得做法,隱隱約約的丟出許多可思考的議題,諸如 .Net Pet shop 的 Source Code,還有現在 Orchard 的 source code。

    裡面都蘊藏了許多好的(不好的),思考,看得出妥協的痕跡與思考的掙扎,只是這箇中滋味,也要經歷過才會體驗較深。

    還希望您有空多為文,我們才有更多好材料可以思考咀嚼。

    回覆刪除
  16. http://www.dotblogs.com.tw/clark/archive/2011/03/20/21972.aspx
    上列文章說明了,三層式架構中"層"的概念。
    理解這個簡單的概念,再來思考每個開發設計的技術,比較不會被誤導。

    就開發設計來說,DDD應該歸類在設計Tier裡面Layer的架構設計。
    分散式系統的設計則是多個Tier的組合,而在多個Tier中如何共用物件,在DDD中有專門定義了context章節來說明各種做法。


    但排除這些,
    學習DDD的重點,不是照著書中方式來設計系統,而是要學習他的精神,
    也就是「凝聚領域物件」的這個思想,
    Eric Evans的書中,有一半以上的內容,都是在圍繞這個想法並且提出實際的做法。
    也就是先前提出的說法:
    (這樣的設計精神,套用到三層式架構裡的BLL也是共用的。)
    ============================================================================
    其實設計的重點,是在領域層物件的定義上。
    領域層是以完成某個功能來做設計,這些功能職責散落在領域層的物件上。

    採用DDD方式也會設計出貧血的物件,這不是錯誤的。
    因為貧血物件有存在的價值,有些物件就只是單純的資料。

    但是如果整個系統設計下來,全部都是貧血物件,
    那就要檢查看看,
    實際完成系統功能的職責都落在哪邊,這些職責是不是應該有物件來歸納。
    透過這樣的反覆的設計,就能將系統應該完成的功能,封裝成完整的領域層。
    ============================================================================

    回覆刪除
  17. To Clark,
    非常感謝您 blog 中精闢的文章,但是在沒有 DDD 的年代裡 "凝聚領域物件" 這件事情說穿了就是 OO 裡面大家常講的物件職責以及高內聚這件事情。

    私以為,如果整個系統設計下來都是貧血物件,泰半是因為 Domain Logic 沒這麼複雜,更甚者,依照 Transaction Script 去做配上良好的物件設計(not domain modeling) 應該就可以了。

    大多的企業內部營運系統,你如您格上的請假系統案例,其實他是穩定(相對穩定)的case,不用上 DDD 其實也可以達成。

    我要說的是,大多數的案例,解釋 DDD 都很好,都很理想,搬到現實世界中又都不是那麼一回事。

    並非反對或是貶損 DDD 帶來的價值與值得奉為圭臬的觀念。畢竟我也曾是 DDD 的粉絲。

    題外話,我也很喜歡您的文,我訂閱您部落格也很久了。受益良多,感謝諸位 MVPs 的慷慨分享。

    回覆刪除
  18. 可能認錯人嚕,還沒寫過請假系統說。

    另外要說~
    DDD我用在工作上五年了,
    做出來的系統,同事跟老闆都覺得還不錯阿。XD

    回覆刪除
  19. to Clark,
    的確我認錯人啦 :P 因為好幾個 blog feed ,不好意思囉。

    我沒說您沒用過 DDD 在實務上,我也相信以您的功力一定是開發出很棒的系統。

    回覆刪除
  20. To itplayer: 多謝誇獎,您客氣了,很高興可以在這裡聽到實戰經驗的心得分享。
    寫這些文章,主要還是希望把層次化架構的概念以及設計和實作時碰到的問題做個整理。畢竟架構分層已經不算新議題了,相信大部分的開發人員都耳熟能詳。可是從概念的理解到實作之間的差距不小--要懂的東西不少,要處理的問題也很多,我相信不得其門而入者也大有人在。所以不惴淺薄,自說自話地寫了一堆,希望對這塊領域有興趣的人提供一點線索。

    再者,就真的是拋磚引玉,拓展自己的視野了。如果只是自己矇著頭實驗,總有盲點和不足的地方。所以像這樣的討論,我是非常期待也竭誠歡迎的。如果有話,請暢所欲言,我相信其他人也一定跟我一樣看得津津有味。(若要鞭我,麻煩下手輕點)

    倒沒有出書的打算,這部分就隨緣吧。
    Jimmy Nilsson 那本書我碰巧也有 :)
    Thanks!

    回覆刪除
  21. To Clark:
    原來您已經用 DDD 五年啦。希望將來能多看到你寫的技術文章,也歡迎常來坐 :)
    Thanks!

    回覆刪除
  22. To Huanlin Tsai、itplayer

    因為DDD是扭轉我,程式職涯一個很重要的思想。
    讓我從UI<->SQL的工人,轉型為靠頭腦吃飯的工程師。
    所以提到DDD會比較激動一點,造成兩位困擾的話,真是對不起。 Orz

    關於DDD在實際工作上的應用,點部落3月的聚會上,
    我有分享相關的經驗,並且寫成了文章做紀錄與分享。
    也希望對這塊領域有興趣的人提供一點線索。(這句話超棒~借來用了。:D)

    [Architecture Design] DDD經驗分享
    http://www.dotblogs.com.tw/clark/archive/2012/03/13/70707.aspx
    http://www.dotblogs.com.tw/clark/archive/2012/03/13/70728.aspx
    http://www.dotblogs.com.tw/clark/archive/2012/03/14/70732.aspx

    回覆刪除
  23. Clark 言重了..我唯一的困擾是錯過很多好文章了。多謝分享!

    回覆刪除
  24. 作者已經移除這則留言。

    回覆刪除

技術提供:Blogger.