2019 年 3 月

第 34 卷,第 3 期

[孜孜不倦的程序员]

裸编码:裸属性

作者 Ted Neward | 2019 年 3 月

Ted Neward欢迎回来,使用 NOF 的开发人员们。(我认为,这样叫使用裸对象的人比叫“裸编码员”要好听得多)在上一期专栏文章中,我开始生成会议系统域模型,此模型可便于存储演讲者,但到目前为止它只是相当普通的安装程序。我还没有完成通常需要的任何功能,如验证名字或姓氏是否不为空,或支持作为一组绑定选项之一的“主题”字段等。所有这些都是 UI(以及数据模型)需要支持的合理功能。因此,若要将这种“裸”方法用于实际,它必须也能够执行这些操作。

幸运的是,NOF 设计人员了解这一切。

裸概念

接下来将先回过头来看一看 NOF 一般是如何处理这个问题的,再详细介绍具体细节。

请注意,NOF 的目标是避免编写可使用域对象本身的某方面向其发出信号的 UI 代码,发出这种信号的最佳方式是使用自定义属性。实质上,你使用 NOF 自定义属性来批注域对象的各种元素(多半是属性和方法),NOF 客户端根据属性是否存在或属性中的数据,确定是否需要以某种方式自定义相应对象的 UI。请注意,NakedObjects 其实不需要定义其中许多自定义属性,因为可以从标准 .NET 发行版中的 System.ComponentModel 命名空间“免费”获取它们。可重用性!

然而,有时事情并不像“总应该是这样”那么简单。 例如,如果必须根据对象的内部状态来禁用某些属性(如需要在员工没有配偶或子女的情况下禁用的“on-parental-leave”属性),就需要执行代码,这是自定义属性无法实现的。在这种情况下,NOF 依赖约定:具体而言,NOF 在类中寻找特别命名的方法。如果 parental-leave 属性名为 OnLeave,那么 NOF 执行用来确定是否禁用 OnLeave 属性的方法称为 DisableOnLeave。

接下来看看实际运行效果。

裸 Speaker (Redux)

目前,Speaker 类只有三个属性:FirstName、LastName 和 Age。(这还没有算上对用户不可见的 Id 属性,以及由 FirstName 和 LastName 属性计算出的 FullName 属性;因为用户不可修改它们,所以这里其实并不需要关注它们。至少目前还不需要。) 此会议系统禁止使用没有意义的空名字或姓氏,年龄为负数可能也没有多大意义。接下来将先解决这些问题。

指定非零名称是最容易应用的验证之一,因为它是静态验证。无需使用复杂逻辑,只需要求提供给每个属性的字符串长度必须大于零。这是由每个属性中的 StringLength 属性进行处理,如下所示:

[StringLength(100,
  ErrorMessage = "First name must be between 1 and 100 characters",
  MinimumLength = 1)]
public virtual string FirstName { get; set; }
[StringLength(100,
  ErrorMessage = "Last name must be between 1 and 100 characters",
  MinimumLength = 1)]
public virtual string LastName { get; set; }

这负责解决空姓名问题。

年龄就更容易了,因为我可以使用 Range 自定义属性来指定可接受的最小和最大年龄范围。(我真的会考虑引入不满 21 岁的演讲者吗?可能会,因为我想鼓励学龄儿童进行演讲,但可能会难以接受不满 13 岁的演讲者。) 然后,应用 Range 属性,如下所示:

[Range(13, 90, ErrorMessage = "Age must be between 13 and 90")]
public virtual int Age { get; set; }

请注意,如果错误消息存储在资源中,StringLength 和 Range 属性还需要使用 ErrorMessageResourceName 值(为了便于实现国际化,它们应该这样)。

生成并运行模型;请注意 UI 现在如何自动强制实施这些约束。更为可喜的是,还会尽可能对数据库强制实施约束。完美!

实质上,这些属性本身就起到了数据模型验证的作用,只需少量 UI 即可支持它们。不过,经常还需要更改与数据验证无关的 UI 元素。例如,Speaker 对象属性目前按字母顺序显示,这毫无意义。如果显示的第一个值是全名,后跟名字、姓氏和年龄的各个字段(以及需要捕获和使用的其他任何人口统计信息),这就更现实可行(且实用)。

虽然你肯定会在某一刻认为“好吧,这很有趣,是时候打破障碍并生成自己的 UI 了”,但并不需要这样,因为 NOF 已通过 MemberOrder 属性将这项常见要求涵盖在内。使用此属性,我可以建立属性应在 UI 中出现的“顺序”。因此,例如,若要让 FullName 属性第一个出现在 UI 中,我使用 MemberOrder,并传入相对序号位置“1”,如下所示:

[Title]
[MemberOrder(1)]
public string FullName { get { return FirstName + " " + LastName; } }

接下来,我要显示名字、姓氏和年龄,但此时我可能会开始遇到问题。当我随时间推移向此类添加新字段(如“中间名”或“电子邮件地址”)时,尝试保持顺序中的所有序号位置可能并非易事。如果我将 LastName 移到位置 5,我必须找到位置 5(及之后位置)上的所有内容,然后在每一个位置上都插入内容,以确保位置正确。这样做很痛苦。

幸运的是,MemberOrder 有一个绝佳的小技巧:位置本身可以是浮点值,可便于对字段进行“分组”。这样一来,我现在就可以分别将“FirstName”、“LastName”和“Age”标记为序号位置“2.1”、“2.2”和“2.3”,实质上这意味着组“2”包含人口统计信息;若要添加 Speaker 的任何新人口统计信息,只需对特定组中的成员进行改组即可,如图 1 所示。

图 1:对字段进行分组

[MemberOrder(2.1)]
[StringLength(100,
  ErrorMessage = "First name must be between 1 and 100 characters",
  MinimumLength = 1)]
public virtual string FirstName { get; set; }
[MemberOrder(2.2)]
[StringLength(100,
  ErrorMessage = "Last name must be between 1 and 100 characters",
  MinimumLength = 1)]
public virtual string LastName { get; set; }
[Range(13, 90, ErrorMessage = "Age must be between 13 and 90")]
[MemberOrder(2.3)]
public virtual int Age { get; set; }

请注意,值本身并没有什么特别之处,特别的是它们的使用是相对彼此而言,且它们不代表屏幕上的任何特定位置。事实上,如果我愿意,我本可以使用 10、21、22 和 23。NOF 谨慎指出,这些值是按字典顺序进行比较,即基于字符串的比较,而不是进行数字比较,因此请采用对你有意义的方案。

如果用户不确定 Age 是按年计算,还是按天计算,该怎么办?对你来说,答案似乎完全显而易见,但请注意,并非每个人看待世界的方式都相同。虽然这可能不是需要在 UI 上公开显示的信息,但它应该是可以某种方式示意用户的内容。在 NOF 中,使用“DescribedAs”属性来示意应如何描述属性,通常采用的形式是在输入区域之上显示工具提示。(尽管如此,请注意,给定 NOF 客户端可能会选择使用不同的方式来示意用户;例如,如果 NOF 客户端用于以触控为中心的电话,工具提示就不太适用于这种格式。在这种情况下,假设的 NOF 客户端会使用一种更适合此平台的不同机制来描述属性。)

Speaker 需要有 bio!哦,我的天啦,我怎么能忘记这个呢!这就像曾经的演讲者要只写自己及其伟大事迹一样。(开个玩笑,如果说有一件事是演讲者最讨厌的,那就是写自己的个人简介了。) bio 是易于添加到类中的属性,但大多数 bio 需要的不仅仅是一两句话,看看到目前为止由 NOF 生成的 UI,其他所有字符串目前都只占一行。正因为此,NOF 提供了“MultiLine”属性,以指明是否应在比典型字符串更大的文本输入区域中显示这个字段。

但在这种情况下,我需要谨慎处理演讲者的个人简介,因为自由格式输入可能会造成滥用:我可能希望/需要筛选掉一些要显示的字词,以防人们对会议有错误的印象。如果演讲者的简介中有像 COBOL 这样的字词,我就不能让这样的人在会议上发表演讲!幸运的是,NOF 允许验证输入,具体方式是对 Speaker 类查找和调用与 Validate[Property] 约定匹配的方法,如下所示:

[MemberOrder(4)]
[StringLength(400, ErrorMessage = "Keep bios to under 400 characters, please")]
public virtual string Bio { get; set; }
public string ValidateBio(string bio)
{
  if (bio.IndexOf("COBOL") > -1)
    return "We are terribly sorry; nobody wants to hear that";
  else
    return "";
}

总结

NOF 提供了各种描述域对象的选项,简化了自动呈现适当 UI 来强制实施域限制的过程,但到目前为止,模型仍相当简单。在下一期专栏文章中,我将介绍 NOF 如何处理更复杂的主题,即对象之间的关系。(例如,Speaker 需要能够指定所谈论的 Topics,以及最重要的 Talks。) 但本月这期文章的剩余空间不足了,所以在此期间,祝编码快乐!


Ted Neward 是本部位于西雅图的 Polytechnology 公司的顾问、讲师和导师。他写过大量文章,独自撰写并与人合著过十几本书,并在世界各地发表演讲。可通过 ted@tedneward.com 与他联系,也可阅读他的博客 blogs.tedneward.com

衷心感谢以下技术专家对本文的审阅:Richard Pawson
Richard Pawson 于 1977 年开始他的计算职业生涯,当时他就职于一家制作便携式计算器的公司。在他加入三周后,这家公司宣布推出全球首款个人电脑:Commodore PET。随后这些年,Richard 担任过技术记者、机器人工程师、电子玩具设计人员、管理顾问和软件开发人员。  2003 年,Richard 开始将裸对象作为自己博士论文的主题,并从那时起开始管理开放源代码裸对象框架的开发。在业余时间,他一直在修复一辆 1939 年的戴姆勒软顶敞篷车,这辆车最初属于英国国王乔治六世。