阅读目录:html
随着如今的企业应用架构都在向着SOA方向转变,目的就是将一个庞大的业务系统按照业务进行划分,无论从公司的管理上、产品的开发上,这一系列流程来看,都是正确的。SOA确实带来了解决如今大型企业级应用系统快速膨胀的解决办法。前端
可是本文要说的是,咱们都将目光转向到了后端,也就是服务端,而将精力和时间都重点投在了后端服务的架构设计上,渐渐的忽视了显示端的架构设计。然而显示端的逻辑也愈来愈复杂,显示端轻薄的架构其实已经浮现出难以应付后端服务接口快速膨胀的危险,服务接口都是按照指数级增长,基本上每个新的业务需求都是提供新的接口,这没有问题。按照服务的设计原则,服务接口就应该有着明确的做用,而不是按照代码的思惟来考虑接口的设计。程序员
可是由此带来的问题就是组合这些接口的显示端的结构是否和这种变化是一致的,是否作好了这种变化带来显示端逻辑复杂的准备。后端
根据我本身的亲身体会,我发现显示端的架构设计不被重视,这里的重视不是老板是否重视,而是咱们开发人员没有重视,固然这里排除时间问题。我观察过不少用户接口项目架构,结构及其简单,没有封装、没有重用,看不到任何的设计原则。这样就会致使这些代码很难随着业务的快速推进由服务接口带来的冲击,这里还有一个最大的问题就是,做为程序员的咱们是否有快速重构的意识,我很喜欢这条程序员职业素质。它可让咱们敏捷的、快速的跟上由业务的发展带来的项目结构的变化。api
迭代重构对项目有着微妙的做用,重构不可以过早也不可以过迟,要恰好在须要的时候重构。对于重构个人经验就是,当你面对新功能写起来比较蹩脚的时候时,这是一个重构信号,此时应该是最优的重构时间。重构不是专门的去准备时间,而是穿插在你写代码的过程当中,它是你编码的一部分。因此我以为TDD被人接受的理由也在于此。架构
显示端的架构腐化我我的以为有两个问题致使,第一个,本来显示端的结构在传统系统架构中能够工做的很好,可是如今的总体架构变了,因此须要及时做出调整。第二,显示端的架构未能及时的重构,未能将显示端结构进行进一步分离,将显示逻辑独立可测试。app
这样随着SOA接口的不断增长,显示端直接将调用服务的方法嵌入到显示逻辑中,如,ASP.NET Mvc、ASP.NET Webapi的控制器中,包括两个层面之间的DTO转换。工具
按照DDD的上下文设计方法,在用户显示端也是能够有选择的建立面向显示的领域模型,此模型主要处理领域在即将到达服务端以后的前期处理。毕竟一个领域实体有着多个方面的职责,若是能在显示端创建起轻量级的领域模型,对显示逻辑的重构将大有好处,固然前提是你有着复杂的领域逻辑。(我以前的上一家公司(美国知名的电子商务平台),他们的显示端有着复杂的领域逻辑,就光一个显示端就复杂的让人吃惊,若是能在此基础上引入领域模型显示端上下文,将对复杂的逻辑处理颇有好好处,固然这只是我未经验证的猜想而已,仅供参考。)单元测试
对显示端领域模型处理有兴趣的能够参考本人写的有关这方面的两篇文章:测试
.NET应用架构设计—面向查询的领域驱动设计实践(调整传统三层架构,外加维护型的业务开关)
.NET应用架构设计—面向查询服务的参数化查询设计(分解业务点,单独配置各自的数据查询契约)
本来干净的显示逻辑多了不少无关的服务调用细节,还有不少转换逻辑,判断逻辑,而这些东西本来不属于这个地方,让他们放在合适的地方对显示逻辑的重构、重用颇有帮助。
若是不将其移出显示逻辑中,那么随着服务接口的不断增长和扩展,将直接致使你修改显示逻辑代码,若是你的显示逻辑代码是MVC、Webapi共用的逻辑,那么状况就更加复杂了,最后显示逻辑里面将被ViewModel与Service Dto之间的转换占领,你很难找到有价值的逻辑了。
解决这些问题的方法就是引入防腐层,尽管防腐层的初衷是为了解决系统集成时的领域模型之间的转换,可是我以为如今的系统架构和集成有着不少类似之处,咱们能够适当的借鉴这些好的设计方法来解决类似的问题。
引入防腐层以后,将本来不应出如今显示逻辑中的代码所有搬到防腐层中来,在防腐层中创建起OO机制,让这些OO对象可以和显示逻辑一块儿搭配使用。
图1:
将用户层分层三个子层,UiLayer,Show Logic Layer,Anticorrosive Layer,最后一个是服务的接口组,全部的服务接口调用均须要从防腐层走。
咱们须要将Show Logic Layer中的服务调用,类型转换代码迁移到Anticorrsoive Layer中,在这里能够对象化转换逻辑也能够不对象化,具体能够看下项目是否须要。若是业务确实比较复杂的时候,那么咱们为了封装、重用就须要进行对象化。
首先要作的就是将逻辑代码中的服务对象重构成面向接口的,而后让其动态的依赖注入到逻辑类型中。在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对象。
咱们须要一个防腐层来处理这些转换代码,包括对后端服务的调用逻辑,将这部分代码移入防腐对象中以后会对咱们后面重构颇有帮助。
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>基类中。
对于防腐层的设计,其实若是你的转换代码很少,业务也比较简单时,我建议直接写成过程式的代码比较简单点。将一些能够重用的代码直接使用静态的扩展方法来使用也是比较简单方便的,最大问题就是不利于后期的持续重构,咱们没法预知将来的业务变化,可是咱们可使用重构来解决。
相对应的,能够将转换代码进行对象化,造成防腐对象,每个对象专门用来处理某一个业务点的数据获取和转换逻辑,若是你有数据发送逻辑那么将在防腐对象中大大获益,对象化后就能够直接订阅相关控制器的依赖注入事件,若是你是过程式的代码想完成动态的转换、发送、获取会比较不方便。
咱们接着看一下如何让防腐对象无干扰的进行自动化的服务调用和发送,咱们但愿防腐对象彻底透明的在执行着防腐的职责,并不但愿它会给咱们实现上带来多大的开销。
咱们可使用事件来实现观察者模式,让防腐层对象监听某个事件,当事件触发时,自动的处理某个动做,而不是要显示的手动调用。
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 }
依赖注入接口是彻底为了将控制器与防腐对象之间隔离用的,上述代码中我是将接口定义在了防腐对象层中,那么也就是说控制器对象所在的项目须要引用防腐层,在处理事件和方法同时使用时会显得有点不三不四的,既有接口又有方法,其实这就是一种平衡吧,越纯粹的东西越要付出一些代价。
若是咱们定义纯粹的依赖注入接口让防腐对象去实现,那么在触发事件时就须要专门的方法来执行事件的触发,由于不在本类中的事件是没办法触发的。
本篇文章是我对在UI层使用防腐层架构设计思想的一个简单总结,目的只有一个,提供一个参考,谢谢你们。