[Architecture] Lazy Decoration

[Architecture] : Lazy Decoration

動機 :

在設計物件導向應用程式架構的時候,
物件會包含相關的企業邏輯,而不是單純的資料物件。
但是當企業邏輯需要取得其他物件一起運算,如何「取得」是一件很複雜的事情。

 

例如說:
在系統內有一個「查詢客戶訂單總金額」的企業邏輯,需要從系統取出客戶的所有訂單做金額加總。
這個企業邏輯實作上可以分配到不同的物件,這邊我們先定義這個企業邏輯是客戶物件的職責。
並用下列的程式碼,實作這個企業邏輯,
這樣的範例是可以正常的工作。

 

 

但是換個場景會發現,在只是要編輯客戶電話的時候,也需要取得訂單查詢介面。
當系統越來越龐大,企業邏輯越來越多時,這個範例架構就會顯得是個災難。
而且再細看的話會發現訂單有參考到客戶,這個範例有循環相依的問題。

 


namespace ConsoleApplication001
{
    public class Customer
    {
        public Guid Id { get; private set; }

        public string Name { get; set; }

        private readonly IOrderRepository _orderRepository = null;

        
        public Customer(Guid id, IOrderRepository orderRepository)
        {
            this.Id = id;
            this.Name = string.Empty;
            _orderRepository = orderRepository;
        }


        public int GetTotal()
        {
            int total = 0;
            foreach (Order order in _orderRepository.GetListByCustomer(this))
            {
                total += order.Price;
            }
            return total;
        }
    }

    public class Order
    {
        public Guid Id { get; private set; }

        public Customer Customer { get; private set; }

        public int Price { get; set; }


        public Order(Guid id, Customer customer)
        {
            this.Id = id;
            this.Customer = customer;
            this.Price = 0;
        }
    }

    public interface IOrderRepository
    {
        IEnumerable<Order> GetListByCustomer(Customer customer);
    }
}

 

將系統重寫成下列的程式碼,改由運作時將訂單查詢介面注入。
這樣的範例也是可以正常的工作,但是依然沒有解決循環相依的問題。

 


namespace ConsoleApplication002
{
    public class Customer
    {
        public Guid Id { get; private set; }

        public string Name { get; set; }


        public Customer(Guid id)
        {
            this.Id = id;
            this.Name = string.Empty;
        }


        public int GetTotal(IOrderRepository orderRepository)
        {
            int total = 0;
            foreach (Order order in orderRepository.GetListByCustomer(this))
            {
                total += order.Price;
            }
            return total;
        }
    }

    public class Order
    {
        public Guid Id { get; private set; }

        public Customer Customer { get; private set; }

        public int Price { get; set; }


        public Order(Guid id, Customer customer)
        {
            this.Id = id;
            this.Customer = customer;
            this.Price = 0;
        }
    }

    public interface IOrderRepository
    {
        IEnumerable<Order> GetListByCustomer(Customer customer);
    }
}

 

本文介紹一個『Lazy Decoration模式』。
定義物件的職責跟規則,將物件與物件之間的相依性做切割。
用來解決上列描述的問題。

 

 

結構 :

 

下圖是這個模式的示意圖。
可以看到除了系統原本就有的客戶、訂單、訂單查詢介面之外,多了兩個客戶實體、客戶實體工廠物件。

 

訂單到客戶之間的相依,透過客戶實體、客戶實體工廠做了相依性切割。
並且將「查詢客戶訂單總金額」的企業邏輯,改分派到(客戶實體)上。
需要做「查詢客戶訂單總金額」時,再建立(客戶實體)來查詢。
而(客戶實體)因為是繼承自(客戶)物件,在後續的應用,也可以直接將它當作(客戶)來用。

 

image

 

實作 :

 

文字寫起來很複雜,其實看程式碼很簡單。
首先定義基本的(客戶)、(訂單)、(訂單查詢介面)這三個物件。
要特別注意的是(客戶)物件,它除了基本的建構函式之外,還包含了一個將自己當作參數的建構函式。
這讓繼承的物件,不用關注屬性增加、屬性更名、屬性值初始化...等等工作。


