.NET應用架構設計—使用者端的防腐層作用及設計

王清培發表於2014-09-08

閱讀目錄:

  • 1.背景介紹
  • 2.SOA架構下的顯示端架構腐化
  • 3.有效使用防腐層來隔離碎片服務導致顯示端邏輯腐爛
  • 4.剝離服務呼叫的技術元件讓其依賴介面
  • 5.將服務的DTO與顯示端的ViewModel之間的轉換放入防腐層
    • 5.1.轉換邏輯過程化,直接寫在防腐層的方法中
    • 5.2.轉換邏輯物件化,建立起封裝、重用結構,防止進一步腐化
  • 6.防腐層的兩種依賴倒置設計方法
    • 6.1.事件驅動(防腐層監聽顯示邏輯事件)
    • 6.2.依賴注入介面
  • 7.總結

1.背景介紹

隨著現在的企業應用架構都在向著SOA方向轉變,目的就是將一個龐大的業務系統按照業務進行劃分,不管從公司的管理上、產品的開發上,這一系列流程來看,都是正確的。SOA確實帶來了解決現在大型企業級應用系統快速膨脹的解決辦法。

但是本文要說的是,我們都將目光轉向到了後端,也就是服務端,而將精力和時間都重點投在了後端服務的架構設計上,漸漸的忽視了顯示端的架構設計。然而顯示端的邏輯也越來越複雜,顯示端輕薄的架構其實已經浮現出難以應付後端服務介面快速膨脹的危險,服務介面都是按照指數級增加,基本上每一個新的業務需求都是提供新的介面,這沒有問題。按照服務的設計原則,服務介面就應該有著明確的作用,而不是按照程式碼的思維來考慮介面的設計。

但是由此帶來的問題就是組合這些介面的顯示端的結構是否和這種變化是一致的,是否做好了這種變化帶來顯示端邏輯複雜的準備。

根據我自己的親身體會,我發現顯示端的架構設計不被重視,這裡的重視不是老闆是否重視,而是我們開發人員沒有重視,當然這裡排除時間問題。我觀察過很多使用者介面專案架構,結構及其簡單,沒有封裝、沒有重用,看不到任何的設計原則。這樣就會導致這些程式碼很難隨著業務的快速推動由服務介面帶來的衝擊,這裡還有一個最大的問題就是,作為程式設計師的我們是否有快速重構的意識,我很喜歡這條程式設計師職業素質。它可以讓我們敏捷的、快速的跟上由業務的發展帶來的專案結構的變化。

迭代重構對專案有著微妙的作用,重構不能夠過早也不能夠過遲,要剛好在需要的時候重構。對於重構我的經驗就是,當你面對新功能寫起來比較蹩腳的時候時,這是一個重構訊號,此時應該是最優的重構時間。重構不是專門的去準備時間,而是穿插在你寫程式碼的過程中,它是你編碼的一部分。所以我覺得TDD被人接受的理由也在於此。

2.SOA架構下的顯示端架構腐化

顯示端的架構腐化我個人覺得有兩個問題導致,第一個,原本顯示端的結構在傳統系統架構中可以工作的很好,但是現在的整體架構變了,所以需要及時作出調整。第二,顯示端的架構未能及時的重構,未能將顯示端結構進行進一步分離,將顯示邏輯獨立可測試。

這樣隨著SOA介面的不斷增加,顯示端直接將呼叫服務的方法嵌入到顯示邏輯中,如,ASP.NET Mvc、ASP.NET Webapi的控制器中,包括兩個層面之間的DTO轉換。

按照DDD的上下文設計方法,在使用者顯示端也是可以有選擇的建立面向顯示的領域模型,此模型主要處理領域在即將到達服務端之後的前期處理。畢竟一個領域實體有著多個方面的職責,如果能在顯示端建立起輕量級的領域模型,對顯示邏輯的重構將大有好處,當然前提是你有著複雜的領域邏輯。(我之前的上一家公司(美國知名的電子商務平臺),他們的顯示端有著複雜的領域邏輯,就光一個顯示端就複雜的讓人吃驚,如果能在此基礎上引入領域模型顯示端上下文,將對複雜的邏輯處理很有好好處,當然這只是我未經驗證的猜測而已,僅供參考。)

對顯示端領域模型處理有興趣的可以參考本人寫的有關這方面的兩篇文章:

.NET應用架構設計—面向查詢的領域驅動設計實踐(調整傳統三層架構,外加維護型的業務開關)

.NET應用架構設計—面向查詢服務的引數化查詢設計(分解業務點,單獨配置各自的資料查詢契約)

