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)。
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实例中。目的就是为了能够在后续的迭代重构中对该控制器进行单元测试,这可能有点麻烦,但是为了长久的利益还是需要的。
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类型属性,在正常情况下我们只需要获取订单的条目就行了,但是有些时候确实需要将条目中具体的产品信息也要返回给前台进行部分信息的展现。
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属性。
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实例,有些时候需要将该属性赋上值。
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完整信息。
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之间的转换放入防腐层
我们需要一个防腐层来处理这些转换代码,包括对后端服务的调用逻辑,将这部分代码移入防腐对象中之后会对我们后面重构很有帮助。
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.事件驱动(防腐层监听显示逻辑事件)
我们可以使用事件来实现观察者模式,让防腐层对象监听某个事件,当事件触发时,自动的处理某个动作,而不是要显示的手动调用。
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); 重要方法,该方法是用来让防腐对象自动注册事件用的。
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 } |
在控制器中,每当我们发生某个业务动作时只管触发事件即可,当然主要是以发送数据为主,查询可以直接调用对象的方法。因为防腐对象起到一个与后台服务集成的桥梁,当提交订单时可能需要同时调用很多个后台服务方法,用事件处理会比较方便。
/// <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层使用防腐层架构设计思想的一个简单总结,目的只有一个,提供一个参考,谢谢大家。
|