原始vs类代表简单的域对象?


14

什么时候使用特定于域的对象与普通字符串或数字的通用准则或经验法则是什么?

例子:

  • 年龄层vs整数?
  • FirstName类与String?
  • 唯一ID与字符串
  • PhoneNumber类vs字符串vs长?
  • 域名类与字符串?

我认为大多数OOP从业人员肯定会说PhoneNumber和DomainName的特定类。有关使它们变得有效以及如何进行比较的更多规则,使简单的类更易于处理和更安全。但是对于前三个,还有更多的争论。

我从来没有遇到过“年龄”类,但是有人认为它必须是非负数是有道理的(好吧,我知道您可以为负数年龄辩护,但这是一个很好的例子,它几乎等同于原始整数)。

字符串通常代表“名字”,但并不是完美的,因为空字符串是有效字符串,但不是有效名称。比较通常会忽略大小写。当然,有一些方法可以检查是否为空,不区分大小写,等等,但这需要使用者执行此操作。

答案是否取决于环境?我主要关心的是企业/高价值软件,该软件可以生存和维护超过十年。

也许我想得太多了,但是我真的很想知道是否有人对何时选择类还是原始有规则。


2
如果可以用整数代替年龄类,则不是一个好主意。如果在构造函数中使用出生日期,并且具有getCurrentAge()和getAgeAt(Date date)方法,则该类具有含义。但是不能用整数代替。
kiwiron

5
“字符串有时不是字符串。请对其进行相应的建模。” 有一个很好的职位由Mark塞曼约“原始痴迷”:blog.ploeh.dk/2015/01/19/...
Serhii Shushliapin

4
实际上,Age类别确实在某种程度上有些怪异。西方常见的年龄定义是,出生时为零,下一个生日时加1。东亚的方法是您出生时为1岁,下一个元旦又增加1个。因此,根据您的语言环境,您的年龄可能会有所不同。来自en.wikipedia.org/wiki/East_Asian_age_reckoning的 EGpeople may be one or two years older in Asian reckoning than in the western age system
Peter M,

@PeterM,这是个很好的信息!日期/时间和年龄。它们应该是简单的概念,但这证明了世界上比我们过去处理的复杂得多的方法。
马查多

6
我在这个问题下面看到很多文章,但是答案并不是那么复杂。Age当您需要此类提供的其他行为时,可以使用类而不是整数。
罗伯特·哈维

Answers:


20

什么时候使用特定于域的对象与普通字符串或数字的通用准则或经验法则是什么?

一般准则是您要使用特定于域的语言对域建模。

考虑:为什么我们使用整数?我们可以轻松地用字符串表示所有整数。或带字节。

如果我们使用领域不可知的语言进行编程,其中包括整数年龄的原始类型,您会选择哪一种?

真正归结为某些类型的“原始”性质是我们实施语言选择的偶然。

特别是数字,通常需要附加的上下文。年龄不仅是数字,还具有维度(时间),单位(年?)和舍入规则! 以某种方式相加年龄是有意义的,而不是给货币增加年龄。

区分类型使我们可以对未验证的电子邮件地址和已验证的电子邮件地址之间的差异进行建模。

这些值如何在内存中表示的偶然性是最不有趣的部分之一。域模型不关心a CustomerId是一个int还是一个String或一个UUID / GUID或一个JSON节点。它只是想要负担能力。

我们真的关心整数是大端还是小端吗?我们是否在乎List传递的是数组或图形的抽象?当我们发现双精度算术效率低下,并且需要更改为浮点表示形式时,域模型应该照管吗?

帕纳斯(Parnas)在1972年写道

相反,我们建议从一系列困难的设计决策或可能会更改的设计决策开始。然后,将每个模块设计为对其他模块隐藏此类决策。

从某种意义上说,我们引入的领域特定值类型是隔离我们决定应使用哪种基本数据表示的模块。

因此,好处在于模块化-我们得到的设计可以更轻松地管理变更范围。缺点是成本- 创建所需的定制类型需要做更多的工作,选择正确的类型需要对域有更深入的了解。创建值模块所需的工作量将取决于您的Blub方言。

等式中的其他术语可能包括解决方案的预期寿命(对投资回报率不佳的脚本软件进行仔细的建模),领域与企业核心竞争力的接近程度。

我们可能考虑的一种特殊情况是跨越边界的通信。我们不希望出现一种情况,即对一个可部署单元的更改需要与其他可部署单元的协调更改。因此,消息往往更关注表示形式,而不考虑不变性或特定领域的行为。我们不会尝试以消息格式传达“此值必须严格为正”,而是在线路上传达其表示,并将验证应用于域边界处的表示。


隐藏(或抽象化)那些困难且可能会更改的东西的绝妙点。
user949300

4

您正在考虑抽象,那太好了。但是,在我看来,您列出的内容主要是较大内容的个别属性(当然,不是同一件事物)。

我觉得更重要的是提供将属性分组在一起的抽象,并为它们提供适当的位置(例如Person类),以便客户端程序员不必处理多个属性,而只需处理一个更高级别的抽象。

一旦有了这种抽象,就不必为只是值的属性添加其他抽象。Person类可以将BirthDate用作(原始)日期,将PhoneNumbers作为(原始)字符串的列表,将Name用作(原始)字符串。

如果您的电话号码带有格式代码,那么现在这是两个属性,并且应该为电话号码添加一个类(因为它也可能会有一些行为,例如仅获取号码与获取格式化的电话号码)。

