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


閱讀目錄:

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM