面向.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下载示例中的一部分。)
当然,很多能以简单的CRUD用户界面形式呈现领域对象模型的框架都提供了数据库的浏览或维护功能。Naked Objects之所以获得更多关注,则是它能够生成完整功能的应用程序。默认情况下,任何公共方法对于用户而言都是可用的,形式则是通过在对象图标上弹出 菜单的动作,而方法名则以动作名进行重新格式:
如果方法具有参数方法签名,通常会创建一个对话框用于调用动作,如:
public IExpenseItem CopyAnExistingExpenseItem( IExpenseItem otherItem)
则呈现为:
Other Item字段要求用户拖动或粘贴一个类型为IExpenseItem(UI字段会拒绝其他任何类型的对象)的现有对象。在字段的右边同样是一个下拉框指示器,它能够自动提供一个列表对象,其类型在其他标签上对用户是可见的,这样就能够避免不必要的标签转换。
如果Expense Item在屏幕或下拉列表中不存在,用户如何能够找到它?或者说,当他们没有另外一个对象作为开始对象时,用户该如何创建一个新的对象实例?这正是仓储模 式和工厂模式所要解决的,它们都是领域驱动设计的标准模式。在Naked Objects的术语中,仓储和工厂都是服务样本,服务本身就是最上等的领域对象。服务的使用有三种方式。
第一种方式,服务提供主菜单,并呈现在屏幕顶端。通常这些菜单提供业务活动起点的动作,例如创建一个新的客户,或者查找一个现有客户。第二种方式,可以将服务注入到其他需要服务的领域对象中,这遵循了依赖注入(DI)模式。Naked Objects以透明的方式管理服务的注入,你只需要为所需服务类型提供一个可设置的属性,而不需要像许多依赖注入框架所要求的那样在外部进行配置。
使用服务的第三种方式按照我们的术语规定就是“赠予动作”。如果服务具有一个公共方法:
public virtual IList <RecordedAction> allRecordedActions(IRecordedActionContext context)
那么它就会自动作为一个用户动作被“赠予”给实现了IRecordedActionContext
的任何对象,你所选择的动作所执行的对象会自动填充对话框的第一个字段。(该动作表现为“Recorded Actions”子菜单,如第一张截图所示——并成为提供该动作的服务名)。
Naked Objects最强大的功能是为你提供了一种实现多继承的方法(至少从用户的角度来讲是如此)。它的某些概念与mix-ins或者.NET扩展方法的概念相仿,虽然在实现上略有不同。
你该如何实现业务规则呢?有两个选择。其一是使用特性,它可以应用到类、属性、方法或参数。例如:
[MaxLength()],
[Mask()]
和[RegEx()]
允许你为输入字符串指定约束以及/或者对他们进行格式规范。public
,但又不应暴露给用户,则可以使用[Hidden]
。各种条件都可以应用到该特性上。[Named()]
。(注意这是一种完全独立的机制,它为所有label提供了国际化支持)。第二种选择更为灵活,可以根据下面所展示的约定采用编程方式:
public virtual void CopyFrom(IExpenseItem otherItem) {...} public virtual string ValidateCopyFrom(IExpenseItem otherItem) {...}
在本例中,CopyFrom
方法会作为一个用户动作被框架所呈现;框架同时能够识别ValidateCopyFrom
方法,作为验证动作参数的逻辑:如果方法返回一个非空字符串,则对话框的“OK”按钮就会从灰色转为可用,字符串消息则作为工具提示显示给用户。相似的,DisableCopyFrom
可以使得动作不可用,譬如当一项主张已经被持久化。(注意,框架具有一套完全独立的基于用户角色管理许可的机制)。还有其它一些约定,例如指定默认值或者为参数指定选项(下拉列表)。
这些编码约定增加了可选的行为。你必须遵守三种简单的编码约定。有两种是应用在属性上的,如下所示:
public virtual Employee Claimant { get { Resolve(employee); return employee; } set { employee = value; ObjectChanged(); } }
Resolve()
调用确保对象已经被加载到内存中,而ObjectChanged()
则会通知框架属性值已经改变。两者都是为了更新对象的视图,以及确保改变被持久化。(注意框架会为你自动化处理持久化:你不需要为加载、保存和更新对象编写任何方法)。
第三个要求是你编程创建的任何对象都必须通知框架。你不能这样写:
Employee employee = new Employee();
而需要这样:
Employee employee = Container.NewTransientInstance< Employee>();
这些调用并不是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的表达式编辑器,它可以免费下载。