.NET應用架構設計—用戶端的防腐層做用及設計

閱讀目錄:html

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

1.背景介紹

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

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

可是由此帶來的問題就是組合這些接口的顯示端的結構是否和這種變化是一致的,是否作好了這種變化帶來顯示端邏輯複雜的準備。後端

根據我本身的親身體會,我發現顯示端的架構設計不被重視,這裏的重視不是老闆是否重視,而是咱們開發人員沒有重視,固然這裏排除時間問題。我觀察過不少用戶接口項目架構,結構及其簡單,沒有封裝、沒有重用,看不到任何的設計原則。這樣就會致使這些代碼很難隨着業務的快速推進由服務接口帶來的衝擊,這裏還有一個最大的問題就是,做爲程序員的咱們是否有快速重構的意識,我很喜歡這條程序員職業素質。它可讓咱們敏捷的、快速的跟上由業務的發展帶來的項目結構的變化。api

迭代重構對項目有着微妙的做用,重構不可以過早也不可以過遲,要恰好在須要的時候重構。對於重構個人經驗就是,當你面對新功能寫起來比較蹩腳的時候時,這是一個重構信號,此時應該是最優的重構時間。重構不是專門的去準備時間,而是穿插在你寫代碼的過程當中,它是你編碼的一部分。因此我以爲TDD被人接受的理由也在於此。架構

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

顯示端的架構腐化我我的以爲有兩個問題致使,第一個,本來顯示端的結構在傳統系統架構中能夠工做的很好,可是如今的總體架構變了,因此須要及時做出調整。第二,顯示端的架構未能及時的重構,未能將顯示端結構進行進一步分離,將顯示邏輯獨立可測試。app

這樣隨着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層使用防腐層架構設計思想的一個簡單總結,目的只有一個,提供一個參考,謝謝你們。

 

相關文章
相關標籤/搜索