原本乾淨的顯示邏輯多了很多無關的服務呼叫細節,還有很多轉換邏輯,判斷邏輯,而這些東西原本不屬於這個地方,讓他們放在合適的地方對顯示邏輯的重構、重用很有幫助。

如果不將其移出顯示邏輯中,那麼隨著服務介面的不斷增加和擴充套件,將直接導致你修改顯示邏輯程式碼,如果你的顯示邏輯程式碼是MVC、Webapi共用的邏輯,那麼情況就更加複雜了,最後顯示邏輯裡面將被ViewModel與Service Dto之間的轉換佔領,你很難找到有價值的邏輯了。

3.有效使用防腐層來隔離碎片服務導致顯示端邏輯腐爛

解決這些問題的方法就是引入防腐層,儘管防腐層的初衷是為了解決系統整合時的領域模型之間的轉換,但是我覺得現在的系統架構和整合有著很多相似之處,我們可以適當的借鑑這些好的設計方法來解決相似的問題。

引入防腐層之後,將原本不該出現在顯示邏輯中的程式碼全部搬到防腐層中來,在防腐層中建立起OO機制,讓這些OO物件能夠和顯示邏輯一起搭配使用。

圖1:

將使用者層分層三個子層,UiLayer,Show Logic Layer,Anticorrosive Layer,最後一個是服務的介面組,所有的服務介面呼叫均需要從防腐層走。

我們需要將Show Logic Layer中的服務呼叫,型別轉換程式碼遷移到Anticorrsoive Layer中,在這裡可以物件化轉換邏輯也可以不物件化,具體可以看下專案是否需要。如果業務確實比較複雜的時候,那麼我們為了封裝、重用就需要進行物件化。

4.剝離服務呼叫的技術元件讓其依賴介面

首先要做的就是將邏輯程式碼中的服務物件重構成面向介面的,然後讓其動態的依賴注入到邏輯型別中。在ASP.NETWEBAPI中,我們基本上將顯示邏輯都寫在這裡面,我也將使用此方式來演示本章例子,但是如果你的MVC專案和WEBAPI專案共用顯示邏輯就需要將其提出來形成獨立的專案(Show Logic Layer)。

 1 using OrderManager.Port.Models;
 2 using System.Collections.Generic;
 3 using System.Web.Http; 
 4 
 5 namespace OrderManager.Port.Controllers
 6 {
 7     public class OrderController : ApiController
 8     {
 9         [HttpGet]
10         public OrderViewModel GetOrderById(long oId)
11         {
12             OrderService.Contract.OrderServiceClient client = new OrderService.Contract.OrderServiceClient();
13             var order = client.GetOrderByOid(oId); 
14 
15             if (order == null) return null; 
16 
17             return AutoMapper.Mapper.DynamicMap<OrderViewModel>(order);
18         }
19     }
20 } 

這是一段很簡單的呼叫Order服務的程式碼,首先需要例項化一個服務契約中包含的客戶端代理,然後通過代理呼叫遠端服務方法GetOrderByOid(long oId)。執行一個簡單的判斷,最後輸出OrderViewModel。

如果所有的邏輯都這麼簡單我想就不需要什麼防腐層了,像這種型別的顯示程式碼是極其簡單的,我這裡的目的不是為了顯示多麼的複雜的程式碼如何寫,而是將服務呼叫呼叫的程式碼重構層介面,然後注入進OrderController例項中。目的就是為了能夠在後續的迭代重構中對該控制器進行單元測試,這可能有點麻煩,但是為了長久的利益還是需要的。

 1 using OrderManager.Port.Component;
 2 using OrderManager.Port.Models;
 3 using System.Collections.Generic;
 4 using System.Web.Http; 
 5 
 6 namespace OrderManager.Port.Controllers
 7 {
 8     public class OrderController : ApiController
 9     {
10         private readonly IOrderServiceClient orderServiceClient;
11         public OrderController(IOrderServiceClient orderServiceClient)
12         {
13             this.orderServiceClient = orderServiceClient;
14         } 
15 
16         [HttpGet]
17         public OrderViewModel GetOrderById(long oId)
18         {
19             var order = orderServiceClient.GetOrderByOid(oId); 
20 
21             if (order == null) return null; 
22 
23             return AutoMapper.Mapper.DynamicMap<OrderViewModel>(order);
24         }
25     }
26 } 

為了能在執行時動態的注入到控制器中,你需要做一些基礎工作,擴充套件MVC控制器的初始化程式碼。這樣我們就可以對OrderController進行完整的單元測試。

