前面不是添加了try/catch了么?难道还不够?也许!比如,服务器离线了,重试次数到达限制了,异常还是会重抛出去,如果是这种情况,我们就需要在程序崩溃前处理这个异常。
因此我们需要在防御性编程后再添加一个try/catch块包裹其他所有的代码如下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);try{using (var ts new 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 (retries 0){retries–;//直到尝试完次数时才重抛异常}else{throw;//没有调用Complete方法事务会回滚}} } } } catch (Exception ex) { if (!ExceptionHelper.Handle(ex))//如果没有处理异常继续重抛 { throw ex; } }}ExceptionHelper是自定义的异常处理帮助类覆盖了个别异常的处理如果是没有覆盖的异常我们可能需要记录日志并告诉客户出现了什么异常。相似地Redeem方法也要做相同的处理此处省略。此时我们已经实现了所有非功能需求logging防御性编程事务重试和异常处理。将这些处理横切关注点的代码添加到原始的Accrue和Redeem方法中使得它们膨胀成巨大的方法。现在代码可以去生产环境或更可能去QA/预发布环境但是这代码太糟糕了你可能在想这个描述有点过了并不是所有的横切关注点都是必须的是的你可能大多数情况只需要一两个横切关注点一些关注点可以移到数据层或UI层。但这里要说明的道理是横切关注点可以使你的代码变杂乱使得代码更难阅读、维护和调试。不使用AOP重构是时候整理下代码了因为Accrue和Redeem方法中有很多重复代码我们可以把这些代码放到它们自己的类或方法中。一种选择是将所有的非功能关注点重构到静态方法中这是个馊主意因为这会将业务逻辑紧耦合到非功能关注点代码中虽然使方法看上去更短更可读了但仍然留下了方法做的事情太多的问题。你也可以使用DI策略将所有的logging防御性编程和其他服务传给LoyaltyAccrualService和LoyaltyRedemptionService的构造函数public class LoyalRedemptionServiceRefactored:ILoyaltyRedemptionService{private readonly ILoyaltyDataService _loyaltyDataService;private readonly IExceptionHandler _exceptionHandler;//异常处理接口private readonly ITransactionManager _transactionManager;//事务管理者public LoyalRedemptionServiceRefactored(ILoyaltyDataService loyaltyDataService, IExceptionHandler exceptionHandler, ITransactionManager transactionManager) { _loyaltyDataService loyaltyDataService; _exceptionHandler exceptionHandler;//通过依赖注入传入 _transactionManager transactionManager; } 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); _exceptionHandler.Wrapper(() { _transactionManager.Wrapper(() { var pointsPerDay 10; if (invoice.Vehicle.SizeSize.Luxury) { pointsPerDay 15; } var totalPoints numberOfDays*pointsPerDay; _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints); invoice.Discount numberOfDays*invoice.CostPerDay; // logging Console.WriteLine(Redeem complete: {0},DateTime.Now); }); }); }}上面是重构过的版本IExceptionHandler等的代码没有贴出来请查看源码这个版本比之前的好多了。我将异常处理代码和事务/重试代码分别放到了IExceptionHandler和ITransactionManager中这种设计有它的优势一是它把那些代码段放到了他们自己的类中以后可以重用二是通过减少了横切关注点的噪音使得代码阅读更容易。当然Accrue方法也可以重构成这样此处略过。重构之后代码和最原始的状态差不多了。但是构造函数好像太庞大了,也就是依赖太多了实际上这里可以优化一下往下看。Code Smells【代码异味】代码异味是一个俚语本质上它不是bug但它暗示了可能会存在一个问题。就像冰箱里的难闻气味表明背后有腐烂的肉一样代码异味可能指示了当前的设计不太好应该被重构。详细了解代码意味可以点击阅读。我们可以将异常处理和事务管理合并成一个服务如下public interface ITransactionManager2{void Wrapper(Action method);}public class TransactionManager2 : ITransactionManager2{public void Wrapper(Action method){using (var tsnew TransactionScope()){var retires 3;var succeeded false;while (!succeeded){try{method();ts.Complete();succeeded true;}catch (Exception ex){if (retires 0)retires–;else{if (!ExceptionHelper.Handle(ex))throw;}}}}}}处理注入依赖过多的另一种方法是将所有的服务移到一个聚合服务或者门面服务即使用门面模式将所有的小服务组合成一个服务来组织这些小服务我们这个例子中TransactionManager和ExceptionHandler服务是独立的但是可以使用第三个门面类来组织它们的使用。门面模式 The Facade Pattern门面模式为更大的或者更复杂的代码段提供了一个简化接口比如一个提供了许多方法和选项的服务类可以放到一个门面接口中这样就可以通过限制选项或者提供简化方法的子集来降低复杂度。public interface ITransactionFacade{void Wrapper(Action method);}public class TransactionFacade : ITransactionFacade{private readonly ITransactionManager _transactionManager;private readonly IExceptionHandler _exceptionHandler;public TransactionFacade(ITransactionManager transactionManager, IExceptionHandler exceptionHandler) { _transactionManager transactionManager; _exceptionHandler exceptionHandler; } public void Wrapper(Action method) { _exceptionHandler.Wrapper(() _transactionManager.Wrapper(method) ); }}这样修改后Accrual和Redemption服务方法中的Wrapper样板代码就减少了很多更干净了。但是还存在防御编程和logging的问题。使用装饰器模式重构不使用AOP重构代码的另一种方式是使用装饰器模式或代理器模式。剧透一下装饰器/代理器模式只是AOP的一种简单形式。试想如果有一种方法可以将上面所有的方法合起来成为一种方法使得代码回到最初始状态只有业务逻辑那将是最好的了。那就读起来最简单有最少的构造函数注入的服务。当业务逻辑变化时我们也不必担心忘记或忽略了这些横切关注点从而减少了变更的代价。变更的代价软件工程中不变的东西就是变化需求变了业务规则变了技术变了。业务逻辑或需求的任何变更对处理原始版本的业务逻辑都是挑战性的在代码重构之前。需求变更因为许多原因需求会变更。需求一开始可能是很模糊的但是随着软件开始成型就会变得更加具体。项目经理等人就会改变想法对他们来说看似很小的变化可能在代码中意味着很大的不同。虽然我们都知道需求会变是个真理并且也已经反复见证了但仍然在犯一个错那就是编码时好像什么都不会改变。作为一个好的开发者不仅要接受需求的变化还要期待需求变化。项目的大小确实很重要如果你是一个人编写一个简单的软件比如一个具有两三个表单和许多静态内容的网站那么变更的代价可能很低因为改动的地方很少。方法签名变更给方法添加或移除参数就会导致方法签名变更。如果移除了一个参数就必须移除该参数的防御性编程否则项目编译不通过。如果修改了一个参数的类型那么防御性编程边界情况也会改变。更危险的是如果添加了一个参数就必须添加该参数的防御性编程不幸的似乎编译器不会帮你做这个自己必须要记得做这件事。看一下之前的Accrue方法签名改变的地方会立即影响防御编程和日志记录如下public void Accrue(RentalAgreement agreement) {// defensive programmingif(agreement null) throw new ArgumentNullException(“agreement”);// loggingConsole.WriteLine(“Accrue: {0}”, DateTime.Now);Console.WriteLine(“Customer: {0}”, agreement.Customer.Id);Console.WriteLine(“Vehicle: {0}”, agreement.Vehicle.Id);// … snip …// loggingConsole.WriteLine(“Accrue complete: {0}”, DateTime.Now);}如果参数名从agreement变成rentalAgreement,那么必须记得更改ArgumentNullException的构造函数的字符串参数。如果方法名本身变了也必须更改logging中记录的字符串方法名。虽然有很多重构工具可以辅助如Resharp,但是其他的还要依赖你自己和团队的警惕。团队开发一个人开发就算了。假设有个新的需求ILoyaltyAccureService接口需要添加一个新的方法也许这个任务会派给其他队友并且这个队友实现了业务逻辑并完成了任务。不幸地是这个队友忘记了使用TransactionFacade的Wrapper方法他的代码通过了UT然后交给了QA。如果这是一个敏捷项目这也许不是大问题QA会捕捉到这个问题并立即把这个问题报告给你。在一个瀑布项目中QA可能在几个月之后才会发现这个bug。几个月后你可能也不记得造成这个bug的原因了。就好像你是团队中的新员工一样。最糟糕的情况它可能通过了QA假设的异常或重试条件不是必要的或者没有被注意到这样代码就没有经过防御性编程、logging、事务等等进入了生产环境这样迟早出问题使用AOP重构再次重构代码这次使用AOP使用NuGet添加Postsharp到项目CarRental.Core中关于如何添加请查看上一篇文章。开发简单、独立的logging先来重构一个简单的横切关注点logging。当方法调用时会记录方法名和时间戳。创建一个日志切面类继承自OnMethodBoundaryAspect它允许我们在方法的边界插入代码[Serializable]public class LoggingAspect:OnMethodBoundaryAspect{public override void OnEntry(MethodExecutionArgs args){Console.WriteLine(“{0}:{1}”,args.Method.Name,DateTime.Now);}public override void OnSuccess(MethodExecutionArgs args) { Console.WriteLine({0} complete:{1},args.Method.Name,DateTime.Now); }}注意我们可以通过MethodExecutionArgs参数获得方法名因此这个切面可以c重复使用可给Accure和Redeem方法使用public class LoyaltyAccrualService:ILoyaltyAccrualService{[LoggingAspect]public void Accrue(RentalAgreement agreement){//…}}public class LoyalRedemptionService:ILoyaltyRedemptionService{[LoggingAspect]public void Redeem(Invoice invoice, int numberOfDays){//…}}现在就可以从这些方法中移除logging代码了。除此之外我们还没有打印传入参数的Id比如Customer.Id。有了Postsharp,我们可以取到所有的传入参数但为了取到Id,必须还得做点事情。public override void OnEntry(MethodExecutionArgs args){Console.WriteLine(“{0}:{1}”,args.Method.Name,DateTime.Now);foreach (var argument in args.Arguments)//遍历方法的参数{if (argument.GetType()typeof(RentalAgreement)){Console.WriteLine(“Customer:{0}”, ((RentalAgreement)argument).Customer.Id);Console.WriteLine(“Vehicle:{0}”, ((RentalAgreement)argument).Vehicle.Id);}if (argument.GetType()typeof(Invoice)){Console.WriteLine(“Invoice:{0}”,((Invoice)argument).Id);}}}就这个例子来说这样没问题了但是对于一个大一点的应用可能会有几十个甚至几百个不同的类型如果需求是记录实体Id和信息那么可以在实体上使用一个公共接口或基类。比如如果Invoice和RentalAgreement都实现了ILoggable接口该接口具有一个方法string LogInfo(),代码可以这样写:public override void OnEntry(MethodExecutionArgs args){Console.WriteLine(“{0}:{1}”,args.Method.Name,DateTime.Now);foreach (var argument in args.Arguments)//遍历方法的参数{if (argument!null){if (typeof(ILoggable).IsAssignableFrom(argument.GetType())){Console.WriteLine((ILoggable)argument.LogInfo());}}} }现在Accure和Redeem方法开始收缩了因为我们将logging功能移到了它自己的类日志切面中去了。重构防御性编程下面还是使用OnMethodBoundaryAspect基类重构防御性编程确保没有参数为null以及所有的int参数不为0或负数[Serializable]public class DefensiveProgramming:OnMethodBoundaryAspect{public override void OnEntry(MethodExecutionArgs args){var parameters args.Method.GetParameters();//获取形参var arguments args.Arguments;//获取实参for (int i 0; i arguments.Count; i){if (arguments[i]null){throw new ArgumentNullException(parameters[i].Name);}if (arguments[i] is int(int)arguments[i]0){throw new ArgumentException(“参数非法”,parameters[i].Name);}}}}首先检查实参是否为null之后再判断参数是否是整型并且是否合法。如果不处理这些事情非法值会使得程序崩溃但这里处理之后我们可以看到崩溃的确定原因ArgumentNullException或ArgumentException 的异常信息。同时这个类没有直接耦合任何参数类型或服务类这意味着可以重复使用在多个服务中。[LoggingAspect][DefensiveProgramming]public void Accrue(RentalAgreement agreement){//…略}[LoggingAspect][DefensiveProgramming]public void Redeem(Invoice invoice, int numberOfDays)

相关新闻

TRAE CUE:AI驱动的智能代码补全工具解析

TRAE CUE:AI驱动的智能代码补全工具解析

1. TRAE CUE:重新定义AI驱动的代码补全体验第一次接触TRAE CUE时,我正在重构一个遗留的Java项目。当我在修改一个复杂DTO类的字段命名时,CUE不仅实时提供了字段命名的补全建议,还自动高亮了项目中所有需要同步修改的引用点——这个…

2026/10/2 7:44:50 阅读更多 →
存储技术演进与核心架构解析

存储技术演进与核心架构解析

1. 存储技术的前世今生2003年,我第一次接触企业级存储系统时,被机房那排闪着蓝光的磁盘阵列震撼到了。那时的存储设备还像衣柜般笨重,需要专门的存储管理员小心翼翼地伺候。二十年后的今天,存储技术已经发生了翻天覆地的变化&…

2026/9/28 20:13:19 阅读更多 →
ShaderGraph屏幕节点深度解析:从原理到实战应用

ShaderGraph屏幕节点深度解析:从原理到实战应用

1. 屏幕节点(Screen Node)的核心价值与设计思路在ShaderGraph的世界里,屏幕节点(Screen Node)是一个看似基础,实则蕴含着巨大潜力的“窗口”。它不像那些花哨的扭曲或发光节点那样引人注目,但却…

2026/10/15 7:12:48 阅读更多 →

最新新闻

ponytail:把终端命令变成可复用技能的高效自动化工具

ponytail:把终端命令变成可复用技能的高效自动化工具

ponytail。第一次在同事的终端里看到这个词,我还以为是某个桌面对弈壁纸的花名。直到他现场敲了两行命令,把原本我得翻半天历史记录、拼接七八条命令才能完成的日志分析,变成了问候式的一行pt log --path access.log --top 5,我才…

2026/10/15 13:46:16 阅读更多 →
TileLang是什么?当前不存在的领域专用语言解析

TileLang是什么?当前不存在的领域专用语言解析

我无法根据当前输入生成符合要求的博文。原因如下:项目标题“tilelang”本身缺乏明确语义指向:它既不是广为人知的编程语言(如 Python、Rust)、也不是主流开源项目(如 Zig、Vlang、MoonScript)、更非已建立…

2026/10/15 13:46:16 阅读更多 →
Tilelang不存在:编程语言术语辨伪与技术查证方法

Tilelang不存在:编程语言术语辨伪与技术查证方法

我无法生成关于“tilelang”的博文内容。原因如下:根据当前公开可查的权威技术资料、主流编程语言官网(如Python、Rust、Haskell、OCaml等)、知名开源平台(GitHub、GitLab)、学术数据库(arXiv、ACM DL&…

2026/10/15 13:46:16 阅读更多 →
机器人动力学约束路径规划:从几何路径到物理可行轨迹

机器人动力学约束路径规划:从几何路径到物理可行轨迹

1. 为什么“能走通”的路径,在真实机器人上反而会翻车?“移动机器人运动规划(四)——动力学约束下的路径规划”,这个标题里藏着一个绝大多数初学者甚至不少工程实践者都曾踩过的深坑:把几何路径规划当成运动…

2026/10/15 13:46:16 阅读更多 →
双芯光纤SPR传感技术及COMSOL仿真优化

双芯光纤SPR传感技术及COMSOL仿真优化

1. 项目背景与核心价值双芯光纤表面等离子体共振(SPR)传感技术是近年来光纤传感领域的重要突破方向。这种将两个纤芯集成在单根光纤中的特殊结构,相比传统单芯光纤SPR传感器具有更高的灵敏度和更好的稳定性。我在实际研究中发现,当…

2026/10/15 13:46:16 阅读更多 →
第一次做 GEO 要几步?从注册到第一篇内容被收录的启动路径

第一次做 GEO 要几步?从注册到第一篇内容被收录的启动路径

结论先放这儿:6 步。注册 → 搭知识库 → 生成用户问题 → 攒标题 → 补正文 → 跑监测。前 4 步搭台子,第 5 步出第一篇成稿,第 6 步验证它有没有被 AI 读到。先说这篇是写给谁的 如果你刚注册完 GEOchat,打开后台看到「知识库、…

2026/10/15 13:45:15 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →