门类介绍(设计模式)
如果您有抚养负担怎么办?
检查我们是否可以批准保险索赔
外部解决方案
创建一个门类
一些注意事项
您的想法是什么?
保持联系
导航您的软件开发职业通讯
最初发布在我的技术博客上
你听说过保护条款吗?Steve Smith 在他的每周开发技巧中讨论过它。
今天我想向大家展示一种我一直在使用的技术,它类似于保护子句模式,但用于更高级的场景。这些场景包括由于外部依赖关系或保护子句中所需的复杂逻辑而出现问题的情况。
使用保护子句,我们将采用具有嵌套条件逻辑的代码,如下所示:
if(order != null)
{
if(order.Items != null)
{
this.PlaceOrder(order);
}
else {
throw new ArgumentNullException("Order is null");
}
}
else {
throw new ArgumentNullException("Order is null");
}
然后我们将反转条件逻辑并尝试“快速失败”:
if(order?.Items == null)
{
throw new ArgumentNullException("Order is null");
}
this.PlaceOrder(order);
注意到了吗,我们一开始就会尝试让该方法失败?这就是我所说的“快速失败”。
接下来,我们可以创建一个可重用的方法:
public static void IsNullGuard(this object me, string message)
{
if(me == null)
{
throw new ArgumentNullException(message);
}
}
最后,我们可以在任何需要的地方使用这个保护子句:
order?.Items.IsNullGuard("Order is null");
this.PlaceOrder(order);
这将使我们的代码更加清晰,避免任何嵌套条件,并且更容易推理!
如果您有抚养负担怎么办?
我喜欢这个图案。
但是,有时你可能会尝试构建一种具有某些外部依赖关系的保护子句,例如存储库或HttpClient
。实际保护的逻辑可能也相当复杂。
示例可能包括确定:
- 用户具有适当的权限来查看系统中的某些资源
- 潜在订单可以被购买(在简单的零售系统中)
- 保险索赔能够被批准
- ETC。
在这些情况下,我喜欢使用我称之为“门类”的东西。它们就像保护条款,但它们是类……想想看。
可以把它们想象成系统中每个请求都必须经过的一系列门(就像中间件一样)。如果其中任何一个发生故障,门就会关闭,请求就无法继续进行。
让我告诉你我的意思。
检查我们是否可以批准保险索赔
假设我们正在构建保险处理系统的一部分。
接下来,我们必须检查该索赔是否能够被批准,如果可以,则批准。
这是我们的用例(清洁架构),或者有些人可能知道的,我们的命令(CQRS):
public class ApproveInsuranceClaimCommand : IUseCase
{
private readonly IInsuranceClaimRepository _claimRepo;
private readonly IUserRepository _userRepo;
public ApproveInsuranceClaimCommand(
IInsuranceClaimRepository claimRepo,
IUserRepository userRepo
)
{
this._claimRepo = claimRepo;
this._userRepo = userRepo;
}
public async Task Handle(Guid claimId, int approvingUserId)
{
User user = await this._userRepo.Find(approvingUserId);
// 1. Figure out if the user has permission to approve this...
InsuranceClaim claim = await this._claimRepo.Find(claimId);
// 2. Figure out if the claim is approvable...
claim.Approve();
await this._claimRepo.Update(claim);
}
}
对于我所发表的评论中的逻辑,如果我们需要更多的存储库来进行这些检查怎么办?
此外,如果我们系统中的其他用例需要进行相同的检查怎么办?
也许我们还有另一个用例,ApproveOnHoldInsuranceClaimCommand
它将批准一项保险索赔,由于某种原因,该索赔被搁置,直到客户提供进一步的文件?
或者,也许在其他用例中我们需要检查用户是否有权更改声明?
这将导致代码混乱和大量的复制和粘贴!
外部解决方案
就像保护子句重构模式一样,为什么我们不做同样的事情,而是将每个保护子句转换为一个全新的类?
好处是我们可以使用依赖注入来注入只有每个门类才需要的任何依赖项,如存储库HttpClient
等。
现在,我们的用例可能看起来像这样(请记住每个门类可能在内部执行一些复杂的逻辑):
public class ApproveInsuranceClaimCommand : IUseCase
{
private readonly IInsuranceClaimRepository _claimRepo;
private readonly CanUserApproveInsuranceClaimGate _canUserApprove;
private readonly CanInsuranceClaimBeApprovedGate _claimCanBeApproved;
public ApproveInsuranceClaimCommand(
IInsuranceClaimRepository claimRepo
CanUserApproveInsuranceClaimGate canUserApprove,
CanInsuranceClaimBeApprovedGate claimCanBeApproved
)
{
this._claimRepo = claimRepo;
this._canUserApprove = canUserApprove;
this._claimCanBeApproved = claimCanBeApproved;
}
public async Task Handle(Guid claimId, int approvingUserId)
{
await this._canUserApprove.Invoke(approvingUserId);
InsuranceClaim claim = await this._claimRepo.Find(claimId);
await this._claimCanBeApproved.Invoke(claim);
claim.Approve();
await this._claimRepo.Update(claim);
}
}
请注意,不再需要,因为它将由门类(和 DI)IUserRepository
处理。CanUserApproveInsuranceClaimGate
注意:为什么我没有为每个门类创建一个接口?只是为了简单起见。但是,是的,通过使用接口而不是具体的类,您可以更轻松地模拟它们以进行测试。
创建一个门类
让我们看看如何构建CanInsuranceClaimBeApprovedGate
门类:
public class CanInsuranceClaimBeApprovedGate
{
private readonly IInsuranceClaimAdjusterRepository _adjusterRepo;
private readonly IInsuranceClaimLegalOfficeRepository _legalRepo;
public CanInsuranceClaimBeApprovedGate(
IInsuranceClaimAdjusterRepository adjusterRepo,
IInsuranceClaimLegalOfficeRepository legalRepo
)
{
this._adjusterRepo = adjusterRepo;
this._legalRepo = legalRepo;
}
public async Task Invoke(InsuranceClaim claim)
{
// Do some crazy logic with the data returned from each repository!
// On failure, throw a general gate type exception that can be handled
// by middleware or a global error handler somewhere at the top of your stack.
throw new GateFailureException("Insurance claim cannot be approved.")
}
}
每个门类要么成功,要么失败。
如果失败,它会抛出一个异常,并被捕获到堆栈中。在 Web 应用程序中,通常有一些全局异常处理程序或中间件,可以将它们转换为特定的 HTTP 错误响应等。
如果我们确实需要在其他地方使用此逻辑,如上所述,则无需重新导入此逻辑所需的所有依赖项。我们可以直接使用门类,并让 DI 机制为我们插入所有依赖项。
一些注意事项
值得一提的是,在某些情况下,您的用例和门类可能需要调用相同的存储库方法。您不希望两次获取该数据(一次在门类中,一次在用例中)。
在这种情况下,有办法解决。
一种是使用装饰器模式构建缓存存储库。
您可以将其设置为作用域依赖项(在 .NET Core 中),这样缓存的数据将仅在 HTTP 请求的生命周期内缓存。或者,您也可以设置缓存的超时时间。
另一种方法是允许用例将原始数据作为依赖项注入到门类中。
无论如何,这种模式对于使您的代码更易于测试、使用和维护非常有帮助!
您的想法是什么?
你以前见过这种技术吗?有什么想法吗?我很想听听!
保持联系
不要忘记通过以下方式与我联系:
您也可以通过我的网站www.jamesmichaelhickey.com找到我。
导航您的软件开发职业通讯
一封电子邮件简报,助您提升软件开发职业水平!您是否想过:
✔ 软件开发人员通常经历哪些阶段?
✔ 我如何知道自己处于哪个阶段?如何进入下一个阶段?
✔ 什么是技术领导者?如何成为技术领导者?
✔ 有人愿意陪伴我并解答我的疑问吗?
听起来很有趣?加入社区吧!
鏂囩珷鏉ユ簮锛�https://dev.to/jamesmh/introducing-gate-classes-4j6a