c# - 我如何处理可能成为策略模式解决方案候选的两种情况?
问题描述
我正在设计一个客户端,它将根据某些输入调用方法。我将发送一个计费系统枚举并调用一个端点来确定哪个计费系统适合现有患者。获得计费系统后,我必须检查我需要执行的操作类型,并根据计费系统进行 API 调用。
例如,如果我需要更新患者记录并且患者在 BillingSystemA 中,我需要为 BillingSystemA 调用 API 的基于 PUT 的方法。
我需要为每个计费系统提供 CRUD 方法。
在两种计费系统之间进行选择并考虑到未来的增长让我认为该战略模式非常适合。策略似乎适用于计费系统,但 CRUD 操作呢?
我有一个包含、和方法的BillingStrategy
抽象类,但我需要这些方法来处理各种类型。我可以只使方法通用,或者我需要策略中的策略来管理它吗?我已经将自己分析到一个角落,可以使用一些建议。Create
Update
Get
Delete
T Create<T>
bool Update<T>
解决方案
这是一个粗略的说明。我发明了很多细节,名字都不是很好。我倾向于在重构时重新审视名称。重点是说明我们如何将问题分解为多个部分。
这假设有 和 的类Patient
和Treatment
的枚举InsuranceType
。目标是为患者开具治疗费用,并根据患者的保险确定将账单寄往何处。
这是一个类:
public class PatientBilling
{
private readonly IBillingHandlerByInsuranceSelector _billingHandlerSelector;
private readonly IBillingHandler _directPatientBilling;
public PatientBilling(
IBillingHandlerByInsuranceSelector billingHandlerSelector,
IBillingHandler directPatientBilling)
{
_billingHandlerSelector = billingHandlerSelector;
_directPatientBilling = directPatientBilling;
}
public void BillPatientForTreatment(Patient patient, Treatment treatment)
{
var billingHandler = _billingHandlerSelector.GetBillingHandler(patient.Insurance);
var result = billingHandler.BillSomeone(patient, treatment);
if (!result.Accepted)
{
_directPatientBilling.BillSomeone(patient, treatment);
}
}
}
和一些接口:
public interface IBillingHandler
{
BillSomeoneResult BillSomeone(Patient patient, Treatment treatment);
}
public interface IBillingHandlerByInsuranceSelector
{
IBillingHandler GetBillingHandler(InsuranceType insurance);
}
如您所见,这将严重依赖依赖注入。这个类很简单,因为它对不同的保险类型一无所知。
它所做的只是
- 根据保险类型选择计费处理程序
- 尝试将账单提交给保险公司
- 如果被拒绝,请向患者收费
它不知道也不关心如何实施任何计费。它可以是数据库调用、API 调用或其他任何东西。这使得这个类很容易阅读和测试。我们已经推迟了与此类无关的任何内容。这将更容易一次解决未来的问题。
的实现IBillingHandlerByInsuranceSelector
可以是一个抽象工厂,它将IBillingHandler
根据患者的保险创建并返回正确的实现。(我对此进行了掩饰,但有很多关于如何使用依赖注入容器创建抽象工厂的信息。)
从某种意义上说,我们可以说这个问题的第一部分已经解决(尽管我们可能会进行更多重构。)原因是我们可以为它编写单元测试,以及任何特定于一种保险类型的工作或者另一个人将在不同的班级。
接下来我们可以编写那些特定于保险的实现。假设其中一种保险类型是WhyCo,现在我们需要IBillingHandler
为它们创建一个。我们基本上将重复相同的过程。
为了便于说明,假设向WhyCo 提交账单分两步完成。首先,我们必须提出检查资格的请求,然后我们必须提交账单。也许其他保险 API 可以一步完成。没关系,因为没有两个实现必须有任何共同点。他们只是实现接口。
在这一点上,我们正在处理一家特定保险公司的细节,因此我们需要在这里的某个地方将我们的Patient
信息Treatment
转换为他们期望接收的任何数据。
public class WhyCoBillingHandler : IBillingHandler
{
private readonly IWhyCoService _whyCoService;
public WhyCoBillingHandler(IWhyCoService whyCoService)
{
_whyCoService = whyCoService;
}
public BillSomeoneResult BillSomeone(Patient patient, Treatment treatment)
{
// populate this from the patient and treatment
WhyCoEligibilityRequest eligibilityRequest = ...;
var elibility = _whyCoService.CheckEligibility(eligibilityRequest);
if(!elibility.IsEligible)
return new BillSomeoneResult(false, elibility.Reason);
// create the bill
WhyCoBillSubmission bill = ...;
_whyCoService.SubmitBill(bill);
return new BillSomeoneResult(true);
}
}
public interface IWhyCoService
{
WhyCoEligibilityResponse CheckEligibility(WhyCoEligibilityRequest request);
void SubmitBill(WhyCoBillSubmission bill);
}
在这一点上,我们还没有编写任何与WhyCo API 对话的代码。这使得WhyCoBillingHandler
单元测试变得容易。现在我们可以编写一个IWhyCoService
调用实际 API 的实现。我们可以WhyCoBillingHandler
为IWhyCoService
.
Patient
(如果将我们的和Treatment
数据转换成他们所期望的更接近具体实现,也许会更好。)
在每个步骤中,我们都在编写代码片段,对其进行测试,并将部分推迟到以后。API 类可能是实现WhyCo 计费的最后一步。然后我们可以转到下一家保险公司。
在每个步骤中,我们还决定每个班级应该投入多少。假设我们必须编写一个私有方法,并且该方法最终变得如此复杂以至于它比调用它的公共方法大并且难以测试。这可能是我们用注入到类中的另一个依赖项(抽象)替换该私有方法的地方。
或者我们可能会预先意识到一些新功能应该被分离到它自己的类中,我们可以从它开始。
我以这种方式说明它的原因是:
我把自己分析到一个角落里
当我们的代码要做这么多事情时,很容易陷入瘫痪。这有助于避免瘫痪,因为它不断地为我们提供前进的道路。我们编写它的一部分以依赖于抽象,然后那部分完成(有点)。然后我们实现这些抽象。实现需要更多的抽象,我们重复(在两者之间一直编写单元测试。)
这不会强制执行最佳实践和原则,但会温和地引导我们走向它们。我们正在编写小型、单一职责的课程。它们依赖于抽象。我们从需要它们的类的角度定义这些抽象(在本例中为接口),这导致了接口隔离。每个类都易于单元测试。
有人会指出,很容易被所有的抽象带走,创建太多的接口和太多的抽象层,它们是正确的。但没关系。在每一步,我们都可能以某种方式失去平衡。
如您所见,当我们必须处理计费系统之间的差异时出现的问题变得更加简单。我们只是以不同的方式创建每个实现。
策略似乎适用于计费系统,但 CRUD 操作呢?
他们都有不同的 CRUD 操作这一事实很好。我们在需要相似的地方(我们与它们交互的接口)使组件相似,但内部实现可以根据需要不同。
我们还回避了使用哪种设计模式的问题,除了那IBillingHandlerByInsuranceSelector
是一个抽象工厂。这也没关系,因为我们不想一开始就过于关注设计模式。如果这是一个真正的应用程序,我会假设我正在做的很多事情都需要重构。因为这些类很小,经过单元测试,并且依赖于抽象,所以当它们的使用变得明显时,更容易引入设计模式。当这种情况发生时,我们可能会将它们隔离到需要它们的类中。或者,如果我们刚刚意识到我们走错了方向,那么重构仍然更容易。除非我们能看到肯定会发生的未来。
值得花一些时间来了解各种实现细节,以确保您使用它们的方式与您正在编写的代码保持一致。(换句话说,我们仍然需要花一些时间在设计上。)例如,如果您的计费提供商没有给您即时响应 - 您必须等待数小时或数天 - 那么将其建模为即时响应的代码将不会没道理。
另一个好处是,这可以帮助您与其他开发人员并行工作。如果您确定给定的抽象对于您的保险公司来说是一个好的开始,并且您可能已经编写了一些抽象,那么很容易将其他抽象交给其他开发人员。他们不必考虑整个系统。他们可以只编写一个接口的实现,创建他们需要的任何依赖项。
推荐阅读
- reactjs - 如何在移动设备上将我的反应应用程序显示为桌面视图?
- javascript - 如何使用 eslint 在 FormattedMessage 组件中强制描述属性
- python - 当你运行一个 python 程序时会发生什么?
- flutter - 文本小部件中的 Flutter 共享首选项
- c++ - 如何创建自定义窗口作为孩子?
- django - 在Django中按类别获取百分比
- webpack - 等到一些异步代码将被执行,然后在 webpack 5 hooks 中继续下一步
- javascript - 如何识别 Vuetify 的 v-autocomplete 何时到达最后一个滚动项
- r - 如何将这些 newcolumn ifelse 语句组合成一个 for 循环?
- css - CSS 导入指令只能用于文档的根目录