namespace ConsoleApplication003
{
    public class Customer
    {
        public Guid Id { get; private set; }

        public string Name { get; set; }


        public Customer(Guid id)
        {
            this.Id = id;
            this.Name = string.Empty;
        }

        public Customer(Customer item)
        {
            this.Id = item.Id;
            this.Name = item.Name;
        }
    }

    public class Order
    {
        public Guid Id { get; private set; }

        public Customer Customer { get; private set; }       

        public int Price { get; set; }        


        public Order(Guid id, Customer customer)
        {
            this.Id = id;
            this.Customer = customer;            
            this.Price = 0;
        }
    }

    public interface IOrderRepository
    {
        IEnumerable<Order> GetListByCustomer(Customer customer);
    }
}

 

再來看看(客戶實體)物件,
它繼承了(客戶)物件,並且實作了「查詢客戶訂單總金額」這個企業邏輯。

 


namespace ConsoleApplication003
{
    public class CustomerEntity : Customer
    {
        private readonly IOrderRepository _orderRepository = null;


        public CustomerEntity(Customer item, IOrderRepository orderRepository)
            : base(item)
        {
            _orderRepository = orderRepository;
        }

        public int GetTotal()
        {
            int total = 0;
            foreach (Order order in _orderRepository.GetListByCustomer(this))
            {
                total += order.Price;
            }
            return total;
        }
    }
}

 

最後是(客戶實體工廠),
它很簡單的只是在建立(客戶實體)時,將(訂單查詢介面)物件做注入的動作。

 


namespace ConsoleApplication003
{
    public class CustomerEntityFactory
    {
        private readonly IOrderRepository _orderRepository = null;


        public CustomerEntityFactory(IOrderRepository orderRepository)
        {
            _orderRepository = orderRepository;
        }


        public CustomerEntity Create(Customer item)
        {
            return new CustomerEntity(item, _orderRepository);
        }
    }
}

 

在這些物件整個建立完畢之後,
當我們要做客戶資料的新增、修改、刪除、查詢,直接將(客戶)物件進出 Data Access Layer(DAL)。

 


namespace ConsoleApplication003
{
    class Test001
    {
        static void MainXXX(string[] args)
        {
            ICustomerRepository customerRepository = null; // 使用例如Spring.Net、Provider Pattern來反射生成。 

            foreach (Customer customer in customerRepository.GetAll())
            {
                Console.WriteLine(customer.Name);
            }
        }
    }        
}

namespace ConsoleApplication003
{
    public interface ICustomerRepository // Customer的DAL介面
    {
        Customer[] GetAll();

        Customer GetById(Guid id);
    }
}

 

當要查詢某個客戶的訂單總金額時,建立(客戶實體)就可以做查詢。

 


namespace ConsoleApplication003
{
    class Test002
    {
        static void MainXXX(string[] args)
        {
            ICustomerRepository customerRepository = null; // 使用例如Spring.Net、Provider Pattern來反射生成。 
            IOrderRepository orderRepository = null;// 使用例如Spring.Net、Provider Pattern來反射生成。             
            CustomerEntityFactory customerEntityFactory = new CustomerEntityFactory(orderRepository);

            Customer customer = customerRepository.GetById(Guid.Parse("xxxxx"));
            CustomerEntity customerEntity = customerEntityFactory.Create(customer);

            Console.WriteLine(customerEntity.GetTotal());
        }
    }
}

namespace ConsoleApplication003
{
    public interface ICustomerRepository // Customer的DAL介面
    {
        Customer[] GetAll();

        Customer GetById(Guid id);
    }
}

後記 :

這個模式除了範例示範的企業邏輯分派為物件方法之外,也可以延伸成為物件屬性、物件事件等等的功能。
在實作的時候這個模式,也能將不同的企業邏輯做分類。例如 : CustomerQueryEntity、CustomerVerifyEntity。

 

最後一提的是,這個模式是從 [Application Architecture] : Lazy Boundary 模式 所重整提取出來。
當我們,
將在開發軟體專案的時候,遇到的各種不同功能面物件,歸類並取一個好記的名字。
反覆重整功能面物件跟名詞,最終就會產生屬於自己的模式。 :D

 

期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。