开始一个新项目
没有AOP的生活
变更的代价
使用AOP重构
本系列的源码本人已托管于Coding上:点击查看。
本系列的实验环境:VS 2013 Update 5(建议最好使用集成了Nuget的VS版本,VS Express版也够用),安装了PostSharp。
这篇博客覆盖的内容包括:
这一节会构建一个汽车租赁系统,先是给定业务需求,然后逐渐地添加代码来满足那些需求。 一开始不使用任何AOP,从零开始敲代码。业务需求是最重要的,因此我们先做需求,一旦满足了业务逻辑,然后再覆盖非功能需求。最后,尽可能地简化并重构代码,不使用AOP来重构横切关注点。 这些都完成之后,就会转向一个应用生命周期的长尾阶段。软件很少是长期不变的:新的功能需求和新发现的bugs。很少有软件的开发阶段会比生产阶段长,这就意味着大多数软件的生命周期是维护阶段。一个维护困难或昂贵的应用会导致高代价或者低品质(或两者都有),最终形成一个大泥球。 然后,会使用PostSharp重构代码,将各自的横切关注点分离到它们自己的类中。一旦重构完成,你就会看到使用AOP的好处,特别是添加更多功能时。
时间:现在 地点:你公司(汽车租赁服务相关)的研发部的办公室 人物:你的技术团队或者只有你自己 背景:启动一个新的项目,高大上一点,叫做客户忠诚度系统,low一点,叫做客户积分程序。目的是为了增加销售,奖励那些经常购买服务的客户。比如,客户今天租赁了一辆车,那么他就会获得积分,积分累积多了之后,以后可以用于抵消一部分租赁费用或其他费用。
假设有一个基本的三层架构,如下图。我们会从应用到这个积分系统的核心业务逻辑层着手编写代码,持久化层会跟踪客户的忠诚度积分,业务逻辑层供所有的UI层使用:网站,APP和店员使用的桌面端。
这一篇,我们主要看一下中间一层的业务逻辑层。我们可以假设持久化层已经实现了,还要假设一旦业务逻辑实现了,UI也就实现了。
项目经理和利益相关人(比如销售和市场)确定了下图的业务需求,你已经确定了两个主要的需求集:累积积分和使用累积的积分 兑换奖励。
现在的业务需求就是:客户每租一天普通型车辆,累积一积分,豪华型或者大型车辆,每天两积分。这些积分会在他们支付之后并返还了车以后会增加到他们的账户中。一旦客户累积了10积分,那么就可以使用这些积分兑换奖励了,具体兑换规则见上图。 这就是所有业务规则,但是在实现之前还是得和销售和市场确定好:因为他们将来肯定还会更改或者添加一些东西。
在给项目经理估算时间和花销之前,你有自己必须要解决的技术关注点。 第一,需要记录日志。如果客户的积分累积得不对(累积少了),那么他们会生气的,因此必须确保记录了业务逻辑处理的一切(尤其是起初阶段)。 第二,因为业务逻辑代码会被多个UI应用使用,要确保传入业务层的数据是合法的,你的队友可能会在UI里写入一些集成代码,因此,必须编写防御性代码来检查无意义的边缘情况和参数。 第三,还是因为业务逻辑代码会被多个UI应用使用,这些UI可能会使用不同类型的连接(缓慢的移动手机的连接,国外浏览器访问等等),你需要采用事务和重试逻辑来确保维护数据集成以及给用户提供一个愉快的体验。 最后,总有意外会发生,你可能不知道此时你会使用何种类型的持久化,所以需要某种方法处理异常(很可能是记录日志)。
将评估提交给项目经理之后,所有的批准和文件也已经签署了,现在就可以开始了。
新建一个解决方案,名叫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
{
Compact=0,
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 name="agreement">租赁协议实体</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);
}
}
到这里,已经完成了累积积分的业务逻辑!现在回到客户关心的问题上,如何兑换积分?创建一个接口ILoyaltyRedemptionService
:
public 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.Size>=Size.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 dataService=new FakeLoyalDataService();//这里使用的数据库服务是伪造的
var service=new LoyaltyAccrualService(dataService); var agreement=new 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 name="agreement">租赁协议实体</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.Size>=Size.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 (agreement==null)
{ 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
参数的值不能小于1,Invoice
参数也不能为null,因此也必须使用防御性编程:
public void Redeem(Invoice invoice, int numberOfDays) { //防御性编程
if (invoice==null)
{ throw new Exception("invoice为null!");
} if (numberOfDays<=0)
{ throw new Exception("numberOfDays不能小于1!");
} //logging
Console.WriteLine("Redeem:{0}",DateTime.Now);
Console.WriteLine("Invoice:{0}",invoice.Id); var pointsPerDay = 10; if (invoice.Vehicle.Size>=Size.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 (agreement==null)
{ 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 ts=new 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,防御性编程和事务代码。
但是我们还没做完,假设底层的数据持久层偶尔会出现高流量,可能就会导致某些请求失败(比如,抛出超时异常)。如果是那种情况,执行几次重试会保持程序平滑运行(尽管在高流量期间有点慢)。通过在事务中放一个循环,每次事务回滚时,我们就增加重试次数,一旦重试次数达到限制值,我们就不管了