[.NET]重構之路系列v12 –用 Decorator Pattern 讓職責分離與組合

[.NET]重構之路系列v12 –用 Decorator Pattern 讓職責分離與組合

前言

在 legacy 系統中,總是可以看到職責混淆不清,疊床架屋的物件設計。或是為了重用程式碼,把繼承鏈搞的落落長,導致因為繼承關係而產生強耦合的情況,無法因應需求來變更設計。

這篇文章要介紹的,就是將職責混和的情況分開,讓每個職責的程式碼只有一份,因應需求,透過工廠,來將物件組合起來,達到需求所需要的功能。

 

原本的程式碼

之前在準備物件導向 training 的 homework 題目時,剛好在噗浪與小朱討論到什麼樣的題目適合讓學員練習。最後,決定設計一個「幫DAO穿衣服」的題目。

下面這段程式碼,就是重構前的程式碼,題目的需求如同 class 上的註解所描述,接著我們針對重構的需求,以及可能面臨到的需求異動,來進行設計。

    /// <summary>
    /// 原本需求:
    /// Product資料異動時,需記錄log,查詢不用
    /// 對Product進行CRUD時,需檢查權限
    /// 
    /// 重構需求:
    /// 1.
    /// 將權限驗證與log,從ProductDao獨立出來
    /// 讓ProductDao只處理CRUD的需求
    /// 
    /// 需求異動:
    /// 1.
    /// 在測試環境時,不檢查權限與不記錄log。
    /// 檢查測試環境條件,請使用Context.IsTestEnvironment
    ///     
    /// </summary>
    public class ProductDao
    {
        private Log _log = new Log();

        public string Message { get; private set; }

        public void Delete(string id)
        {
            //驗證使用者是否有權限,對此商品ID進行刪除動作
            ProductAuthorizationService auth = new ProductAuthorizationService() { ProductId = id, AccessType = AccessType.Delete };
            var isValid = auth.Check();

            if (isValid)
            {
                //模擬實際刪除資料                
                Console.WriteLine("刪除商品ID為{0}的資料", id);
                this.Message = "刪除成功";
            }
            else
            {
                this.Message = "權限不符,刪除失敗";
            }

            this._log.Write(this.Message);
        }


        public void Create(Entity.Product product)
        {
            //驗證使用者是否有權限,對此商品ID進行新增動作
            ProductAuthorizationService auth = new ProductAuthorizationService() { Supplier = product.Supplier, AccessType = AccessType.Insert };
            var isValid = auth.Check();

            if (isValid)
            {
                //模擬實際新增資料
                Console.WriteLine("新增商品ID為{0}, Name為{1}, Supplier為{2} 的資料", product.Id, product.Name, product.Supplier);
                this.Message = "新增成功";
            }
            else
            {
                this.Message = "權限不符,新增失敗";
            }

            this._log.Write(this.Message);
        }

        public Entity.Product Query(string id)
        {
            ProductAuthorizationService auth = new ProductAuthorizationService() { ProductId = id, AccessType = AccessType.Query };

            var isValid = auth.Check();

            if (isValid)
            {
                //模擬查詢資料
                var result = new Product() { Id = id, Name = "測試Name", Supplier = "測試Supplier" };

                this.Message = "查詢成功";
                return result;
            }
            else
            {
                this.Message = "權限不符,查詢失敗";
                return null;
            }

        }
    }

 

原本的Class Diagram

image

從 class diagram 可以看到,每一個 class 的關係,都是直接相依。而且 ProductDao 的職責除了原本的 CRUD 以外,還與 Log 以及權限驗證模組相耦合。

 

重構後的Class Diagram

image

Class diagram 重點(用紅色箭頭標記的就是重要的關係):

  1. 相依於介面。
  2. 實際進行 CRUD 的動作,仍在 ProductDao 中,但就只有 CRUD 的動作。
  3. ProductDao, LogProductDao, AuthrizeProductDao 三個 class 都實作 IProductDao,也就代表著,三個 class 對外都擁有 IProductDao 的行為。
  4. LogProductDao 以及 AuthrizeProductDao,與 IProductDao 除了實作的關係以外,還有聚合的關係。簡單地說,這兩個物件身上,還放著一個 IProductDao 的物件。

圖上忽略的兩個部分:

  1. 工廠,仍需要工廠類別,只是不在 class diagram 上標記出相關關係,以簡化與關注重點。
  2. Program 直接相依於 Product,以介面導向來說,中間是要隔一層介面。但為了關注在作業重點上,而將其他部分簡化。

