[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模式』。
定義物件的職責跟規則,將物件與物件之間的相依性做切割。
用來解決上列描述的問題。
結構 :
下圖是這個模式的示意圖。
可以看到除了系統原本就有的客戶、訂單、訂單查詢介面之外,多了兩個客戶實體、客戶實體工廠物件。
訂單到客戶之間的相依,透過客戶實體、客戶實體工廠做了相依性切割。
並且將「查詢客戶訂單總金額」的企業邏輯,改分派到(客戶實體)上。
需要做「查詢客戶訂單總金額」時,再建立(客戶實體)來查詢。
而(客戶實體)因為是繼承自(客戶)物件,在後續的應用,也可以直接將它當作(客戶)來用。
實作 :
文字寫起來很複雜,其實看程式碼很簡單。
首先定義基本的(客戶)、(訂單)、(訂單查詢介面)這三個物件。
要特別注意的是(客戶)物件,它除了基本的建構函式之外,還包含了一個將自己當作參數的建構函式。
這讓繼承的物件,不用關注屬性增加、屬性更名、屬性值初始化...等等工作。
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
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。