我们经常需要对一段已有的代码进行修改,结果是一处小的变动就会在整个应用程序中产生连锁反应,导致无法编译通过,甚至能引入某种错误。然而,软件总是需要经常的改变,代码具有弹性、灵活性和适应性,并最终让软件更加具有可维护性是很重要的。不容易改变最大的阻力来自于依赖。
一、评估代码的依赖程度
面向对象程序设计的真正威力在于,对象相互之间能够进行交互,从而形成一个更复杂的模块或组件。这样,我们就能执行更加复杂的过程,这个过程能够转化为通过工作流解决业务问题。依赖就是对象需要用户正确发挥功能的东西;识别代码中的依赖的一种容易的方式就是找出创建其他类的新实例的地方。通过在一个类中创建另一个类的实例,马上就建立了依赖,并使代码成为紧密耦合代码。了解这一点最好的方式当然就是示例:
假定您正供职于一家提供电子商务方案的软件公司。当前的程序只需要支持使用Alipay(支付宝)支付商提取订单付款和执行退款业务。
然而,有些客户需要使用ChinaPay支付平台。您的老板同意在下一个发布版本中包含这个新的支付平台,并选择允许公司通过一种配置设置来指定支付平台。
(1)我们新建一个类库项目并在项目中添加三个文件夹Model、Properties、Services。
(2)在Model文件夹中添加一个Customer类,表示退回订单项的消费者
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace ProEnt.Chap3.Returns.Model 7 { 8 public class Customer 9 { 10 private string _emailAddress; 11 private string _name; 12 private string _mobileNumber; 13 14 public string EmailAddress 15 { 16 get { return _emailAddress; } 17 set { _emailAddress = value; } 18 } 19 20 public string Name 21 { 22 get { return _name; } 23 set { _name = value; } 24 } 25 26 public string MobileNumber 27 { 28 get { return _mobileNumber; } 29 set { _emailAddress = value; } 30 } 31 } 32 }
(3)在Model文件夹下,添加Product,表示消费者退订的产品
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace ProEnt.Chap3.Returns.Model 7 { 8 public class Product 9 { 10 private int _id; 11 private int _stock; 12 private string _name; 13 14 public int Id 15 { 16 get { return _id; } 17 set { _id = value; } 18 } 19 20 public int Stock 21 { 22 get { return _stock;} 23 set { _stock = value;} 24 } 25 26 public string Name 27 { 28 get { return _name;} 29 set { _name = value; } 30 } 31 } 32 }
(4)在Model文件夹下,添加ReturnItem,表示消费者退订的项:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace ProEnt.Chap3.Returns.Model 7 { 8 public class ReturnItem 9 { 10 private int _qty; 11 private Product _product; 12 private decimal _unitPricePaid; 13 14 public int Qty 15 { 16 get { return _qty;} 17 set { _qty = value; } 18 } 19 20 public Product Product 21 { 22 get { return _product;} 23 set { _product = value;} 24 } 25 26 public decimal UnitPricePaid 27 { 28 get { return _unitPricePaid; } 29 set { _unitPricePaid = value; } 30 } 31 32 public decimal LinePrice() 33 { 34 return UnitPricePaid * Qty; 35 } 36 } 37 }
(5)在Model文件夹下,添加ReturnOrder,表示消费者退订的项:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace ProEnt.Chap3.Returns.Model 7 { 8 public class ReturnOrder 9 { 10 private Customer _customer; 11 private IList<ReturnItem> _itemsToReturn; 12 private string _paymentTransactionId; 13 14 public string PaymentTransactionId 15 { 16 get { return _paymentTransactionId; } 17 set { _paymentTransactionId = value; } 18 } 19 20 public Customer Customer 21 { 22 get { return _customer; } 23 set { _customer = value; } 24 } 25 26 public IList<ReturnItem> ItemsToReturn 27 { 28 get { return _itemsToReturn; } 29 set { _itemsToReturn = value; } 30 } 31 32 public void MarkAsProcessed() 33 { 34 // Changes the state of the order 35 } 36 37 public decimal RefundTotal() 38 { 39 var query = from Item in ItemsToReturn 40 select Item; 41 42 return query.Sum(a => a.LinePrice()); 43 } 44 } 45 }
(6)为了能够保存对象,并从数据存储中检索该对象,需要添加Product对象和ReturnOrder库。添加一个新类到Properties文件夹中,命名为:ProductRepository。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Repositories 8 { 9 public class ProductRepository 10 { 11 public Product FindBy(long Id) 12 { 13 // Here you would find code to 14 // retrieve Product from the data store. 15 return null; 16 } 17 18 public void Save(Product product) 19 { 20 // Here you would find code to persist 21 // Product to the data store. 22 } 23 } 24 }
(7)同样添加一个名为ReturnOrderRepository的类:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Repositories 8 { 9 public class ReturnOrderRepository 10 { 11 public void Save(ReturnOrder ReturnOrder) 12 { 13 // Here you would find code to persist 14 // a ReturnOrder to the data store. 15 } 16 } 17 }
(8)为了Alipay能退款,需要一个AlipayGateway网关。在Services文件夹中添加一个名叫AlipayGateway的类:
1 using System; 2 3 namespace ProEnt.Chap3.Returns.Services 4 { 5 public class AlipayGateway 6 { 7 8 public AlipayGateway(string userName, string password) 9 { 10 // Here you would find code to login to the 11 // PayPal payment merchant. 12 } 13 14 public Boolean Refund(decimal amount, string transactionId) 15 { 16 // Here you would find code to create a refund 17 // transaction and send it to PayPal. 18 return true; 19 } 20 } 21 }
(9)最后还要添加一个ReturnOrderService类到Services文件夹中。该类负责调整退订项的库存、将钱退回给相应的消费者,以及通知消费者退单过程已经完成。
1 using System; 2 using System.Net.Mail; 3 using System.Text; 4 using ProEnt.Chap3.Returns.Repositories; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Services 8 { 9 public class ReturnOrderService 10 { 11 private ProductRepository _productRepository; 12 private ReturnOrderRepository _returnOrderRepository; 13 private AlipayGateway _alipayGateway; 14 15 //这些最好从配置文件中读取,在这里为了示例简单就设置为空 16 private string _alipayUserName = string.Empty; 17 private string _alipayPassword = string.Empty; 18 private string _smtpHost = string.Empty; 19 20 public ReturnOrderService() { 21 _productRepository = new ProductRepository(); 22 _returnOrderRepository = new ReturnOrderRepository(); 23 _alipayGateway = new AlipayGateway(_alipayUserName, _alipayPassword); 24 } 25 26 public void Process(ReturnOrder returnOrder) { 27 // 1)Update the stock(库存) 28 foreach (ReturnItem item in returnOrder.ItemsToReturn) { 29 // a)Find the product 30 Product product = _productRepository.FindBy(item.Product.Id); 31 // b)Increase(增加) the stock 32 product.Stock += item.Qty; 33 // c) Save the product 34 _productRepository.Save(product); 35 36 } 37 38 // 2)Refund the payment back to the customer 39 _alipayGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 40 41 // 3) Mark the return order as processed 42 returnOrder.MarkAsProcessed(); 43 44 // 4) Save the updated return order 45 _returnOrderRepository.Save(returnOrder); 46 47 // 5) Notify the customer on the processing of the return. 48 String recipient = returnOrder.Customer.EmailAddress; 49 string from = "Returns@MyCompany.com"; 50 String subject = "Your return order has been processed."; 51 StringBuilder message = new StringBuilder(); 52 message.AppendLine(string.Format("Hello {0}",returnOrder.Customer.Name)); 53 message.AppendLine(" You return order has been processed and you have been refunded."); 54 message.AppendLine("Items returned: "); 55 foreach (ReturnItem itemReturning in returnOrder.ItemsToReturn) { 56 message.AppendLine(string.Format("{0} of {1}",itemReturning.Qty,itemReturning.Product.Name)); 57 } 58 message.AppendLine("Regards"); 59 message.AppendLine("Returns Department"); 60 61 MailMessage msgMail = new MailMessage(from,recipient,subject,message.ToString()); 62 SmtpClient smtp = new SmtpClient(_smtpHost); 63 smtp.Send(msgMail); 64 } 65 } 66 }
只有我们理解了ReturnOrderService内部机制,才能添加代码来支持ChinaPay支付平台。有人可能认为,添加ChinaPay支付平台最简单的方式是添加一个新的支付网关ChinaPayGateway。退款给Alipay方式一样,在ReturnOrderService中使用ChinaPayGateway。
我们首先在Services文件夹中创建一个新类ChinaPayGateway:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace ProEnt.Chap3.Returns.Services 7 { 8 public class ChinaPayGateway 9 { 10 public ChinaPayGateway(string MerchantAccount, string userName, string password) 11 { 12 // Here you would find code to login to the 13 // WorldPay payment merchant. 14 } 15 16 public Boolean Refund(decimal amount, string transactionId) 17 { 18 // Here you would find code to create a refund 19 // transaction and send it to the WorldPay payment merchant. 20 return true; 21 } 22 } 23 }
现在可以按照下面的方式修改ReturnOrderService
1 using System; 2 using System.Net.Mail; 3 using System.Text; 4 using ProEnt.Chap3.Returns.Repositories; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Services 8 { 9 public class ReturnOrderService 10 { 11 private ProductRepository _productRepository; 12 private ReturnOrderRepository _returnOrderRepository; 13 private AlipayGateway _alipayGateway; 14 15 16 private ChinaPayGateway _chinaPayGateway; 17 18 19 //这些最好从配置文件中读取,在这里为了示例简单就设置为空 20 private string _alipayUserName = string.Empty; 21 private string _alipayPassword = string.Empty; 22 private string _smtpHost = string.Empty; 23 24 private string _chinaPayUserName = string.Empty; 25 private string _chinaPayPassword = string.Empty; 26 private string _chinaPayMerchantId = string.Empty; 27 private string _paymentGatewayType = string.Empty; 28 29 public ReturnOrderService() { 30 _productRepository = new ProductRepository(); 31 _returnOrderRepository = new ReturnOrderRepository(); 32 33 if (_paymentGatewayType == "Alipay") 34 { 35 _alipayGateway = new AlipayGateway(_alipayUserName, _alipayPassword); 36 } 37 else if (_paymentGatewayType == "ChinaPay") 38 { 39 _chinaPayGateway = new ChinaPayGateway(_chinaPayMerchantId, _chinaPayUserName, _chinaPayPassword); 40 } 41 42 } 43 44 public void Process(ReturnOrder returnOrder) { 45 // 1)Update the stock(库存) 46 foreach (ReturnItem item in returnOrder.ItemsToReturn) { 47 // a)Find the product 48 Product product = _productRepository.FindBy(item.Product.Id); 49 // b)Increase(增加) the stock 50 product.Stock += item.Qty; 51 // c) Save the product 52 _productRepository.Save(product); 53 54 } 55 56 // 2)Refund the payment back to the customer 57 if (_paymentGatewayType == "Alipay") 58 { 59 _alipayGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 60 } 61 else if (_paymentGatewayType == "ChinaPay") { 62 _chinaPayGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 63 } 64 65 66 // 3) Mark the return order as processed 67 returnOrder.MarkAsProcessed(); 68 69 // 4) Save the updated return order 70 _returnOrderRepository.Save(returnOrder); 71 72 // 5) Notify the customer on the processing of the return. 73 String recipient = returnOrder.Customer.EmailAddress; 74 string from = "Returns@MyCompany.com"; 75 String subject = "Your return order has been processed."; 76 StringBuilder message = new StringBuilder(); 77 message.AppendLine(string.Format("Hello {0}",returnOrder.Customer.Name)); 78 message.AppendLine(" You return order has been processed and you have been refunded."); 79 message.AppendLine("Items returned: "); 80 foreach (ReturnItem itemReturning in returnOrder.ItemsToReturn) { 81 message.AppendLine(string.Format("{0} of {1}",itemReturning.Qty,itemReturning.Product.Name)); 82 } 83 message.AppendLine("Regards"); 84 message.AppendLine("Returns Department"); 85 86 MailMessage msgMail = new MailMessage(from,recipient,subject,message.ToString()); 87 SmtpClient smtp = new SmtpClient(_smtpHost); 88 smtp.Send(msgMail); 89 } 90 } 91 }
因为时间紧迫,所以添加ChinaPay支付选项时,使用过程编码模式与前任开发人员一样;这看起来好像是最容易和最快的解决方案。
在对ReturnOrderService类进行回顾后,发现该类与很多类是紧密耦合的。这样的设计是比较差的。
刚性
上面列出的代码刚性如何呢?为了改变低层关注点的任何方面,例如AlipayGateway的构建方式,需要修改高层类(ReturnOrderService)。这 表明该类是不灵活的,难以改变的。
灵活性
灵活性测度的是改变代码的难易程度。以ReturnOrderService为例,如果该类型多依赖的类型进行了任何的改变,那么很可能破坏原有结果。 相对较低层的改变会影响到高层模块。
关注点分离
ReturnOrderService的关注点 显然没有得到分离,因为它应该只与负责处理退货订单工作流程相关,而不应受限于其他的细节。
可重用性
重用性测度是代码的可移植性,以及在软件其他地方可以重用到什么程度。显然其他类中也不能使用该过程中的部分代码。重用发送电子邮件的逻辑,处理库存调整的代码,我们都应该与其他地方重用。
可维护性
方法应该短而精:在命名上,这些方法应该能够直接体现出它们所要完成的功能。ReturnOrderService类当前的实现非常的不可读,需要一定的时间 来完全理解,在修改和添加功能后,变的更不好读。由于具有紧密耦合的相关性,导致代码缺乏清晰性。当添加功能是,对自己和别人来讲,维护代码库 都将是困难的。还可以看出ReturnOrderService并不符合单一职责准则,该类关注过多的底层细节。 并不是添加新的支付商才导致该类表现出脆弱性,而是实现任意类似的功能时都可能导致问题
- AlipayGateway包装器实现可能改变
- 退货入库过程可能要求将所有的库存调整记录在一个aduit表中。
- 改变配置设置
该代码块存在的另一个问题就是测试。单元测试的目的是要将程序的各个部分分离,并校验每个单独部分的行为是否如期望的那样。这里只能做集成测试了。
二、关注点分离和识别模块性
既然知道这段代码存在问题,我们现在就开始重构这段代码。 首先,需要分离ReturnOrderService类中出现的每个关注点。
关注点分离准则:就是对软件短进行分解,并将其分割为独立的功能,这些功能封装了其他类可能使用的不同行为和数据。 通过模块化代码,并封装不同的特定数据和行为 就可以达到关注点分离的目的。
首先我们来看一下第一段代码,这段代码负责将商品返回库存清单。我们可以封装调整库存的过程,并将这个关注点与退货订单工作流程分离。 我们新添加一个ProductService类,提供调整库存的产品列表相关的服务。
1 using ProEnt.Chap3.Returns.Repositories; 2 using ProEnt.Chap3.Returns.Model; 3 4 namespace ProEnt.Chap3.Returns.Services 5 { 6 public class ProductService 7 { 8 9 private ProductRepository _productRepository; 10 public ProductService() 11 { 12 _productRepository = new ProductRepository(); 13 } 14 15 public void ReturnStockFor(ReturnOrder ReturnOrder) 16 { 17 // 1) Update the stock. 18 foreach (ReturnItem Item in ReturnOrder.ItemsToReturn) 19 { 20 // a) Find the product. 21 Product product = _productRepository.FindBy(Item.Product.Id); 22 // b) Increase the stock. 23 product.Stock += Item.Qty; 24 // c) Save the product. 25 _productRepository.Save(product); 26 } 27 } 28 } 29 }
回到ReturnOrderService类,删除对ProductRepository类的引用,并且使用新类ProductService的调用替代他们。变更可存的代码变的如下:
// 1)Update the stock(库存) _productService.ReturnStockFor(returnOrder);
需要处理的下一段代码就是关于电子邮件的逻辑。首先提取创建消息体的代码段放到一个新方法中,并命名为BuildReturnMessageFrom。 接下来,将发送邮件的方法提取到一个新方法中,并命名为SendReturnOrderNotifiationFor。已经提取两个电子邮件方法,现在创建一个新类将通知的动作与退单的工作流程分离。在Services文件夹中创建一个新的服务,将其命名为EmailNotificationService。
1 using System; 2 using System.Net.Mail; 3 using System.Text; 4 using ProEnt.Chap3.Returns.Model; 5 6 namespace ProEnt.Chap3.Returns.Services 7 { 8 public class EmailNotificationService 9 { 10 // This settings would be stored in the AppSettings of the config file. 11 private string _smtpHost = string.Empty; 12 13 public void SendReturnOrderNotificationFor(ReturnOrder ReturnOrder) 14 { 15 // 5) Notify the customer on the processing of the return. 16 String Recipient = ReturnOrder.Customer.EmailAddress; 17 String From = "Returns@MyCompany.com"; 18 String Subject = "Your return order has been processed."; 19 StringBuilder Message = BuildReturnMessageFrom(ReturnOrder); 20 21 MailMessage msgMail = new MailMessage(From, Recipient, Subject, Message.ToString()); 22 23 SmtpClient smtp = new SmtpClient(_smtpHost); 24 smtp.Send(msgMail); 25 } 26 27 private StringBuilder BuildReturnMessageFrom(ReturnOrder ReturnOrder) 28 { 29 StringBuilder Message = new StringBuilder(); 30 31 Message.AppendLine(String.Format("Hello {0}", ReturnOrder.Customer.Name)); 32 Message.AppendLine("You return order has been processed and you have been refunded."); 33 34 Message.AppendLine("Items returned: "); 35 foreach (ReturnItem ItemReturning in ReturnOrder.ItemsToReturn) 36 { 37 Message.AppendLine(String.Format("{0} of {1}", ItemReturning.Qty, ItemReturning.Product.Name)); 38 } 39 40 Message.AppendLine("Regards"); 41 Message.AppendLine("Returns Department"); 42 return Message; 43 } 44 } 45 }
现在通过对新类EmailNotificationService的引用,就可以更新ReturnOrderService类。
1 using System; 2 using System.Net.Mail; 3 using System.Text; 4 using ProEnt.Chap3.Returns.Repositories; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Services 8 { 9 public class ReturnOrderService 10 { 11 private EmailNotificationService _notificationService; 12 private ProductService _productService; 13 private ReturnOrderRepository _returnOrderRepository; 14 private AlipayGateway _alipayGateway; 15 private ChinaPayGateway _chinaPayGateway; 16 17 //这些最好从配置文件中读取,在这里为了示例简单就设置为空 18 private string _alipayUserName = string.Empty; 19 private string _alipayPassword = string.Empty; 20 private string _smtpHost = string.Empty; 21 22 private string _chinaPayUserName = string.Empty; 23 private string _chinaPayPassword = string.Empty; 24 private string _chinaPayMerchantId = string.Empty; 25 private string _paymentGatewayType = string.Empty; 26 27 public ReturnOrderService() { 28 _notificationService = new EmailNotificationService(); 29 _productService = new ProductService(); 30 _returnOrderRepository = new ReturnOrderRepository(); 31 if (_paymentGatewayType == "Alipay") 32 { 33 _alipayGateway = new AlipayGateway(_alipayUserName, _alipayPassword); 34 } 35 else if (_paymentGatewayType == "ChinaPay") 36 { 37 _chinaPayGateway = new ChinaPayGateway(_chinaPayMerchantId, _chinaPayUserName, _chinaPayPassword); 38 } 39 40 } 41 42 public void Process(ReturnOrder returnOrder) { 43 // 1)Update the stock(库存) 44 _productService.ReturnStockFor(returnOrder); 45 46 // 2)Refund the payment back to the customer 47 if (_paymentGatewayType == "Alipay") 48 { 49 _alipayGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 50 } 51 else if (_paymentGatewayType == "ChinaPay") { 52 _chinaPayGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 53 } 54 // 3) Mark the return order as processed 55 returnOrder.MarkAsProcessed(); 56 57 // 4) Save the updated return order 58 _returnOrderRepository.Save(returnOrder); 59 60 // 5) Notify the customer on the processing of the return. 61 _notificationService.SendReturnOrderNotificationFor(returnOrder); 62 } 63 } 64 }
对于最后一个重构器,需要提取处理支付方法的逻辑。两种支付网关的退货方法,都需要TransactionId和Amount来处理退货。可以提取一个超类,让两种支付网关的情况通过继承实现;第一步构建一个基类,使AlipayGateway和ChinaPayGateway都继承它。在service文件夹中添加一个名为PaymentGateway的新类:
using System; namespace ProEnt.Chap3.Returns.Services { public abstract class PaymentGateway { public abstract Boolean Refund(decimal amount, string transactionId); } }
之后,我们要调整AlipayGateway和ChinaPayGateway,使他们继承已经创建好的基类:
public class AlipayGateway :PaymentGateway { public AlipayGateway(string userName, string password) { // Here you would find code to login to the // PayPal payment merchant. } public override Boolean Refund(decimal amount, string transactionId) { // Here you would find code to create a refund // transaction and send it to PayPal. return true; } } public class ChinaPayGateway :PaymentGateway { public ChinaPayGateway(string MerchantAccount, string userName, string password) { // Here you would find code to login to the // WorldPay payment merchant. } public override Boolean Refund(decimal amount, string transactionId) { // Here you would find code to create a refund // transaction and send it to the WorldPay payment merchant. return true; } }
现在ReturnOrderService类可以在Process()方法中使用新的PaymentGateway抽象基类执行退货,并将确定使用哪种支付平台实现的问题留在构造函数中完成:
1 using System; 2 using System.Net.Mail; 3 using System.Text; 4 using ProEnt.Chap3.Returns.Repositories; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Services 8 { 9 public class ReturnOrderService 10 { 11 private EmailNotificationService _notificationService; 12 private ProductService _productService; 13 private ReturnOrderRepository _returnOrderRepository; 14 private PaymentGateway _paymentGateway; 15 16 17 //这些最好从配置文件中读取,在这里为了示例简单就设置为空 18 private string _alipayUserName = string.Empty; 19 private string _alipayPassword = string.Empty; 20 private string _smtpHost = string.Empty; 21 22 private string _chinaPayUserName = string.Empty; 23 private string _chinaPayPassword = string.Empty; 24 private string _chinaPayMerchantId = string.Empty; 25 private string _paymentGatewayType = string.Empty; 26 27 public ReturnOrderService() { 28 _notificationService = new EmailNotificationService(); 29 _productService = new ProductService(); 30 _returnOrderRepository = new ReturnOrderRepository(); 31 if (_paymentGatewayType == "Alipay") 32 { 33 _paymentGateway = new AlipayGateway(_alipayUserName, _alipayPassword); 34 } 35 else if (_paymentGatewayType == "ChinaPay") 36 { 37 _paymentGateway = new ChinaPayGateway(_chinaPayMerchantId, _chinaPayUserName, _chinaPayPassword); 38 } 39 40 } 41 42 public void Process(ReturnOrder returnOrder) { 43 44 _productService.ReturnStockFor(returnOrder); 45 46 _paymentGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 47 48 returnOrder.MarkAsProcessed(); 49 50 _returnOrderRepository.Save(returnOrder); 51 52 _notificationService.SendReturnOrderNotificationFor(returnOrder); 53 } 54 } 55 }
三、依赖倒置准则
我们对代码进行模块化处理,并将单一职责和关注点分离准则应用到ReturnOrderService后,其对较低层模块的依赖依然没有变。 需要一种方式消除这种具体的依赖,使得对低层模块的改变不会影响到ReturnOrderService。应该将这种状况倒置过来, 有些人将倒置以后的情形称为“好莱坞准则”:“不用给我们打电话,我们会给你打电话”。这意味着要以抽象的形式来引用低层 模块,而不是引用具体的实现。
依赖倒置准则定义:
(1)高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
(2)抽象不应该依赖于细节。细节应该依赖于抽象。
依赖倒置准则就是将类从具体的实现中分离出来,让这些类仅依赖于抽象类或接口。当使用了依赖倒置准则以后,对低层模块的改变不再会 对高层模块产生想前面列出的代码中所见到的影响了。
应用依赖倒置准的第一步是针对每种低层依赖提取接口。首先,可以针对ProjuctService提取接口。打开ProjuctService类,然后右击显示上下文菜单, 从中选择 重构|提取接口命令。在弹出对话框中单击“选择全部”按钮,然后单击“确定”按钮。 现在我们有了针对ProjuctService的接口,因此可以将其用于ReturnOrderService类。 对下面的类,执行同样的提取接口重构,并将具体的变量声明替换为使用接口进行的声明。
- EmailNotificationService 将接口命名为 INotificationService。
- ReturnOrderRepository 将接口命名为 IReturnOrderRepository。
- ProductRepository 将接口命名为 IProductRepository。
- PaymentGateway 将接口命名为 IPaymentGateway。
完成这些工作后,ReturnOrderService类如下所示:
1 using System; 2 using System.Net.Mail; 3 using System.Text; 4 using ProEnt.Chap3.Returns.Repositories; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Services 8 { 9 public class ReturnOrderService 10 { 11 private INotificationService _notificationService; 12 private IProductService _productService; 13 private IReturnOrderRepository _returnOrderRepository; 14 private IPaymentGateway _paymentGateway; 15 16 17 //这些最好从配置文件中读取,在这里为了示例简单就设置为空 18 private string _alipayUserName = string.Empty; 19 private string _alipayPassword = string.Empty; 20 private string _smtpHost = string.Empty; 21 22 private string _chinaPayUserName = string.Empty; 23 private string _chinaPayPassword = string.Empty; 24 private string _chinaPayMerchantId = string.Empty; 25 private string _paymentGatewayType = string.Empty; 26 27 public ReturnOrderService() { 28 _notificationService = new EmailNotificationService(); 29 _productService = new ProductService(); 30 _returnOrderRepository = new ReturnOrderRepository(); 31 if (_paymentGatewayType == "Alipay") 32 { 33 _paymentGateway = new AlipayGateway(_alipayUserName, _alipayPassword); 34 } 35 else if (_paymentGatewayType == "ChinaPay") 36 { 37 _paymentGateway = new ChinaPayGateway(_chinaPayMerchantId, _chinaPayUserName, _chinaPayPassword); 38 } 39 40 } 41 42 public void Process(ReturnOrder returnOrder) { 43 // 1)Update the stock(库存) 44 _productService.ReturnStockFor(returnOrder); 45 46 // 2)Refund the payment back to the customer 47 _paymentGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 48 49 // 3) Mark the return order as processed 50 returnOrder.MarkAsProcessed(); 51 52 // 4) Save the updated return order 53 _returnOrderRepository.Save(returnOrder); 54 55 // 5) Notify the customer on the processing of the return. 56 _notificationService.SendReturnOrderNotificationFor(returnOrder); 57 } 58 } 59 }
现在ReturnOrderService类依赖于接口形式的抽象类,而不是直接依赖于具体的实现。
四、使用依赖注入彻底解放类
ReturnOrderService类依然与每个接口的单一实现关联,它仍然负责创建那些依赖关系;例如如果希望使用不同类型的INotificationService,那么就必须 修改ReturnOrderService类,这可能需要添加更多的一些配置项。这显然不合适,我们可以用依赖注入来帮助我们解决这个问题。
依赖注入模式是指为高层模块提供外部依赖行为。它属于一种控制倒置,在该方法中,获取低层模块的过程是倒置的关注点。 控制倒置通常与依赖注入互换使用,它是一种高层抽象准则,与工作流的过程样式相比,控制倒置与系统的流或过程的倒置有关;
使用依赖注入获得对外部模块引用,方法有三种:
- 构造函数注入:构造函数注入通常应用于重构的最后阶段,它是通过类的构造函数提供依赖的过程。
- 设置函数注入:设置函数注入通过设置函数(setter)属性将依赖模块注入依赖模块的过程。
- 方法注入:方法注入要求依赖实现一个接口,高层模块会在运行时引用和注入这个接口。
现在将依赖注入的构造函数注入应用于ReturnOrderService,从而将INotificationService的实例注入Service类,而不再在构造函数中创建。 修改过ReturnOrderService类代码如下所示:
public ReturnOrderService(INotificationService notificationService) { _notificationService = notificationService; ..... }
现在ReturnOrderService类不再负责创建NotificationService类的有效实例了,更重要的是,service类不再与INotificationService的任何一个实现相关;
设置函数注入需要使用关于高层模块的属性以注入低层模块的依赖。这样的好处之一就是允许随意的交换依赖。这种方式提供了更大的灵活性。对于一个更复杂的示例,不使用构造函数,而是通过一个设置函数(setter)属性,就可以在ReturnOrderService中添加一个属性注入INotificationService的实例。如果不打算向消费者发送退单通知,那么可以选择不注入INotificationService的实例,或者使用一个占位程序实现。实现代码如下所示:
1 using System; 2 using System.Net.Mail; 3 using System.Text; 4 using ProEnt.Chap3.Returns.Repositories; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Services 8 { 9 public class ReturnOrderService 10 { 11 private INotificationService _notificationService; 12 private IProductService _productService; 13 private IReturnOrderRepository _returnOrderRepository; 14 private IPaymentGateway _paymentGateway; 15 16 17 //这些最好从配置文件中读取,在这里为了示例简单就设置为空 18 private string _alipayUserName = string.Empty; 19 private string _alipayPassword = string.Empty; 20 private string _smtpHost = string.Empty; 21 22 private string _chinaPayUserName = string.Empty; 23 private string _chinaPayPassword = string.Empty; 24 private string _chinaPayMerchantId = string.Empty; 25 private string _paymentGatewayType = string.Empty; 26 27 public ReturnOrderService() 28 { 29 _productService = new ProductService(); 30 _returnOrderRepository = new ReturnOrderRepository(); 31 if (_paymentGatewayType == "Alipay") 32 { 33 _paymentGateway = new AlipayGateway(_alipayUserName, _alipayPassword); 34 } 35 else if (_paymentGatewayType == "ChinaPay") 36 { 37 _paymentGateway = new ChinaPayGateway(_chinaPayMerchantId, _chinaPayUserName, _chinaPayPassword); 38 } 39 40 } 41 42 public void Process(ReturnOrder returnOrder) { 43 // 1)Update the stock(库存) 44 _productService.ReturnStockFor(returnOrder); 45 46 // 2)Refund the payment back to the customer 47 _paymentGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 48 49 // 3) Mark the return order as processed 50 returnOrder.MarkAsProcessed(); 51 52 // 4) Save the updated return order 53 _returnOrderRepository.Save(returnOrder); 54 55 if (_notificationService != null) { 56 _notificationService.SendReturnOrderNotificationFor(returnOrder); 57 } 58 } 59 public INotificationService NotificationService { 60 get { return _notificationService; } 61 set { _notificationService = value; } 62 } 63 } 64 }
需要注意的是,设置函数注入仅有的不足是,需要进行一定的检查以确保在使用依赖之前已经设置了该依赖。设置函数注入方法可能不如构造函数注入那么显而易见,但是如果在构造函数中需要注入大量的其他依赖,那么将可选的依赖移到基于设置函数的方法更加明智。
使用方法注入的一个适当场合是当需要有一些不适合放入属性的逻辑时。当需要将日志 功能添加到ReturnOrderService和所有的低层依赖中时,可以在ReturnOrderService中添加一个方法,然后该类会自动更新所有与该日志相关的依赖。示例代码如下所示:
public class ReturnOrderService { ... public ReturnOrderService() { ... } public void Process(ReturnOrder returnOrder) { ... } public void SetLogger(ILogger logger) { _notificationService.SetLogger(logger); _productService.SetLogger(logger); _returnOrderRepository.SetLogger(logger); _paymentGateway.SetLogger(logger); } }
为了在创建所有的依赖时确保这些依赖都注入到了ReturnOrderService中,可以选择依赖注入的构造函数注入。从ProductService类开始,按照如下方式修改构造函数:
1 using ProEnt.Chap3.Returns.Repositories; 2 using ProEnt.Chap3.Returns.Model; 3 4 namespace ProEnt.Chap3.Returns.Services 5 { 6 public class ProductService : ProEnt.Chap3.Returns.Services.IProductService 7 { 8 9 private ProductRepository _productRepository; 10 public ProductService(ProductRepository productRepository) 11 { 12 _productRepository = productRepository; 13 } 14 15 public void ReturnStockFor(ReturnOrder ReturnOrder) 16 { 17 // 1) Update the stock. 18 foreach (ReturnItem Item in ReturnOrder.ItemsToReturn) 19 { 20 // a) Find the product. 21 Product product = _productRepository.FindBy(Item.Product.Id); 22 // b) Increase the stock. 23 product.Stock += Item.Qty; 24 // c) Save the product. 25 _productRepository.Save(product); 26 } 27 } 28 } 29 }
接下来,修改ReturnOrderService类的构造函数:
1 using System; 2 using System.Net.Mail; 3 using System.Text; 4 using ProEnt.Chap3.Returns.Repositories; 5 using ProEnt.Chap3.Returns.Model; 6 7 namespace ProEnt.Chap3.Returns.Services 8 { 9 public class ReturnOrderService 10 { 11 private INotificationService _notificationService; 12 private IProductService _productService; 13 private IReturnOrderRepository _returnOrderRepository; 14 private IPaymentGateway _paymentGateway; 15 16 public ReturnOrderService(INotificationService notificationService, IProductService productService, IReturnOrderRepository returnOrderRepository, IPaymentGateway paymentGateway) 17 { 18 _notificationService = notificationService; 19 _productService = productService; 20 _returnOrderRepository = returnOrderRepository; 21 _paymentGateway = paymentGateway; 22 } 23 24 public void Process(ReturnOrder returnOrder) { 25 // 1)Update the stock(库存) 26 _productService.ReturnStockFor(returnOrder); 27 28 // 2)Refund the payment back to the customer 29 _paymentGateway.Refund(returnOrder.RefundTotal(), returnOrder.PaymentTransactionId); 30 31 // 3) Mark the return order as processed 32 returnOrder.MarkAsProcessed(); 33 34 // 4) Save the updated return order 35 _returnOrderRepository.Save(returnOrder); 36 37 if (_notificationService != null) { 38 _notificationService.SendReturnOrderNotificationFor(returnOrder); 39 } 40 } 41 42 } 43 }
可以看出,依赖注入是高层依赖倒置准则的一种实现,以及依赖注入本身就是某种形式的控制反转。
刚性
现在ReturnOrderService很容易改变依赖,而不用担心影响到ReturnOrderService本身。
灵活性
现在不存在由于低层细节改变而导致ReturnOrderService类崩溃的危险;这是因为ReturnOrderService依赖于接口而不依赖于具体的类型。
关注点分离
现在,退单返还相关的每个过程的关注点都位于独立的类中,而这些类都是通过接口来引用的,ReturnOrderService仅负责协调工作流,不再处理低层细节;
可重性
现在,对于低层模块的任意数量的新实现,ReturnOrderService都可以得到重用,而自身却不会发生改变
可维护性
现在ReturnOrderService职责很清晰,并通过显而易见的服务名和方法名,从而使开发人员和以后的维护人员,快速的浏览高层过程方法。
从客户的观点来看如何使用ReturnOrderService类的代码如下:
ReturnOrderService returnOrderService; INotificationService notificationService = new EmailNotificationService(); IProductRepository productRepository = new ProductRepository(); IProductService productService = new ProductService(productRepository); IPaymentGateway paymentGateway = new AlipayGateway("USERNAME","PASSWORD"); IReturnOrderRepository returnOrderRepository = new ReturnOrderRepository(); returnOrderService = new ReturnOrderService(notificationService, productService, returnOrderRepository, paymentGateway);