剛才說了,如果顯示邏輯都是這樣的及其簡單,那麼一切都沒有問題了,真實的顯示邏輯非常的複雜而且多變,並不是所有的型別轉換都能使用Automapper這一類動態對映工具解決,有些型別之間的轉換還有邏輯在裡面。GetOrderById(long oId)方法是為了演示此處的重構服務呼叫元件用的。

大部分情況下我們是需要組合多個服務呼叫的,將其多個結果組合起來返回給前端的,這裡的OrderViewModel物件裡面的Items屬性型別OrderItem型別中包含了一個Product型別屬性,在正常情況下我們只需要獲取訂單的條目就行了,但是有些時候確實需要將條目中具體的產品資訊也要返回給前臺進行部分資訊的展現。

 1 using System.Collections.Generic; 
 2 
 3 namespace OrderManager.Port.Models
 4 {
 5     public class OrderViewModel
 6     {
 7         public long OId { get; set; } 
 8 
 9         public string OName { get; set; } 
10 
11         public string Address { get; set; } 
12 
13         public List<OrderItem> Items { get; set; }
14     }
15 } 

在OrderViewModel中的Items屬性是一個List<OrderItem>集合,我們再看OrderItem屬性。

 1 using System.Collections.Generic; 
 2 
 3 namespace OrderManager.Port.Models
 4 {
 5     public class OrderItem
 6     {
 7         public long OitemId { get; set; } 
 8 
 9         public long Pid { get; set; } 
10 
11         public float Price { get; set; } 
12 
13         public int Number { get; set; } 
14 
15         public Product Product { get; set; }
16     }
17 } 

它裡面包含了一個Product例項,有些時候需要將該屬性賦上值。

 1 namespace OrderManager.Port.Models
 2 {
 3     public class Product
 4     {
 5         public long Pid { get; set; } 
 6 
 7         public string PName { get; set; } 
 8 
 9         public long PGroup { get; set; } 
10 
11         public string Production { get; set; }
12     }
13 } 

產品型別中的一些資訊主要是用來作為訂單條目展現時能夠更加的人性化一點,你只給一個產品ID,不能夠讓使用者知道是哪個具體的商品。

我們接著看一個隨著業務變化帶來的程式碼急速膨脹的例子,該例子中我們需要根據OrderItem中的Pid獲取Product完整資訊。

 1 using OrderManager.Port.Component;
 2 using OrderManager.Port.Models;
 3 using System.Collections.Generic;
 4 using System.Web.Http;
 5 using System.Linq; 
 6 
 7 namespace OrderManager.Port.Controllers
 8 {
 9     public class OrderController : ApiController
10     {
11         private readonly IOrderServiceClient orderServiceClient; 
12 
13         private readonly IProductServiceClient productServiceClient;
14         public OrderController(IOrderServiceClient orderServiceClient, IProductServiceClient productServiceClient)
15         {
16             this.orderServiceClient = orderServiceClient;
17             this.productServiceClient = productServiceClient;
18         } 
19 
20         [HttpGet]
21         public OrderViewModel GetOrderById(long oId)
22         {
23             var order = orderServiceClient.GetOrderByOid(oId); 
24 
25             if (order == null && order.Items != null && order.Items.Count > 0) return null; 
26 
27             var result = new OrderViewModel()
28             {
29                 OId = order.OId,
30                 Address = order.Address,
31                 OName = order.OName,
32                 Items = new System.Collections.Generic.List<OrderItem>()
33             }; 
34 
35             if (order.Items.Count == 1)
36             {
37                 var product = productServiceClient.GetProductByPid(order.Items[0].Pid);//呼叫單個獲取商品介面
38                 if (product != null)
39                 {
40                     result.Items.Add(ConvertOrderItem(order.Items[0], product));
41                 }
42             }
43             else
44             {
45                 List<long> pids = (from item in order.Items select item.Pid).ToList(); 
46 
47                 var products = productServiceClient.GetProductsByIds(pids);//呼叫批量獲取商品介面
48                 if (products != null)
49                 {
50                     result.Items = ConvertOrderItems(products, order.Items);//批量轉換OrderItem型別
51                 } 
52 
53             } 
54 
55             return result;
56         } 
57 
58         private static OrderItem ConvertOrderItem(OrderService.OrderItem orderItem, ProductService.Contract.Product product)
59         {
60             if (product == null) return null; 
61 
62             return new OrderItem()
63             {
64                 Number = orderItem.Number,
65                 OitemId = orderItem.OitemId,
66                 Pid = orderItem.Pid,
67                 Price = orderItem.Price, 
68 
69                 Product = new Product()
70                 {
71                     Pid = product.Pid,
72                     PName = product.PName,
73                     PGroup = product.PGroup,
74                     Production = product.Production
75                 }
76             };
77         } 
78 
79         private static List<OrderItem> ConvertOrderItems(List<ProductService.Contract.Product> products, List<OrderService.OrderItem> orderItems)
80         {
81             var result = new List<OrderItem>(); 
82 
83             orderItems.ForEach(item =>
84             {
85                 var orderItem = ConvertOrderItem(item, products.Where(p => p.Pid == item.Pid).FirstOrDefault());
86                 if (orderItem != null)
87                     result.Add(orderItem);
88             }); 
89 
90             return result;
91         }
92     }
93 } 

