面向.NET 的Naked Objects 作为一个框架,提供了针对.NET 框架下裸对象架构模式的 一种实现。裸对象背后隐藏的基本概念是在编写一个业务应用程序时,开发者只需要编写领域对象以及封装在领域对象中的业务逻辑。而该框架就会将领域对象以丰 富的面向对象样式的用户界面形式暴露给用户,同时还会处理这些对象的持久化与管理,这通常通过一个ORM 实现。对于那些提出领域驱动设计的人,这一模式可能正是投其所好。除了消除编写用户界面层和数据访问层的需求,裸对象模式还有助于良好的对象建模——因为你可以瞬间将一个原型领域模型转换为一个能够为业务用户评估的应用程序。
很多人听及此的第一反应就是它无法在大规模的复杂业务应用程序下工作。然而在爱尔兰政府的一个员工人数超过1000 人的部门 DSFA ,使用的应用程序就是 Naked Objects 开发的,它能够支持每年价值数亿欧元的社会保障金管理。
历史
Naked Objects 框架最初脱胎于 Java 平台。最早的版本是用 Java 1.1 编写的。在软件开发的历史长河中,一个最伟大的偶然事件是我们发现它能够未经修改就能够以 J#语言运行在.NET 平台上。然而,当 Naked Objects 的后期版本迁移到 Java 1.5 后,它与.NET 的兼容性就消失了。让人大吃一惊的是在 2007 年 1 月,微软证实从.NET 3.5 之后将不再支持 J#。
为解决这一问题,面向.NET 的 Naked Objects 就作为框架的完整重现应运而生。它使用 C#进行编写,设计时完全利用了.NET 平台的功能,包括泛型和 LINQ。而面向.NET 的 Naked Objects 创建的通用用户界面则使用 WPF 编写。
Naked Objects 的工作原理
通用用户界面并不依赖于代码生成:它是动态创建的。在运行时,框架使用了反射技术将领域对象呈现在用户界面上。这一技术被称之为“内省”技术,它支 持开发人员快速地将域模型转换为可用的应用程序。下面的截图展示了一个对象的打开视图,该对象包含了几个关联对象的链接(每个图标表示一个域对象);用户 可以通过单击浏览这些关联对象:
(文中所有的截图和代码示例都节选自一个简单的费用处理程序,该程序是 Naked Objects下载示例中的一部分。)
当然,很多能以简单的CRUD 用户界面形式呈现领域对象模型的框架都提供了数据库的浏览或维护功能。Naked Objects 之所以获得更多关注,则是它能够生成完整功能的应用程序。默认情况下,任何公共方法对于用户而言都是可用的,形式则是通过在对象图标上弹出 菜单的动作,而方法名则以动作名进行重新格式:
如果方法具有参数方法签名,通常会创建一个对话框用于调用动作,如:
<span color="#0000ff">public</span> <span color="#2b91af">IExpenseItem</span> CopyAnExistingExpenseItem( <span color="#2b91af">IExpenseItem</span> otherItem)
则呈现为:
Other Item 字段要求用户拖动或粘贴一个类型为 IExpenseItem(UI 字段会拒绝其他任何类型的对象)的现有对象。在字段的右边同样是一个下拉框指示器,它能够自动提供一个列表对象,其类型在其他标签上对用户是可见的,这样就能够避免不必要的标签转换。
如果 Expense Item 在屏幕或下拉列表中不存在,用户如何能够找到它?或者说,当他们没有另外一个对象作为开始对象时,用户该如何创建一个新的对象实例?这正是仓储模 式和工厂模式所要解决的,它们都是领域驱动设计的标准模式。在 Naked Objects 的术语中,仓储和工厂都是服务样本,服务本身就是最上等的领域对象。服务的使用有三种方式。
第一种方式,服务提供主菜单,并呈现在屏幕顶端。通常这些菜单提供业务活动起点的动作,例如创建一个新的客户,或者查找一个现有客户。第二种方式,可以将服务注入到其他需要服务的领域对象中,这遵循了依赖注入(DI)模式。Naked Objects 以透明的方式管理服务的注入,你只需要为所需服务类型提供一个可设置的属性,而不需要像许多依赖注入框架所要求的那样在外部进行配置。
使用服务的第三种方式按照我们的术语规定就是“赠予动作”。如果服务具有一个公共方法:
<span color="#0000ff">public</span> <span color="#0000ff">virtual</span> <span color="#2b91af">IList</span> <<span color="#2b91af">RecordedAction</span>> allRecordedActions(<span color="#2b91af">IRecordedActionContext</span> context)
那么它就会自动作为一个用户动作被“赠予”给实现了IRecordedActionContext
的任何对象,你所选择的动作所执行的对象会自动填充对话框的第一个字段。(该动作表现为“Recorded Actions”子菜单,如第一张截图所示——并成为提供该动作的服务名)。
Naked Objects 最强大的功能是为你提供了一种实现多继承的方法(至少从用户的角度来讲是如此)。它的某些概念与 mix-ins 或者.NET 扩展方法的概念相仿,虽然在实现上略有不同。
实现业务规则
你该如何实现业务规则呢?有两个选择。其一是使用特性,它可以应用到类、属性、方法或参数。例如:
- 默认情况下,框架会要求用户能够在保存一个对象之前完成所有字段,并且能够在执行动作之前,在对话框中提供所有字段。你可以使用 [Optionally()] 特性重写该行为。
[MaxLength()],``[Mask()]
和[RegEx()]
允许你为输入字符串指定约束以及 / 或者对他们进行格式规范。- 如果因为编程的原因,你必须将属性或方法定义为
public
,但又不应暴露给用户,则可以使用[Hidden]
。各种条件都可以应用到该特性上。 - 默认情况下,类、属性和动作的名称都会被使用,在用户界面上可以对其名称进行合理的格式规范。我们认为这是一个好的实践。然而,如果你需要一个代码不会使用的 label(例如它包含了符号或标点),则可以使用
[Named()]
。(注意这是一种完全独立的机制,它为所有 label 提供了国际化支持)。
第二种选择更为灵活,可以根据下面所展示的约定采用编程方式:
<span color="#0000ff">public virtual void</span> CopyFrom(<span color="#2b91af">IExpenseItem</span> otherItem) {...} <span color="#0000ff">public virtual string</span> ValidateCopyFrom(<span color="#2b91af">IExpenseItem</span> otherItem) {...}
在本例中,CopyFrom
方法会作为一个用户动作被框架所呈现;框架同时能够识别ValidateCopyFrom
方法,作为验证动作参数的逻辑:如果方法返回一个非空字符串,则对话框的“OK”按钮就会从灰色转为可用,字符串消息则作为工具提示显示给用户。相似的,DisableCopyFrom
可以使得动作不可用,譬如当一项主张已经被持久化。(注意,框架具有一套完全独立的基于用户角色管理许可的机制)。还有其它一些约定,例如指定默认值或者为参数指定选项(下拉列表)。
POCOs
这些编码约定增加了可选的行为。你必须遵守三种简单的编码约定。有两种是应用在属性上的,如下所示:
<span color="#0000ff">public virtual</span> Employee Claimant { <span color="#0000ff">get</span> { Resolve(employee); <span color="#0000ff">return</span> employee; } <span color="#0000ff">set</span> { employee = <span color="#0000ff">value</span>; ObjectChanged(); } }
Resolve()
调用确保对象已经被加载到内存中,而ObjectChanged()
则会通知框架属性值已经改变。两者都是为了更新对象的视图,以及确保改变被持久化。(注意框架会为你自动化处理持久化:你不需要为加载、保存和更新对象编写任何方法)。
第三个要求是你编程创建的任何对象都必须通知框架。你不能这样写:
<span color="#0000ff">Employee</span> employee = new Employee();
而需要这样:
<span color="#0000ff">Employee</span> employee = Container.NewTransientInstance< <span color="#2b91af">Employee</span>>();
这些调用并不是 Naked Objects 框架要求的,而只是将其委托给定义在IDomainObjectContainer
中的方法。如果你为你的对象类型提供了一个属性,框架就会将其注入到容器中,该容器在运行时为你实现这些功能。作为一种快捷方式,你可以选择让域对象继承自AbstractDomainObject
,它可以代替你为每种情形生成这几行代码。
但是必须声明的重点是继承是可选的:Naked Objects 是一个基于 POCO(Plain Old CLR Object,普通旧式 CLR 对象)的方式。除了通常意义上 POCO 所具有的优势外,它还意味着你可以选择使用相同的域对象,并将其运行在更加广泛的架构中:编写必要的用户界面和其他层。你所需要做的就是为容器提供这三类调用的存根代码。(如果你希望使用面向方面编程,你可以从域代码中去除Resolve()
,ObjectChanged()
和newTransientInstance()
调用,但是我们不希望将 Naked Objects 与具体的 AOP 实现紧密耦合。)
Naked Objects 引人入胜之处是你可以只使用它就能够支持开发过程中的领域驱动设计:Naked Objects 不会强迫你必须要实现整个系统。还有一个好消息是你可以只使用 Naked Objects 的表达式编辑器,它可以免费下载。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论