如果您有一个名称作为具有多个属性的名称(例如,前缀/标题,首个,最后一个,后缀),则该名称值得一个类(因为现在您有一些行为,例如将全名作为字符串而不是较小的名称,标题等)。 )。


1

您应该根据需要进行建模,如果只需要一些数据就可以使用基本类型,但是如果要对这些数据进行更多操作,则应在特定的类中进行建模,例如(我同意@kiwiron的意见) Age类没有意义,但是最好用Birthdate类代替它,然后将这些方法放在那里。

在类中对这些概念进行建模的好处是,您可以增强模型的丰富性,例如,您可以使用一种方法来接受任何日期,用户可以用任何日期,二战开始日期或冷战结束日期来填充它,这些日期中的一个仍然是有效的生日。您可以将其替换为Birthdate,因此在这种情况下,您可以强制您的方法接受Birthdate,而不接受任何Date。

对于Firstname,我也看不到太多好处,例如UniqueID,PhoneNumber,您可以要求它提供您所在的国家和地区,例如,因为通常PhoneNumber指的是国家和地区,某些运营商使用预定义的数字后缀来对Mobile进行分类,Office ...数字,因此您可以将这些方法添加到该类中,与DomainName相同。

有关更多详细信息,我建议您看一下DDD(域驱动设计)中的值对象。


1

它取决于您是否具有与该数据相关联的行为(需要维护和/或更改)。

简而言之,如果只是一块数据而没有太多相关行为,则使用原始类型,或者对于一些更复杂的数据,使用(大部分无行为)数据结构(例如,只有吸气剂和二传手)。

另一方面,如果您发现为了做出决定而在代码中的各个位置检查或操作了这些数据,这些位置通常彼此之间并不靠近-然后通过定义一个类将其转变为合适的对象并将相关行为作为方法放入其中。如果出于某种原因您认为定义这样的类没有意义,那么另一种方法是将这种行为合并为某种对数据进行操作的“服务”类。


1

您的示例看起来更像其他属性或属性。这意味着仅从原始开始是完全合理的。在某些时候,您将需要使用原语,因此通常可以节省实际需要的时间。

例如,假设我正在制作游戏,而不必担心角色老化。为此使用整数是很有意义的。可以在简单的检查中使用年龄,以查看角色是否被允许做某事。

正如其他人指出的,根据您所在的地区,年龄可能是一个复杂的话题。如果您需要协调不同语言环境中的年龄等,那么引入Age具有DateOfBirthLocale属性的类是很有意义的。该年龄类还可以计算任何给定时间戳记的当前年龄。

因此,有理由将原语提升为类,但是它们是非常特定于应用程序的。这里有些例子:

  • 您具有特殊的验证逻辑,该逻辑必须在所有使用信息类型的地方(例如电话号码,电子邮件地址)应用。
  • 您具有特殊的格式化逻辑,用于渲染供人类阅读(例如IP4和IP6地址)
  • 您有一组与该对象类型有关的函数
  • 您有比较相似值类型的特殊规则

基本上,如果您有一系列关于如何对待值的规则,并且希望在整个项目中使用该规则,请创建一个类。如果您可以使用简单的值,因为它对功能并不是很关键,请使用原语。


1

归根结底,一个对象只是某个过程实际上已经运行的见证者

原语int意味着某些进程将4个字节的内存清零,然后将表示某些整数所需的位翻转到-2,147,483,648到2,147,483,647之间;这不是关于年龄概念的有力说明。同样,(非空)System.String意味着分配了一些字节,并且翻转了位以表示有关某些编码的Unicode字符序列。

因此,在决定如何表示域对象时,应考虑充当域对象的过程。如果一个FirstNametrue真正应该是一个非空字符串,则它应作为系统运行某种过程的见证,该过程确保至少一个非空白字符是在一系列递给您的字符中创建的。

同样,一个Age对象可能应该是某个过程计算了两个日期之间的差的见证人。由于ints通常不是计算两个日期之间差异的结果,因此它们实际上不足以表示年龄。

因此,很明显,您几乎总是想为每个域对象创建一个类(只是一个程序)。所以有什么问题?问题是C#(我喜欢)和Java在创建类时要求太多的仪式。但是,我们可以想象一下替代语法,这些语法将使定义域对象更加简单:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

例如,F#实际上以Active Patterns的形式具有不错的语法:

//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an `Age` when needed.

let (|Age|) (x, y) =  (date_diff y x).Days / 365

关键是,仅仅因为您的编程语言使定义域对象变得繁琐,并不意味着这样做实际上不是一个好主意。


†我们也称这些条件为条件,但我更喜欢“见证”一词,因为它可以证明某些过程可以并且确实可以运行。


0

想一想使用类和原始语言所获得的收益。如果使用课程可以帮助您

  • 验证
  • 格式化
  • 其他有用的方法
  • 类型安全性(例如,对于一个PhoneNumber类,我应该不可能将其分配给我希望有电话号码的字符串或随机字符串(例如,“黄瓜”))
  • 任何其他好处

这些好处使您的生活更轻松,提高了代码质量,可读性,并且超过了使用此类工具的痛苦,那就去做吧。否则,请坚持使用原语。如果关闭,请进行判断。

还应认识到,有时好命名就可以处理它(即,名为firstNamevs 的字符串使整个类仅包含string属性)。类不是解决问题的唯一方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.