我应该把我的业务规则和验证放在哪里?
简短的 DDD 入门
我的验证去哪儿了?
保险政策情景
出发啦!
保持联系
导航您的软件开发职业通讯
您是否曾经为将应用程序的业务规则和验证放在哪里而苦恼?
您应该将它们放入您的 MVC 模型中吗?
您是否应该创建特殊的类来进行验证?
为了回答这个问题,我将向您展示领域驱动设计中使用的久经考验的技术!
简短的 DDD 入门
领域驱动设计是一种整体性的软件设计方法和框架。它非常重视探索业务问题,首先从业务问题入手,然后研究相关人员当前如何处理该问题,找出该领域的专家,咨询专家,探索建模问题的潜在方法等等。
DDD 是一个庞大的课题。今天,我们将探讨一项源自 DDD 的技术。
让我们看一下两个 DDD 概念来奠定基础。
实体
实体只是一个由唯一标识符标识的模型。例如客户、订单、保险单等等。
当测试两个实体是否相同或相等时,我们会测试每个实体的唯一 ID 以查看它们是否匹配。
值对象
值对象指的是那些无法唯一识别的事物。例如,地址、人名或日期范围等都可以是值对象。
它们可以保存多条数据,如前所述,一个日期范围(有开始日期和结束日期)。
当测试两个值对象是否相同或相等时,我们测试所有值是否相同。
结合两者
实体和值对象通常是相辅相成的。
实体通常由(a)其他实体和(b)值对象组成。
例如,订单实体可能包含对几个订单项实体的引用,但它也可能包含对购买价格(值对象)的引用。
我的验证去哪儿了?
在 DDD 中,将业务规则和验证纳入值对象是一种很好的做法。
有两个基本特性使其成为一项伟大的技术——不变性和创建时立即验证。
立即验证
通过在创建时测试值对象是否有效,我们确保不可能存在处于无效状态的值对象。
不变性
使值对象不可变意味着一旦创建它们就无法被修改。
这两个属性都确保您不会拥有处于无效状态的对象 - 无论是通过使用无效数据创建它还是通过引起改变数据的副作用。
知道您将始终拥有有效对象的实例,这很简单但很强大。
保险政策情景
让我们看一个真实的例子,了解如何使用值对象来执行业务验证。
假设我们需要验证一份保险单。该保险单包含以下内容:
- 受保人年龄
- 受保人性别
- 被保险人地址
- 开始日期
假设存在以下规则:
- 基本价格为每月 15 美元
- 年龄超过 50 岁,价格加倍
- 当性别为男性时,价格会翻倍(因为男性更容易受伤......)
- 如果地址位于加拿大,则开始日期必须是当前月份的月初
建模技术
围绕实体和值对象的建模,存在着一个完整的研究领域。归根结底,一般规则是将变化的数据集中保存在同一个地方。
这实际上是面向对象的基本原则。我们常常选择忽略它……
因此,我们需要根据业务需求,弄清楚哪些数据是一起变化的:
- 年龄和性别影响价格
- 地址影响开始日期
通过跟踪数据的变化,我们可以看到我们的模型可能是什么样的:
public class Object1
{
public int Age { get; private set; }
public Gender Gender { get; private set; }
public decimal Price { get; private set; }
public Object1(int age, Gender gender)
{
this.Age = age;
this.Gender = gender;
this.Price = 15.00m; // 1st business requirement: Base price is $15/month
}
}
public class Object2
{
public Address Address { get; private set; }
public DateTime DateStarted { get; private set; }
public Object2(Address address, DateTime dateStarted)
{
this.Address = address;
this.DateStarted = dateStarted;
}
}
关于命名的说明
使用此技术创建对象时,通常建议不要立即命名值对象和实体。
这可以确保您专注于数据以及基于业务的事物归属。我们很容易在脑海中给对象贴上标签,这会导致对每个模型中“应该”包含哪些内容产生偏见。
但我们不应该决定这一点——业务需求才是决定因素。一旦我们确定我们的模型已经达到了我们所能达到的水平,我们就可以给它们赋予有意义的名称。
添加我们的业务规则
让我们添加前 3 条规则。
我们将在对象的构造函数中添加此验证。这满足了值对象在创建时必须有效的属性。您永远不应该创建包含无效数据的对象。
public class Object1
{
public int Age { get; private set; }
public Gender Gender { get; private set; }
public decimal Price { get; private set; }
public Object1(int age, Gender gender)
{
// Rule #1: Base price is $15/month.
decimal basePrice = 15.00m;
// Rule #2: When age is above 50 then price is doubled.
if(age >= 50)
{
basePrice = basePrice * 2;
}
// Rule #3: When gender is Male then price is doubled.
if(gender == Gender.Male)
{
basePrice = basePrice * 2;
}
this.Age = age;
this.Gender = gender;
this.Price = basePrice;
}
}
如何处理无效状态
我们将使用一种技术来确保我们的值对象永远不会处于无效状态:
- 在值对象的构造函数中测试业务需求
- 如果规则失败则抛出异常
为此,我们将创建一个新的异常类型:
public class BusinessRuleException : Exception
{
public BusinessRuleException(string message) : base(message) { }
}
我们将使用它来添加下一个业务规则:
public class Object2
{
public Address Address { get; private set; }
public DateTime DateStarted { get; private set; }
public Object2(Address address, DateTime dateStarted)
{
// Rule #4: If address is in Canada then Date Started must be the beginning of the current month.
if(address.IsCanadian && dateStarted.Day != 1)
{
throw new BusinessRuleException("Canadian policies must begin on the first day of the current month.");
}
this.Address = address;
this.DateStarted = dateStarted;
}
}
有人可能会指出,我们可以自动将开始日期更改为月初。
但这只是需求的一部分。我们到底要不要为用户转换日期呢?在本例中,我们的业务只是想告知用户这条业务规则。
进一步,调用者将捕获异常并向用户显示错误消息。
但是显示多条错误消息怎么办?
很高兴您问这个问题!
顺便提一句,一种方法是用静态工厂方法替换构造函数。你可能会返回一个元组(在 C# 中):
public class Object2
{
public Address Address { get; private set; }
public DateTime DateStarted { get; private set; }
// Make sure no one can create this object with a constructor.
private Object2() { }
// Static Factory Method that returns (a) the value object and (b) any errors.
public static (Object2, IEnumerable<string>) Make(Address address, DateTime dateStarted)
{
var errors = new List<string>();
// Rule #4: If address is in Canada then Date Started must be the beginning of the current month.
if(address.IsCanadian && dateStarted.Day != 1)
{
errors.Add("Canadian policies must begin on the first day of the current month.");
}
// Imagine that there might be more business rules here.
if(errors.Any()){
return (null, errors); // Never return an invalid instance of the value object ;)
}
return (
new Object2
{
Address = address,
DateStarted = dateStarted
},
errors
);
}
}
还有其他模式可以实现此目的,例如结果对象模式。
建立我们的实体
现在让我们命名我们的价值对象并为我们的整体保险政策创建一个实体。
注意:我的名字并非最佳。实际上,赋予这些有意义的名字需要花费大量的时间和精力。如果您的业务正在充分运用 DDD,那么预先了解通用语言总是能帮助您选择使用哪些名称,并可能帮助您选择如何构建对象模型。
public class InsurancePolicy
{
public PolicyPricing Pricing { get; private set; }
public PolicyAddress Address { get; private set; }
public InsurancePolicy(PolicyPricing pricing, PolicyAddress address)
{
this.Pricing = pricing;
this.Address = address;
}
}
使用我们的值对象进行验证
以下是我们如何提供用户输入并创建实体:
try {
var pricing = new PolicyPricing(age, gender);
var address = new PolicyAddress(userAddress, dateStarted);
// You can never get here unless all business rules are valid.
var entity = new InsurancePolicy(pricing, address);
// Now you might save the entity in a DB?
}
catch(BusinessRuleException ex)
{
errorMessage = ex.Message; // This would be returned to the UI, etc.
}
测试
您可能会注意到,测试您的值对象可以使测试特定的业务规则变得非常容易!
出发啦!
我希望这是一个有用的介绍,可以帮助您了解如何将领域驱动设计概念融入到您的代码中。
以这种方式使用值对象可以为您提供一个可用于以下用途的框架:
- 正确建模您的业务问题
- 弄清楚业务验证应该去哪里
保持联系
导航您的软件开发职业通讯
一封电子邮件简报,助您提升软件开发职业水平!您是否想过:
✔ 软件开发人员通常经历哪些阶段?
✔ 我如何知道自己处于哪个阶段?如何进入下一个阶段?
✔ 什么是技术领导者?如何成为技术领导者?
✔ 有人愿意陪伴我并解答我的疑问吗?
听起来很有趣?加入社区吧!
鏂囩珷鏉ユ簮锛�https://dev.to/jamesmh/where-do-i-put-my-business-rules-and-validation-o2f