为了便于大家参考,我把Domain Model的部分也放上来吧。但是因为在Domain Model中不
涉及数据库操作了。源代码主要解决算法问题,没有太多的语言特征,所以我也不转换源
代码了。文中关于J2EE的部分,由于我对这个方面不熟悉,许多术语都不能很好的译出。
因此大家在看那部分的时候,仅仅用作参考就好了。而且请懂J2EE的朋友帮助修正。
Home Articles Talks Links Contact Me ISA ThoughtWorks
--------------------------------------------------------------------------------
Domain Model
--------------------------------------------------------------------------------
Build an object model of the domain that incorporate both behavior and data.
创建一个为业务领域整合了行为和数据的对象模型
图 http://martinfowler.com/isa/domainModelSketch.gif
在最坏的情况下,业务逻辑会变得非常复杂。 规则和逻辑描述了不同的情况和行为倾向。
面向对象正是设计来处理这种复杂的情况。一个Domain Model创建一个由互相关联的对象
所组成的网络。在这个网络中每个对象都表示一个有意义的实体。这些实体从非常巨大,例
如一间公司,到非常微小,例如订单中的一行。
How it Works
关于这个主题可以写出一本书来,因此我不知道从哪里开始才好。
把一个Domain Model放到一个应用中涉及到在其中插入一整个对象层。这些对象把你正在工
作的业务领域模型化。你会发现这些对象与业务中的数据很相似,而且这些对象包含了业务
中使用的规则。通常这些数据和过程被整合到一起使得过程与它们所工作的数据尽量靠近。
一个面向对象的domain model看起来常常与数据库模型很相似,然而它们之间其实是有很多
不同的。一个Domain Model混合了数据和过程,具有多值的属性(multi-valued attributes),
和可以使用继承。
因为业务行为经常变动,所以把这一层设计得易于修改,构建和测试非常重要。因此你会希
望使Domain Model与系统中其他层的耦合最小化。你会发现许多分层模式的指导思想就是使
domain model与系统其他部分的相互依赖尽可能小。
当你使用Domain Model时候,可能会使用几种不同的规模。最简单的情况是一个单用户的应
用程序,它把整个对象图(译注:object graph,应该是指图论角度上的图,即是读入所有
的对象并建立起它们之间的连接)都从磁盘中读入内存。一个桌面应用程序可能会以这种方
式工作,但在一个多层的IS应用中很会使用它。原因很简单,因为对象太多了。把每个对象
都放进内存消耗太多的时间和占用太多的内存。面向对象数据库的美丽之处在于它们在把对
象从内存和磁盘间来回移动的事情上做得很出色。
离开了一个面向对象数据库(OODB),你必须自己来做这件事。通常每个session都会牵涉到
读入一个由被这个session涉及到的所有对象组成的对象图。当然,这不会是所有的对象
(译注:指整个系统),而且通常不会是所有的类。因此如果你正着眼于一批合同,你可
能只会读入你正在处理的这些合同所涉及的产品对象。如果你仅仅在contracts和revenue recognition(译
注:见上面的关于例子背景的贴子)对象上执行一些计算,你可能根本不会读入任何产品
对象。你的数据库映射对象会精确地管理什么需要读入内存。
如果你在在对服务器的不同调用之间需要使用同一个对象图,你需要把服务器的状态保存在
某处。这是关于保存服务器状态的章节的主题。
一个对domain logic的普遍的担忧是过度膨胀的业务领域对象。当你在创建一个屏幕界面来
处理订单时,你注意到一些订单的行为仅仅在这个屏幕界面中被用到。如果你把这些职责放
进订单类里,你就面临着订单类会变得太大的风险。因为它会被这种仅仅被一个用例所用到
的职责所充斥。这个担忧使人们考虑一些职责是否具有普遍性,如果有,它应该被放进订单
类中;而一些特殊的方法,则应该放进面向特定用途的类中去。这些类可能是一
个Transaction Script或可能是界面本身。(译注:原文为presentation,这里根据上下文
译为界面。但实际上presentation并不特指用户界面,而可能是任何一种对数据的表示方
式)
分离面向特定用途的行为的问题是,它倾向于导致重复代码。从订单类中分离出来的行为很
难被找到,因此人们倾向于看不到它,于是重写一个。这种重复很快会导致更多的复杂性和
前后不一致。另一方面,我发现一个膨胀的业务对象并不如我想像中导致那么多问题。膨胀
也比预测中更少发生。如果膨胀真的发生了,它相对地容易被看到而且不难修正。因此我的
建议是不要尝试分离面向特定用途的行为。把所有行为顺其自然地放在合适的类中。当出现
膨胀时-如果它真的发生的话-再去修正它。
Java
当人们谈到在J2EE中开发Domain Model的时候,总是会情绪激昂起来。许多J2EE的教材和介
绍书籍都建议你使用entity beans来开发一个domain model。然而这种手段有几个严重的问
题。Entity beans是可以远程访问的(remotable),而因此建议使用一个粗糙的接口
(a coarse-grained interface),但一个Domain Model在你使用具有良好接口的对象
(fine grained objects)时才工作得最好。一个解决方案是使用session beans来包装
entity beans,这样entity bean就不能由远程访问了。虽然这是一个不错的做法,但在调
用entity bean的方法时仍然有额外的代价。
另一个entity beans的问题是-至少在J2EE第一版中-由容器管理的持久映射
(container managed persistence mapping)非常有限。这意味着你只能使entity beans一
一对应地映射到数据表,这种做法仅在Active Record风格的映射中才有效。你可以通过使
用由bean管理的持久性(bean managed persistence)获得更多地弹性,但如果你正在使用
BMP,不会远程访问你的entity beans的时候,它们的价值会显著地下降。
没错,entity beans处理内存缓冲。它减少了对Unit of Work(译注:另一种模式,用于管
理并发,它保存一份受事务影响的对象列表,协调数据库更新和解决并发性问题)的需求。
但它们同时非常难以排错,需要使用大量的类,在处理关联和继承的时候非常笨拙,增加了
项目的创建时间(译注:increase the build times,我真的不知道这个build中文怎么译。反正就是在delphi里的Project菜单里Compile选项下面那条选项里的那个build),而且通常会在解决一些性能问题的同时引起同样数量的其他性能问题。
一个替代方法是使用普通的Java对象。这个建议常常会造成惊讶,因为有数量令人吃惊的
人认为你不可能在一个EJB container中使用普通的java对象。我得出这样结论:人们之所
以会忘记普通的java对象,是因为它们没有一个响亮的名字。所以当我在准备一个演讲的时
候,Rebecca Parsons, Josh Mackenzie和我为它们起了一个:POJO (Plain Old Java Object,简
单的旧式java对象)。一个POJO Domain Model易于整合,build起来很快,可以在
EJB Container之外运行和测试,而且不依赖于EJB (也许这正是EJB厂商不鼓励你使用它的
原因)
我曾经见过用entity beans进行的很好的项目,前提是这些项目有着最复杂的业务逻辑而你
只有一个简单的到数据库的连接。大部分情况下,我更喜欢选择POJO的路线。
当然以上种种在EJB 2.0出现时将变得没有意义了。但是,因为EJB 2.0的说明书比一个即将
举行告别音乐会的人有着更多的“最终稿” (译注:反正就是很多啦),对我来说,现在
还很难作出任何评论。我真正希望的是,Domain Model能够尽可能从反复无常的开发平台中
独立出来。
When to Use it
如果说关于怎样使用Domain Model很难(difficult)是因为它是一个如此大的题目,那么关
于何时使用它则更难(hard)因为这类建议都既含糊又简单。一言而蔽之,选择的关键在于你
的系统中行为的复杂度。如果你面临复杂而且不停变动的商业规则,涉及到检验,计算和
派生...这是一个你使用对象模型来处理它们的良好机会。另一方面,如果你仅仅需要一个
简单的非空检验和一些累加计算,那么你最好把赌注押在Transaction Script上。
一个考虑因素是怎样运用正在使用业务对象的开发小组。学习怎样设计和使用一个Domain Model是
一个很具规模的练习 - 它把你带到一大堆关于"paradigm shift" of using object (译
注:不会译
)的文章中。当然,要习惯使用一个Domain Model需要实践和指导,但一旦
你习惯了它,我发现对于任何项目,很少人会回过头去使用Transaction Script - 除非它
是最最简单的问题。
如果你正在使用Domain Model,则我在数据库交互上的首选是Data Mapper。(译注:不要
问我:“你”使用什么关“我”什么事?原文就是这样写的)它帮助你保持Domain Model独
立于数据库。如果在Domain Model与数据库方案有分歧的时候,这是最好的解决办法了。
Example: Revenue Recognition (Java)
在我描述Domain Model中遇到的一个最大的挫败是:任何我展示的例子都必须足够简单,这
样你才能理解它;然而这种简单化隐藏了Domain Model的强大。你仅仅在一个现实的复杂业
务领域中,才可以真正体会到Domain Model的强大。
但即使这样的简单例子不能真正地告诉你为什么要使用一个Domain Model, 最低限度这个例
子能给你一个印象:它看起来象什么。因此在这里我使用了我在Transaction Script中使用
的那个例子-一个关于收入入帐的小事务。你可以比较这两个例子。
图1 : 使用Domain Model时这个例子的类图
http://martinfowler.com/isa/domainModelClasses.gif
有一件事必须在这里说明:每一个类,即使在这么小的例子里,同时包括了行为和数据。即
使最微不足道的Revenue recognition类也包括了一个简单的方法来找出这个对象的值(收
入的金额)在某个日期里是否可以入帐:
class RevenueRecognition...
private Money amount;
private MfDate date;
public RevenueRecognition(Money amount, MfDate date) {
this.amount = amount;
this.date = date;
}
public Money getAmount() {
return amount;
}
boolean isRecognizableBy(MfDate asOf) {
return asOf.after(date) || asOf.equals(date);
}
计算在一个特定日子里有多少收入可以入帐涉及到contract和revenue recognition类
class Contract...
private List revenueRecognitions = new ArrayList();
public Money recognizedRevenue(MfDate asOf) {
Money result = Money.dollars(0);
Iterator it = revenueRecognitions.iterator();
while (it.hasNext()) {
RevenueRecognition r = (RevenueRecognition) it.next();
if (r.isRecognizableBy(asOf))
result = result.add(r.getAmount());
}
return result;
}
在domain model中你会发现一件很普遍的事:多个类相互作用来完成一件即使是最简单的任
务。这常常导致人们抱怨在面向对象的程序中,你花费大量的时间逐个类地搜寻某段程序。
对于这种抱怨可以有很多方面的看法。当决定某件东西能否在某一天入帐的逻辑变得复杂起
来,而且其他对象需要知道这个决定时,它(指类合作)的价值才显现出来。把这个行为放
在需要知道两者的类中能够避免代码重复和减少不同对象之间耦合。 (译注:这段译得不
好,我也没能很好理解。因此原文放在下面,希望有朋友能帮忙修正)
A common thing you find in domain models is how multiple classes interact in order to do even the simplest tasks. This is what often leads to the complaint that with OO programs you spend a lot of time hunting around from class to class trying to find the program. There's a lot of sense to this complaint. The value comes as the decision whether something is recognizable by a certain date gets more complex and as other objects need to know. Containing the behavior on the object that needs to know both avoids duplication and reduces coupling between the different objects.
再看看计算和创建这些revenue recognition对象的代码,更进一步证明了这个关于大量小
对象相互合作的说法。在本例中计算和创建从customer开始,通过product对象传递到一个
strategy层次(继承树)中。strategy模式是一个著名的面向对象模式,它允许你把一组
操作整合到一个小型的类层次中。每一个product实例都连接到一个Recognition Strategy的
实例。这个strategy实例决定使用哪种算法来计算revenue recognition。在本例中我们有
两个Recognition Strategy的子类,对应两种不同的情况。代码的结构看起来象这样:
class Contract...
private Product product;
private Money revenue;
private MfDate whenSigned;
private Long id;
public Contract(Product product, Money revenue, MfDate whenSigned) {
this.product = product;
this.revenue = revenue;
this.whenSigned = whenSigned;
}
class Product...
private String name;
private RecognitionStrategy recognitionStrategy;
public Product(String name, RecognitionStrategy recognitionStrategy) {
this.name = name;
this.recognitionStrategy = recognitionStrategy;
}
public static Product newWordProcessor(String name) {
return new Product(name, new CompleteRecognitionStrategy());
}
public static Product newSpreadsheet(String name) {
return new Product(name, new ThreeWayRecognitionStrategy(60, 90));
}
public static Product newDatabase(String name) {
return new Product(name, new ThreeWayRecognitionStrategy(30, 60));
}
class RecognitionStrategy...
abstract void calculateRevenueRecognitions(Contract contract);
class CompleteRecognitionStrategy...
void calculateRevenueRecognitions(Contract contract) {
contract.addRevenueRecognition(new RevenueRecognition(contract.getRevenue(), contract.getWhenSigned()));
}
class ThreeWayRecognitionStrategy...
private int firstRecognitionOffset;
private int secondRecognitionOffset;
public ThreeWayRecognitionStrategy(int firstRecognitionOffset,
int secondRecognitionOffset)
{
this.firstRecognitionOffset = firstRecognitionOffset;
this.secondRecognitionOffset = secondRecognitionOffset;
}
void calculateRevenueRecognitions(Contract contract) {
Money[] allocation = contract.getRevenue().allocate(3);
contract.addRevenueRecognition(new RevenueRecognition
(allocation[0], contract.getWhenSigned()));
contract.addRevenueRecognition(new RevenueRecognition
(allocation[1], contract.getWhenSigned().addDays(firstRecognitionOffset)));
contract.addRevenueRecognition(new RevenueRecognition
(allocation[2], contract.getWhenSigned().addDays(secondRecognitionOffset)));
}
strategies的巨大价值在于它们提供了一个良好的插口来扩充这个应用程序。增加一个新
的revenue recognition算法只涉及到创建一个新的子类并重载calculateRevenueRecognitions方
法。这样使扩充应用程序的算法行为变得非常方便
当你创建产品对象时,你把它们与一个合适的strategy对象挂钩。在本例中,我在我的测
试代码中这样做:
class Tester...
private Product word = Product.newWordProcessor("Thinking Word");
private Product calc = Product.newSpreadsheet("Thinking Calc");
private Product db = Product.newDatabase("Thinking DB");
一旦所有东西都设置好,那么计算入帐不需要涉及到这些strategy子类的任何细节。
class Contract...
public void calculateRecognitions() {
product.calculateRevenueRecognitions(this);
}
class Product...
void calculateRevenueRecognitions(Contract contract) {
recognitionStrategy.calculateRevenueRecognitions(contract);
}
这种在对象与对象之间连续推进的面向对象的编程习惯不但把一个行为传递到最适合处理
它的对象中,而且解决了大部分的条件分支行为。你会注意到在这个计算中没有条件结构。
选择的路径在product对象被创建的时候就已经通过设置正确的strategy对象定好了。一旦
象这样设置好之后,算法只需要跟随这条路径。Domain Model在你面对一系列相似的条件
分支时工作得很好,因为它把这些相似的条件分支从代码中抽出来放到对象本身的结构中
去。这样就把复杂性从算法中移到了对象间的关系中。逻辑越接近,你会在系统的不同部分
发现越多的同样的关系网络。任何依赖于类型来计算的算法都可以使用这种特定的对象网络
结构。
你会注意到在这个例子中,我没有展示任何关于如何从数据库中存取对象信息的方法。这种
做法有一系列原因,首先把一个Domain Model映射到数据库总是稍微有点难度的,因此我不
敢在这里提供例子(译注:传统说法就是,因为这东西太复杂了,因为篇幅的关系,
所以......)。其次,在许多方面来看,一个Domain Model的主旨是隐藏数据库细节,这个
隐藏不但是对于上层系统,而且是对于使用这些Domain Model本身的编程人员。因此在这里
隐藏了数据库操作正是反映了在真实的编程环境中它看起来是什么样子的。
--------------------------------------------------------------------------------