我的第一感覺就是,顯示邏輯已經基本上都是型別轉換程式碼,而且這裡我沒有新增任何一個有關顯示的邏輯,在這樣的情況下都讓程式碼急速膨脹了,可想而知,如果再在這些程式碼中加入顯示邏輯,我們基本上很難在後期維護這些顯示邏輯,而這些顯示邏輯才是這個類的真正職責。

由此帶來的問題就是重要的邏輯淹沒在這些轉換程式碼中,所以我們急需一個能夠容納這些轉換程式碼的位置,也就是防腐層,在防腐層中我們專門來處理這些轉換邏輯,當然我這裡的例子是比較簡單的,只包含了查詢,真正的防腐層是很複雜的,它裡面要處理的東西不亞於其他層面的邏輯處理。我們這裡僅僅是在轉換一些DTO物件而不是複雜的DomainModel物件。

5.將服務的DTO與顯示端的ViewModel之間的轉換放入防腐層

我們需要一個防腐層來處理這些轉換程式碼,包括對後端服務的呼叫邏輯,將這部分程式碼移入防腐物件中之後會對我們後面重構很有幫助。

 1 namespace OrderManager.Anticorrsive
 2 {
 3     using OrderManager.Port.Component;
 4     using OrderManager.Port.Models;
 5     using System.Collections.Generic;
 6     using System.Linq; 
 7 
 8     /// <summary>
 9     /// OrderViewModel 防腐物件
10     /// </summary>
11     public class OrderAnticorrsive : AnticorrsiveBase<OrderViewModel>, IOrderAnticorrsive
12     {
13         private readonly IOrderServiceClient orderServiceClient; 
14 
15         private readonly IProductServiceClient productServiceClient; 
16 
17         public OrderAnticorrsive(IOrderServiceClient orderServiceClient, IProductServiceClient productServiceClient)
18         {
19             this.orderServiceClient = orderServiceClient;
20             this.productServiceClient = productServiceClient;
21         } 
22 
23         public OrderViewModel GetOrderViewModel(long oId)
24         {
25             var order = orderServiceClient.GetOrderByOid(oId); 
26 
27             if (order == null && order.Items != null && order.Items.Count > 0) return null; 
28 
29             var result = new OrderViewModel()
30             {
31                 OId = order.OId,
32                 Address = order.Address,
33                 OName = order.OName,
34                 Items = new System.Collections.Generic.List<OrderItem>()
35             }; 
36 
37             if (order.Items.Count == 1)
38             {
39                 var product = productServiceClient.GetProductByPid(order.Items[0].Pid);//呼叫單個獲取商品介面
40                 if (product != null)
41                 {
42                     result.Items.Add(ConvertOrderItem(order.Items[0], product));
43                 }
44             }
45             else
46             {
47                 List<long> pids = (from item in order.Items select item.Pid).ToList(); 
48 
49                 var products = productServiceClient.GetProductsByIds(pids);//呼叫批量獲取商品介面
50                 if (products != null)
51                 {
52                     result.Items = ConvertOrderItems(products, order.Items);//批量轉換OrderItem型別
53                 } 
54 
55             } 
56 
57             return result;
58         } 
59 
60         private static OrderItem ConvertOrderItem(OrderService.OrderItem orderItem, ProductService.Contract.Product product)
61         {
62             if (product == null) return null; 
63 
64             return new OrderItem()
65             {
66                 Number = orderItem.Number,
67                 OitemId = orderItem.OitemId,
68                 Pid = orderItem.Pid,
69                 Price = orderItem.Price, 
70 
71                 Product = new Product()
72                 {
73                     Pid = product.Pid,
74                     PName = product.PName,
75                     PGroup = product.PGroup,
76                     Production = product.Production
77                 }
78             };
79         } 
80 
81         private static List<OrderItem> ConvertOrderItems(List<ProductService.Contract.Product> products, List<OrderService.OrderItem> orderItems)
82         {
83             var result = new List<OrderItem>(); 
84 
85             orderItems.ForEach(item =>
86             {
87                 var orderItem = ConvertOrderItem(item, products.Where(p => p.Pid == item.Pid).FirstOrDefault());
88                 if (orderItem != null)
89                     result.Add(orderItem);
90             }); 
91 
92             return result;
93         }
94     }
95 }