重構的目的

  1. 在 Product Entity 中,Product應該只會看的到一個 IProductDao,而且只關注於 IProductDao 介面上所提供的行為。Product Entity 的行為會很乾淨,與對外物件互動的關係也會很乾淨。
  2. 讓 ProductDao只負責處理 CRUD,LogDao 則只負責加上 log,AuthDao 則只關注在權限驗證。將物件職責分乾淨,同樣的程式碼不會散落一地與重複出現。
  3. 生成 IProductDao 的動作,以及該如何根據條件來決定組合,則交給 factory 來決定。這樣一來脆弱的 if/else 或變動的條件,就只會存在於 factory,且這樣的變動只會影響物件如何組合,而不會影響到物件內部的職責邏輯。
  4. 讓物件的相依性降到最低,內聚力拉到最高,就會有彈性,且影響範圍最低,發生需求異動或問題時,可以快速地找到對應物件進行修改,且外部對這樣的修改是無感的。

重構的設計

Product Entity 的設計,只需相依於 IProductDao,也只關注於和 IProductDao 介面的互動,程式碼如下圖:

image

ProductDao 則只保留、只關注 CRUD,而沒有 log 或驗證權限的職責與邏輯,程式碼如下圖:

image

LogProductDao 則只關注在,當 ProductDao 的 Create 方法與 Delete 方法被呼叫時,要記錄 log,Query則不需記錄 log。Log 的實作,則相依於 ILog 介面,而不與 Log 的實作細節相依。

需要注意的是,LogProductDao 除了實作 IProductDao 介面以外,建構式還需傳入 IProductDao 來進行實際的 CRUD 動作。簡單地說,LogProductDao 就只是件 IProductDao 的衣服。

程式碼如下圖:

image

AuthrorizeProductDao 的設計,則與 LogDao 雷同,只關注在權限的驗證,相依於權限驗證的介面,而不直接相依於實際驗證的細節。在驗證權限的職責中,也不關注實際 IProductDao 中資料存取的 CRUD 動作。

程式碼請見下圖(篇幅限制,Query 方法就先略過):

image

到這邊,基本上物件本身的職責就設計完成了。

ProductDao 只負責實際的資料存取動作,LogProductDao 只負責在 Create 方法與 Delete 方法之後,加上 log 的記錄。而 AuthorizeProductDao ,則只負責在呼叫 Create, Delete 與 Query 方法之前,檢查權限是否合法。

接下來的重頭戲,就在工廠要怎麼決定物件的組合。

 

Factory 的職責,主要就是生成物件。而在這作業的重點,就在於如何產生 IProductDao 的 instance。

在延伸需求中提到,當為測試環境時,不檢查權限也不記錄 log。在這邊,判斷測試環境的條件為一個 Context 的靜態 property : IsTestEnvironment。

程式碼如下圖:

image

在上面的程式碼,就是這次作業的精髓:幫 Dao 穿衣服。

在這邊,我們幫 ProductDao 穿上了 LogProductDao 的衣服。接著再穿上了 AuthorizeProductDao 的衣服。然後 assign 給 Product Entity。

Product Entity 實際呼叫 IProductDao 時,是呼叫了穿著多件衣服的 ProductDao ,如下圖所示:

image

結論

透過不同的衣服,來定義出實作同一個介面,但擁有不同的職責,且物件可重用、可互動、可組合。物件組合方式的決定,則在生成物件的階段,由工廠來決定傳回什麼樣的物件組合。

這樣的設計,碰到下面的幾個需求,都只需要修改「該職責所屬之物件」,且外部物件無感,無感的原因是因為低耦合,無感的目的就是不會互相影響。

  1. 要加上 log 或權限驗證的條件改變,只需要改變 Factory。
  2. 實際的 CRUD 資料存取動作改變,只需要改變 ProductDao。
  3. 針對不同方法,要加上、改變或取消 log 的情況,只要改變 LogProductDao。
  4. 針對不同方法,要加上、改變或取消權限驗證的情況,只要改變 AuthrorizeProductDao。
  5. Log 的實作細節要抽換,只需新增一個實作 ILog 介面的類別。
  6. 權限驗證的實作細節要抽換,只需新增一個實作 IProductAuthorizationService 介面的類別。

單一職責原則、開放封閉原則、依賴反轉原則,組成了這一次重構的基礎。

透過介面的抽象化,一般化關係搭配聚合關係,來完成最最基本的 Decorator 模式,而這樣的方式,也是 AOP 設計的基底。AOP 可參考之前的文章介紹:[Spring.Net]Aop introduction–以performance log為例

下面這張圖則是之前介紹 AOP 概念時,所描繪的說明圖:

Aop implement Dynamic Proxy Pattern

備註

  1. 最後重構後物件組成的順序,如果要按照原本需求的話,應該是new LogProductDao(new AuthorizeProductDao(new ProductDao)); 的順序才對。感謝 Kevin Chang 的提醒。

Sample Code

題目:OO session 4 homework.zip

重構後結果:Session4Sample.zip


blog 與課程更新內容,請前往新站位置:http://tdd.best/