.Net中的AOP系列之构建一个汽车租赁应用
这一节会构建一个汽车租赁系统先是给定业务需求然后逐渐地添加代码来满足那些需求。一开始不使用任何AOP从零开始敲代码。业务需求是最重要的因此我们先做需求一旦满足了业务逻辑然后再覆盖非功能需求。最后尽可能地简化并重构代码不使用AOP来重构横切关注点。这些都完成之后就会转向一个应用生命周期的长尾阶段。软件很少是长期不变的新的功能需求和新发现的bugs。很少有软件的开发阶段会比生产阶段长这就意味着大多数软件的生命周期是维护阶段。一个维护困难或昂贵的应用会导致高代价或者低品质或两者都有最终形成一个大泥球。然后会使用PostSharp重构代码将各自的横切关注点分离到它们自己的类中。一旦重构完成你就会看到使用AOP的好处特别是添加更多功能时。开始一个新项目时间现在地点你公司汽车租赁服务相关的研发部的办公室人物你的技术团队或者只有你自己背景启动一个新的项目高大上一点叫做客户忠诚度系统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);}////// 发票实体///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。现在当累积积分和兑换积分时添加日志其余代码和之前的一样。////// 该方法包含了积分系统累积客户积分的逻辑和规则////// 租赁协议实体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 pointsPerDaynumberOfDays;invoice.Discount numberOfDaysinvoice.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”);}//loggingConsole.WriteLine(“Redeem:{0}”,DateTime.Now);Console.WriteLine(“Invoice:{0}”,invoice.Id);var pointsPerDay 10;if (invoice.Vehicle.SizeSize.Luxury){pointsPerDay 15;}var totalPoints pointsPerDaynumberOfDays;invoice.Discount numberOfDaysinvoice.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方法中添加这里不做了省略。问题越来越明显了

相关新闻

深入解析Tiva EPI高级配置:多设备总线管理与时序优化实战

深入解析Tiva EPI高级配置:多设备总线管理与时序优化实战

1. 项目概述:为什么需要深入理解EPI配置?在嵌入式系统开发中,尤其是基于像TI Tiva C系列这类高性能ARM Cortex-M微控制器的项目,我们常常会遇到一个核心需求:扩展。片上资源(如SRAM、Flash)总有…

2026/7/23 6:39:39 阅读更多 →
Unity高性能响应式编程:R3框架核心优势与实战应用指南

Unity高性能响应式编程:R3框架核心优势与实战应用指南

1. 项目概述:为什么Unity开发者需要关注R3?如果你在Unity项目里用过UniRx,或者对C#的LINQ和异步编程有感觉,那你肯定对“响应式编程”这个概念不陌生。简单说,就是把数据流和事件流当成一条可以观察的“河流”&#xf…

2026/7/23 6:39:39 阅读更多 →
软件开发中的错误堆叠与处理最佳实践

软件开发中的错误堆叠与处理最佳实践

1. 项目概述这个标题看起来像是键盘误触或系统错误产生的随机字符串,但我们可以从另一个角度来解读——把它视为一个关于"错误处理"的隐喻性项目。在软件开发领域,错误堆叠(error stacking)是一个值得深入探讨的话题。2. 错误堆叠现象解析2.1 …

2026/7/23 6:39:39 阅读更多 →

最新新闻

Draw.io or diagrams.net 使用方法

Draw.io or diagrams.net 使用方法

0 Preface/Foreword 在工作中,经常需要用到框图,流程图,时序图,等等,draw.io可以完成以上工作。 official website:draw.io 1 Usage 1.1 VS code插件 draw.io可以扩展到VS code工具中。

2026/7/23 18:14:28 阅读更多 →
如何在OpenWRT软路由系统部署uhttpd搭建web服务器实现远程访问——“cpolar内网穿透”

如何在OpenWRT软路由系统部署uhttpd搭建web服务器实现远程访问——“cpolar内网穿透”

文章目录前言1. 检查uhttpd安装2. 部署web站点3. 安装cpolar内网穿透4. 配置远程访问地址5. 配置固定远程地址前言 uhttpd 是 OpenWrt/LuCI 开发者从零开始编写的 Web 服务器,目的是成为优秀稳定的、适合嵌入式设备的轻量级任务的 HTTP 服务器,并且和 O…

2026/7/23 18:14:28 阅读更多 →
DefaultCPUAllocator: not enough memory: you tried to allocate 4200595456 bytes

DefaultCPUAllocator: not enough memory: you tried to allocate 4200595456 bytes

DefaultCPUAllocator: not enough memory: you tried to allocate 4200595456 bytes报错:DefaultCPUAllocator: not enough memory: you tried to allocate 4200595456 bytes程序加载到 text_encoder(文本编码器) 阶段,需要一次性…

2026/7/23 18:14:27 阅读更多 →
docker中终端用户名显示为淡蓝色

docker中终端用户名显示为淡蓝色

问题 一般情况下默认进入docker后,用户名和主机名都是白色的,看着不舒服,因此想将用户名和主机名改为自己定义的颜色 解决方法 修改~/.lbashrc,修改PS1的值即可.淡蓝色为: PS1${debian_chroot:($debian_chroot)}\[\033[01;36m\]\u…

2026/7/23 18:14:27 阅读更多 →
一篇文章彻底搞懂 MySQL 和 Redis:原理、区别、项目用法全解析(建议收藏)

一篇文章彻底搞懂 MySQL 和 Redis:原理、区别、项目用法全解析(建议收藏)

一篇文章彻底搞懂 MySQL 和 Redis:原理、区别、项目用法全解析(建议收藏) 在现代后端开发中,MySQL 和 Redis 是两种最常用的数据存储系统。很多开发者虽然会使用它们,但对底层原理和适用场景的理解往往比较模糊。本文将…

2026/7/23 18:14:27 阅读更多 →
Mini小主机All-in-one搭建教程2-安装Openwrt软路由系统

Mini小主机All-in-one搭建教程2-安装Openwrt软路由系统

Mini小主机All-in-one搭建教程2-安装Openwrt软路由系统 硬件介绍 在狗东买的 极摩客M2 到手价是2799元 具体配置如下: 酷睿英特尔11代标压ai7 11390H 64G1TB固态。 以下是安装Openwrt软路由系统的教程。 安装Openwrt软路由系统 下载镜像包 首先下载软路由的懒…

2026/7/23 18:13:27 阅读更多 →

日新闻

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

更多请点击: https://intelliparadigm.com 第一章:从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表) 当AI副业主理人不再仅满足于单次服务交付,而是主动构建可复用、可裂变、可…

2026/7/23 0:00:25 阅读更多 →
AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

更多请点击: https://codechina.net 第一章:AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析 在对2,346篇跨行业AI生成文案的A/B测试数据进行聚类分析后,我们发现&#xff1…

2026/7/23 0:01:26 阅读更多 →
Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具 【免费下载链接】chitchatter Secure peer-to-peer chat that is serverless, decentralized, and ephemeral 项目地址: https://gitcode.com/gh_mirrors/ch/chitchatter Chitchatter是一款革命性的安…

2026/7/23 0:01:26 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 8:58:19 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 19:43:43 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 17:49:47 阅读更多 →

月新闻