如果你覺得有必要可以將IOrderServiceClient、IProductServiceClient 兩個介面放入AnticorrsiveBase<OrderViewModel>基類中。

5.1.轉換邏輯過程化,直接寫在防腐層的方法中

對於防腐層的設計,其實如果你的轉換程式碼不多,業務也比較簡單時,我建議直接寫成過程式的程式碼比較簡單點。將一些可以重用的程式碼直接使用靜態的擴充套件方法來使用也是比較簡單方便的,最大問題就是不利於後期的持續重構,我們無法預知未來的業務變化,但是我們可以使用重構來解決。

5.2.轉換邏輯物件化,建立起封裝、重用結構,防止進一步腐化

相對應的,可以將轉換程式碼進行物件化,形成防腐物件,每一個物件專門用來處理某一個業務點的資料獲取和轉換邏輯,如果你有資料傳送邏輯那麼將在防腐物件中大大獲益,物件化後就可以直接訂閱相關控制器的依賴注入事件,如果你是過程式的程式碼想完成動態的轉換、傳送、獲取會比較不方便。

6.防腐層的兩種依賴倒置設計方法

我們接著看一下如何讓防腐物件無干擾的進行自動化的服務呼叫和傳送,我們希望防腐物件完全透明的在執行著防腐的職責,並不希望它會給我們實現上帶來多大的開銷。

6.1.事件驅動(防腐層監聽顯示邏輯事件)

我們可以使用事件來實現觀察者模式,讓防腐層物件監聽某個事件,當事件觸發時,自動的處理某個動作,而不是要顯示的手動呼叫。

1 namespace OrderManager.Anticorrsive
2 {
3     public interface IOrderAnticorrsive
4     {
5         void SetController(OrderController orderController); 
6 
7         OrderViewModel GetOrderViewModel(long oId);
8     }
9 }

Order防腐物件介面,裡面包含了一個void SetController(OrderController orderController); 重要方法,該方法是用來讓防腐物件自動註冊事件用的。

 1 public class OrderController : ApiController
 2 {
 3     private IOrderAnticorrsive orderAnticorrsive; 
 4 
 5     public OrderController(IOrderAnticorrsive orderAnticorrsive)
 6     {
 7         this.orderAnticorrsive = orderAnticorrsive; 
 8 
 9         this.orderAnticorrsive.SetController(this);//設定控制器到防腐物件中
10     } 
11 
12     public event EventHandler<OrderViewModel> SubmitOrderEvent; 
13 
14     [HttpGet]
15     public void SubmitOrder(OrderViewModel order)
16     {
17         this.SubmitOrderEvent(this, order);
18     }
19 }

在控制器中,每當我們發生某個業務動作時只管觸發事件即可,當然主要是以傳送資料為主,查詢可以直接呼叫物件的方法。因為防腐物件起到一個與後臺服務整合的橋樑,當提交訂單時可能需要同時呼叫很多個後臺服務方法,用事件處理會比較方便。

 1     /// <summary>
 2     /// OrderViewModel 防腐物件
 3     /// </summary>
 4     public class OrderAnticorrsive : AnticorrsiveBase<OrderViewModel>, IOrderAnticorrsive
 5     {
 6         public void SetController(OrderController orderController)
 7         {
 8             orderController.SubmitOrderEvent += orderController_SubmitOrderEvent;
 9         } 
10 
11         private void orderController_SubmitOrderEvent(object sender, OrderViewModel e)
12         {
13             //提交訂單的邏輯
14         }
15     }
16 }

6.2.依賴注入介面

依賴注入介面是完全為了將控制器與防腐物件之間隔離用的,上述程式碼中我是將介面定義在了防腐物件層中,那麼也就是說控制器物件所在的專案需要引用防腐層,在處理事件和方法同時使用時會顯得有點不倫不類的,既有介面又有方法,其實這就是一種平衡吧,越純粹的東西越要付出一些代價。

如果我們定義純粹的依賴注入介面讓防腐物件去實現,那麼在觸發事件時就需要專門的方法來執行事件的觸發,因為不在本類中的事件是沒辦法觸發的。

7.總結

本篇文章是我對在UI層使用防腐層架構設計思想的一個簡單總結,目的只有一個,提供一個參考,謝謝大家。

 

相關文章