开始一个新项目
时间现在地点你公司汽车租赁服务相关的研发部的办公室人物你的技术团队或者只有你自己背景启动一个新的项目高大上一点叫做客户忠诚度系统low一点叫做客户积分程序。目的是为了增加销售奖励那些经常购买服务的客户。比如客户今天租赁了一辆车那么他就会获得积分积分累积多了之后以后可以用于抵消一部分租赁费用或其他费用。假设有一个基本的三层架构如下图。我们会从应用到这个积分系统的核心业务逻辑层着手编写代码持久化层会跟踪客户的忠诚度积分业务逻辑层供所有的UI层使用网站APP和店员使用的桌面端。这一篇我们主要看一下中间一层的业务逻辑层。我们可以假设持久化层已经实现了还要假设一旦业务逻辑实现了UI也就实现了。业务需求项目经理和利益相关人比如销售和市场确定了下图的业务需求你已经确定了两个主要的需求集累积积分和使用累积的积分兑换奖励。现在的业务需求就是客户每租一天普通型车辆累积一积分豪华型或者大型车辆每天两积分。这些积分会在他们支付之后并返还了车以后会增加到他们的账户中。一旦客户累积了10积分那么就可以使用这些积分兑换奖励了具体兑换规则见上图。这就是所有业务规则但是在实现之前还是得和销售和市场确定好因为他们将来肯定还会更改或者添加一些东西。必要的非功能需求在给项目经理估算时间和花销之前你有自己必须要解决的技术关注点。第一需要记录日志。如果客户的积分累积得不对累积少了那么他们会生气的因此必须确保记录了业务逻辑处理的一切尤其是起初阶段。第二因为业务逻辑代码会被多个UI应用使用要确保传入业务层的数据是合法的你的队友可能会在UI里写入一些集成代码因此必须编写防御性代码来检查无意义的边缘情况和参数。第三还是因为业务逻辑代码会被多个UI应用使用这些UI可能会使用不同类型的连接缓慢的移动手机的连接国外浏览器访问等等你需要采用事务和重试逻辑来确保维护数据集成以及给用户提供一个愉快的体验。最后总有意外会发生你可能不知道此时你会使用何种类型的持久化所以需要某种方法处理异常很可能是记录日志。没有AOP的生活将评估提交给项目经理之后所有的批准和文件也已经签署了现在就可以开始了。新建一个解决方案名叫CarRental并创建一个类库项目存放业务逻辑取名CarRental.Core编写业务逻辑创建一个累积积分的接口代码如下public interface ILoyaltyAccrualService { void Accrue(RentalAgreement agreement); }RentalAgreement是该积分系统领域公用的一个实体类因此按理说它应该在一个不同的程序集但这里为了演示我创建了一个Entities的文件夹存放所有的实体。public class RentalAgreement { public Guid Id { get; set; } public Customer Customer { get; set; } public Vehicle Vehicle { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } } public class Customer { public Guid Id { get; set; } public string Name { get; set; } public string DriversLicense { get; set; } public DateTime DateOfBirth { get; set; } } public class Vehicle { public Guid Id { get; set; } public string Make { get; set; } public string Model { get; set; } public Size Size { get; set; } public string Vin { get; set; } } public enum Size { Compact0, Midsize, FullSize, Luxury, Truck, SUV }再回头看ILoyaltyAccrualService接口该接口有一个使用了这些实体的Accure方法用来为客户累积积分。下面是该接口的实现它会依赖一个持久化数据的服务。Accure方法会包含了计算协议中天数和这些天共累积多少积分的业务逻辑并将这些积分数量存储到数据库中。public class LoyaltyAccrualService:ILoyaltyAccrualService { private readonly ILoyaltyDataService _loyaltyDataService; public LoyaltyAccrualService(ILoyaltyDataService loyaltyDataService) { _loyaltyDataService loyaltyDataService;//数据服务必须在该对象初始化时传入该对象 } /// summary /// 该方法包含了积分系统累积客户积分的逻辑和规则 /// /summary /// param nameagreement租赁协议实体/param public void Accrue(RentalAgreement agreement) { var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate); var numberOfDays (int)rentalTimeSpan.TotalDays; var pointsPerDay 1; if (agreement.Vehicle.Size Size.Luxury) { pointsPerDay 2; } var points numberOfDays*pointsPerDay; //调用数据服务存储客户获得的积分 _loyaltyDataService.AddPoints(agreement.Customer.Id,points); } }ILoyaltyDataService只有两个方法public interface ILoyaltyDataService { void AddPoints(Guid customerId,int points); void SubstractPoints(Guid customerId, int points); }ILoyaltyDataService作为数据库接口会通过DI的方式传入到业务层的构造函数。因为我们现在只集中在业务逻辑层所以我们在数据服务层只是简单地打印一些东西就好了FakeLoyaltyDataService实现了ILoyaltyDataService如下public class FakeLoyalDataService:ILoyaltyDataService { public void AddPoints(Guid customerId, int points) { Console.WriteLine(客户{0}增加了{1}积分,customerId,points); } public void SubstractPoints(Guid customerId, int points) { Console.WriteLine(客户{0}减少了{1}积分, customerId, points); } }到这里已经完成了累积积分的业务逻辑现在回到客户关心的问题上如何兑换积分创建一个接口ILoyaltyRedemptionServicepublic interface ILoyaltyRedemptionService { void Redeem(Invoice invoice, int numberOfDays); } /// summary /// 发票实体 /// /summary public class Invoice { public Guid Id { get; set; } public Customer Customer { get; set; } public Vehicle Vehicle { get; set; } public int CostPerDay { get; set; } public decimal Discount { get; set; } }兑换积分是基于客户租赁的车型和兑换的天数从客户的账户中减去积分并填充发票中的折扣金额。代码如下public class LoyalRedemptionService:ILoyaltyRedemptionService { private readonly ILoyaltyDataService _loyaltyDataService; public LoyalRedemptionService(ILoyaltyDataService loyaltyDataService) { _loyaltyDataService loyaltyDataService; } public void Redeem(Invoice invoice, int numberOfDays) { var pointsPerDay 10; if (invoice.Vehicle.SizeSize.Luxury) { pointsPerDay 15; } var totalPoints pointsPerDay*numberOfDays; invoice.Discount numberOfDays*invoice.CostPerDay; _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints); } }测试业务逻辑下面创建一个控制台UI模拟业务逻辑的使用class Program { static void Main(string[] args) { SimulateAddingPoints();//模拟累积 Console.WriteLine(***************); SimulateRemovingPoints();//模拟兑换 Console.Read(); } /// summary /// 模拟累积积分 /// /summary static void SimulateAddingPoints() { var dataServicenew FakeLoyalDataService();//这里使用的数据库服务是伪造的 var servicenew LoyaltyAccrualService(dataService); var agreementnew RentalAgreement { Customer new Customer { Id Guid.NewGuid(), Name tkb至简, DateOfBirth new DateTime(2000,1,1), DriversLicense 123456 }, Vehicle new Vehicle { Id Guid.NewGuid(), Make Ford, Model 金牛座, Size Size.Compact, Vin 浙-ABC123 }, StartDate DateTime.Now.AddDays(-3), EndDate DateTime.Now }; service.Accrue(agreement); } /// summary /// 模拟兑换积分 /// /summary static void SimulateRemovingPoints() { var dataService new FakeLoyalDataService(); var service new LoyalRedemptionService(dataService); var invoice new Invoice { Customer new Customer { Id Guid.NewGuid(), Name Farb, DateOfBirth new DateTime(1999, 1, 1), DriversLicense abcdef }, Vehicle new Vehicle { Id Guid.NewGuid(), Make 奥迪, Model Q7, Size Size.Compact, Vin 浙-DEF123 }, CostPerDay 100m, Id Guid.NewGuid() }; service.Redeem(invoice,3);//这里兑换3天 } }运行程序伪造的数据服务会在控制台上打印一些东西结果如下现在业务逻辑完成了代码很干净分离地也很好很容易阅读和维护但是这代码还不能进入生产环境因为有各种各样可能会出错的事情发生因此下面着手新功能的需求开发。添加日志虽然审计积分事务还不是一个需求但是为了安全起见最好还是记录每个请求至少是为了QA质量保证的目的。在生产环境可能会限制或减少日志但是现在我们要放一些简单的日志帮助开发者重现QA找到的bugs。现在当累积积分和兑换积分时添加日志其余代码和之前的一样。/// summary /// 该方法包含了积分系统累积客户积分的逻辑和规则 /// /summary /// param nameagreement租赁协议实体/param public void Accrue(RentalAgreement agreement) { Console.WriteLine(Accrue:{0},DateTime.Now); Console.WriteLine(Customer:{0},agreement.Customer.Id); Console.WriteLine(Vehicle:{0},agreement.Vehicle.Id); var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate); var numberOfDays (int)rentalTimeSpan.TotalDays; var pointsPerDay 1; if (agreement.Vehicle.Size Size.Luxury) { pointsPerDay 2; } var points numberOfDays*pointsPerDay; //调用数据服务存储客户获得的积分 _loyaltyDataService.AddPoints(agreement.Customer.Id,points); Console.WriteLine(Accrue Complete{0},DateTime.Now); } public void Redeem(Invoice invoice, int numberOfDays) { Console.WriteLine(Redeem:{0},DateTime.Now); Console.WriteLine(Invoice:{0},invoice.Id); var pointsPerDay 10; if (invoice.Vehicle.SizeSize.Luxury) { pointsPerDay 15; } var totalPoints pointsPerDay*numberOfDays; invoice.Discount numberOfDays*invoice.CostPerDay; _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints); Console.WriteLine(Redeem Complete:{0},DateTime.Now); }现在还不是很糟糕只不过在每个实现中添加了几行代码而已。咱们继续往下走防御性编程因为我们的业务逻辑没有对传入的参数进行控制因此必须要检查一下是否是最坏的情景。比如如果Accrue方法传入一个null会怎样我们的业务逻辑不能处理这个所以会抛异常但我们希望它能调用我们的API处理这个异常如果处理不了就提醒UI开发者或QA发生了一些错误的东西。这种哲学就叫防御性编程只是为了减少危险场景的风险。下面我们使用防御性编程检查传入参数为null的无效场景public void Accrue(RentalAgreement agreement) { //防御性编程 if (agreementnull) { throw new Exception(agreement为null); } //日志 Console.WriteLine(Accrue:{0},DateTime.Now); Console.WriteLine(Customer:{0},agreement.Customer.Id); Console.WriteLine(Vehicle:{0},agreement.Vehicle.Id); var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate); var numberOfDays (int)rentalTimeSpan.TotalDays; var pointsPerDay 1; if (agreement.Vehicle.Size Size.Luxury) { pointsPerDay 2; } var points numberOfDays*pointsPerDay; //调用数据服务存储客户获得的积分 _loyaltyDataService.AddPoints(agreement.Customer.Id,points); Console.WriteLine(Accrue Complete{0},DateTime.Now); }我们也可以检查RentalAgreement的属性但现在上面的就足够了。Redeem的实现也有相同的问题numberOfDays参数的值不能小于1Invoice参数也不能为null因此也必须使用防御性编程public void Redeem(Invoice invoice, int numberOfDays) { //防御性编程 if (invoicenull) { throw new Exception(invoice为null); } if (numberOfDays0) { throw new Exception(numberOfDays不能小于1); } //logging Console.WriteLine(Redeem:{0},DateTime.Now); Console.WriteLine(Invoice:{0},invoice.Id); var pointsPerDay 10; if (invoice.Vehicle.SizeSize.Luxury) { pointsPerDay 15; } var totalPoints pointsPerDay*numberOfDays; invoice.Discount numberOfDays*invoice.CostPerDay; _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints); Console.WriteLine(Redeem Complete:{0},DateTime.Now); }现在我们的代码开始变得具有防御性了如果在核心逻辑的控制之外发生了错误也不会影响到我们了。在添加了日志和防御性代码之后Accrue和Redeem方法开始变得有点长了也有点重复但继续看一下事务和重试逻辑。使用事务和重试如果我们使用了不止一个数据层操作为了使这些操作具有原子性那么事务是必须的。也就是说我们想要所有的数据层调用都成功提交要么都失败回滚。假设我们可以将事务放到业务逻辑层。假设底层的数据层会使用和.NET内置的事务类TransactionScope兼容的技术结合try/catch块我们可以给Accrue方法添加事务代码public void Accrue(RentalAgreement agreement) { //防御性编程 if (agreementnull) { throw new Exception(agreement为null); } //日志 Console.WriteLine(Accrue:{0},DateTime.Now); Console.WriteLine(Customer:{0},agreement.Customer.Id); Console.WriteLine(Vehicle:{0},agreement.Vehicle.Id); using (var tsnew TransactionScope())//开始一个新事务 { try { var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate); var numberOfDays (int)rentalTimeSpan.TotalDays; var pointsPerDay 1; if (agreement.Vehicle.Size Size.Luxury) { pointsPerDay 2; } var points numberOfDays * pointsPerDay; //调用数据服务存储客户获得的积分 _loyaltyDataService.AddPoints(agreement.Customer.Id, points); ts.Complete();//调用Complete方法表明事务成功提交 } catch (Exception ex) { throw;//没有调用Complete方法事务会回滚 } } Console.WriteLine(Accrue Complete{0},DateTime.Now); }记住只有调用了事务的Complete方法事务才会提交否则就会回滚。如果抛出了异常这里我们只是重新抛出相似地也可以在Redeem方法中使用TransactionScope,这里不再贴了请自行看源码。上面的代码开始变长、变丑了原始的业务逻辑代码周围包了很多和横切关注点有关的代码块logging防御性编程和事务代码。但是我们还没做完假设底层的数据持久层偶尔会出现高流量可能就会导致某些请求失败比如抛出超时异常。如果是那种情况执行几次重试会保持程序平滑运行尽管在高流量期间有点慢。通过在事务中放一个循环每次事务回滚时我们就增加重试次数一旦重试次数达到限制值我们就不管了如下public void Accrue(RentalAgreement agreement) { //防御性编程 if (agreementnull) { throw new Exception(agreement为null); } //日志 Console.WriteLine(Accrue:{0},DateTime.Now); Console.WriteLine(Customer:{0},agreement.Customer.Id); Console.WriteLine(Vehicle:{0},agreement.Vehicle.Id); using (var tsnew TransactionScope())//开始一个新事务 { var retries 3;//重试事务3次 var succeeded false; while (!succeeded)//一直循环直到成功 { try { var rentalTimeSpan agreement.EndDate.Subtract(agreement.StartDate); var numberOfDays (int)rentalTimeSpan.TotalDays; var pointsPerDay 1; if (agreement.Vehicle.Size Size.Luxury) { pointsPerDay 2; } var points numberOfDays * pointsPerDay; //调用数据服务存储客户获得的积分 _loyaltyDataService.AddPoints(agreement.Customer.Id, points); ts.Complete();//调用Complete方法表明事务成功提交 succeeded true;//成功后设置为true确保最后一次循环迭代 Console.WriteLine(Accrue Complete{0}, DateTime.Now);//这句移入try里 } catch { if (retries0) { retries--;//直到尝试完次数时才重抛异常 } else { throw;//没有调用Complete方法事务会回滚 } } } } }相似地我们也要在Redeem方法中添加这里不做了省略。问题越来越明显了横切关注点基本上占据了这个方法的一半代码。但是我们还没有做完我们需要讨论一下异常处理。处理异常前面不是添加了try/catch了么?难道还不够也许比如服务器离线了重试次数到达限制了异常还是会重抛出去如果是这种情况我们就需要在程序崩溃前处理这个异常。因此我们需要在防御性编程后再添加一个try/catch块包裹其他所有的代码如下