领域驱动设计(DDD)的理念 - 首先由Eric Evans在他的同名书[1]中描述 - 是关于将我们的把稳力放在运用程序的核心,关注业务领域固有的繁芜性本身。我们还将核心域(业务独占)与支持子域(常日是通用的,如金钱或韶光)区分开来,并将更多的设计事情放在核心上。
域驱动设计包含一组用于从域模型构建企业运用程序的模式。在您的软件生涯中,您可能已经碰着过许多这样的想法,特殊是如果您是OO措辞的履历丰富的开拓职员。但将它们一起运用将许可您构建真正知足业务需求的系统。
在本文中,我将先容DDD的一些紧张模式,理解一些新手彷佛很难办理的问题,并重点先容一些工具和资源(特殊是一个),以帮助您在事情中运用DDD。
代码和模型......
利用DDD,我们希望创建问题域的模型。持久性,用户界面和通报的东西可以在往后涌现,这是须要理解的领域,由于正在构建的系统中,可以区分公司的业务与竞争对手。 (如果不是这样,那么考虑购买包装产品)。
按模型,我们不是指图表或一组图表;确定,图表很有用,但它们不是模型,只是模型的不同视图(拜会图)。不,模型是我们选择在软件中实现的观点集,以代码和用于构建交付系统的任何其他软件工件表示。换句话说,代码便是模型。文本编辑器供应了一种利用此模型的方法,只管当代工具也供应了大量其他可视化(UML类图,实体关系图,Spring beandocs [2],Struts / JSF流等)。
Figure 1: Model vs Views of the Model
这是DDD模式的第一个:模型驱动设计(model-driven design)。这意味着能够将模型中的观点映射到设计/代码的观点(空想情形下)。模型的变革意味着代码的变革;变动代码意味着模型已变动。 DDD并没有逼迫哀求您利用面向工具来构建域 - 例如,我们可以利用规则引擎构建模型 - 但鉴于主流企业编程措辞是基于OO的,大多数模型实质上都是OO。毕竟,OO基于建模范例。模型的观点将表示为类和接口,作为类成员的职责。
措辞现在让我们看一下域驱动设计的另一个基本原则。回顾一下:我们想要构建一个捕获正在构建的系统的问题域的域模型,并且我们将在代码/软件工件中表达这种理解。为了帮助我们做到这一点,DDD提倡领域专家和开拓职员故意识地利用模型中的观点进行沟通。因此,域专家不会根据屏幕或菜单项上的字段描述新的用户故事,而是谈论域工具所需的根本属性或行为。类似地,开拓职员不会谈论数据库表中的类或列的新实例变量。
严格哀求我们开拓一种普世的措辞(ubiquitous language)。如果一个想法不能轻易表达,那么它表明了一个观点,这个观点在领域模型中缺失落,并且团队共同努力找有缺失落的观点是什么。一旦建立了这个,那么数据库表中的屏幕或列上的新字段就会连续显示。
像DDD一样,这种开拓无处不在的措辞的想法并不是一个新想法:XPers称之为“名称系统”,多年来DBA将数据字典组合在一起。但无处不在的措辞是一个令人回味的术语,可以出售给商业和技能职员。现在,“全体团队”敏捷实践正在成为主流,这也很故意义。
模型和高下文......每当我们谈论模型时,它总是在某种情形下。常日可以从利用该系统的终极用户集推断出该高下文。因此,我们有一个支配到交易员的前台交易系统,或超市收银员利用的发卖点系统。这些用户以特定办法与模型的观点干系,并且模型的术语对这些用户故意义,但不一定对该高下文之外的任何其他人故意义。 DDD称之为有界高下文(BC)。每个域模型都只存在于一个BC中,而BC只包含一个域模型。
我必须承认,当我第一次读到关于BC时,我看不出这一点:如果BC与域模型同构,为什么要引入一个新术语?如果只有与BC相互浸染的终极用户,则可能不须要这个术语。然而,不同的系统(BC)也相互交互,发送文件,通报,调用API等。如果我们知道有两个BC相互交互,那么我们知道我们必须把稳在一个观点之间进行转换。领域和其他领域。
在模型周围设置明确的边界也意味着我们可以开始谈论这些BC之间的关系。实际上,DDD确定了BC之间的一整套关系,因此当我们须要将不同的BC链接在一起时,我们可以合理地确定该当做什么:
已发布的措辞:交互式BCs就共同的措辞(例如企业做事总线上的一堆XML模式)达成同等,通过它们可以相互交互;开放主机做事:BC指定任何其他BC可以利用其做事的协议(例如RESTful Web做事);共享内核:两个BC利用一个共同的代码内核(例如一个库)作为一个通用的通用措辞,但是否则以他们自己的特定办法实行其他的东西;客户/供应商:一个BC利用另一个BC的做事,并且是另一个BC的利益干系者(客户)。因此,它可以影响该BC供应的做事;屈服者:一个BC利用另一个BC的做事,但不是其他BC的利益干系者。因此,它利用“原样”(符合)BC供应的协议或API;反堕落层:一个BC利用另一个做事而不是利益干系者,但旨在通过引入一组适配器 - 一个反腐败层来最小化它所依赖的BC变革的影响。你可以看到,在这个列表中,两个BC之间的互助水平逐渐降落(见图2)。利用已发布的措辞(published language),我们从BC建立一个他们可以互动的共同标准开始;既不拥有这种措辞,而是由他们所居住的企业所拥有(乃至可能是行业标准)。有了开放主机做事(open host),我们仍旧做得很好; BC供应其作为任何其他BC调用的运行时做事的功能,但是(可能)随着做事的发展将保持向后兼容性。
Figure 2: Spectrum of Bounded Context Relationship
然而,当我们走向屈服时,我们只是和我们一起生活; 一个BC明显屈从于另一个。 如果我们必须与购买megabucks的总分类帐系统集成,那可能便是我们所处的情形。如果我们利用反腐败层,那么我们常日会与遗留系统集成,但是 额外的层将我们尽可能地隔离开来。 当然,这须要费钱来履行,但它降落了依赖风险。 反腐败层也比重新实现遗留系统便宜很多,这最多会分散我们对核心域的把稳力,最坏的情形因此失落败告终。
DDD建议我们制订一个高下文图(context map t)来识别我们的BC以及我们依赖或依赖的BC,以确定这些依赖关系的性子。 图3显示了我过去5年旁边一贯在研究的系统的高下文映射。
Figure 3: Context Mapping Example
所有这些关于背景图和BC的谈论有时被称为计策性DDD( strategic DDD),并且有充分的情由。 毕竟,当你想到它时,弄清楚BC之间的关系是非常政治的:我的系统将依赖哪些上游系统,我是否随意马虎与它们集成,我是否能够利用它们,我相信它们吗? 下贱也是如此:哪些系统将利用我的做事,我如何将我的功能作为做事公开,他们会对我有利吗? 误解了这一点,您的运用程序可能很随意马虎失落败。
层和六边形现在让我们转向内部并考虑我们自己的BC(系统)的架构。 从根本上说,DDD只关心域层,实际上,它对其他层有很多话要说:表示,运用程序或根本架构(或持久层)。 但它确实期望它们存在。 这是分层架构模式(图4)。
Figure 4: Layered Architecture
当然,我们多年来一贯在构建多层系统,但这并不虞味着我们必须善于它。确实,过去的一些主流技能 - 是的,EJB 2,我正在看着你!
- 对域模型可以作为故意义的层存在的想法产生了积极的影响。所有的业务逻辑彷佛渗透到运用层或(更糟糕的)表示层,留下一组血虚的域类[3]作为数据持有者的空壳。这不是DDD的意思。
因此,要绝对清楚,运用程序层中不应存在任何域逻辑。相反,运用程序层卖力事务管理和安全性等事务。在某些体系构造中,它还可能卖力确保从根本构造/持久层中检索的域工具在与之交互之前已精确初始化(只管我更喜好根本构造层实行此操作)。
在表示层在单独的存储空间中运行的情形下,运用层也充当表示层和域层之间的中介。表示层常日处理域工具或域工具(数据传输工具或DTO)的可序列化表示,常日每个“视图”一个。如果这些被修正,那么表示层会将任何变动发送回运用程序层,而运用程序层又确定已修正的域工具,从持久层加载它们,然后转发对这些域工具的变动。
分层体系构造的一个缺陷是它建议从表示层一贯到根本构造层的依赖性的线性堆叠。但是,我们可能希望在表示层和根本构造层中支持不同的实现。如果(正如我认为的那样!
)我们想要测试我们的运用程序便是这种情形:
我们可能还想区分“内部”和“外部”层之间的交互,个中内部我指的是两个层完备在我们的系统(或BC)内的交互,而外部交互超过BC。
因此,不要将我们的运用程序视为一组图层,另一种方法是将其视为六边形[5],如图5所示。我们的终极用户利用的查看器以及FitNesse测试利用内部客户端API(或端口),而来自其他BC的调用(例如,RESTful用于开放主机交互,或来自ESB适配器的调用用于已发布的措辞交互)命中外部客户端端口。对付后端根本架构层,我们可以看到用于替代工具存储实现的持久性端口,此外,域层中的工具可以通过外部做事端口调用其他BC。
Figure 5: Hexagonal Architecture
但这足够大的东西; 让我们来看看DDD在煤炭面板上的样子。
构建模块正如我们已经把稳到的,大多数DDD系统可能会利用OO范例。因此,我们的域工具的许多构建块可能很熟习,例如实体,值工具和模块(entities, value objects and modules. )。例如,如果您是Java程序员,那么将DDD实体视为与JPA实体基本相同(利用@Entity注释)就足够安全了;值工具是字符串,数字和日期之类的东西;一个模块便是一个包。
但是,DDD方向于更多地强调值工具(value objects ),而不是过去习气。以是,是的,您可以利用String来保存Customer的givenName属性的值,例如,这可能是合理的。但是一笔钱,例如产品的价格呢?我们可以利用int或double,但是(乃至忽略可能的舍入缺点)1或1.0是什么意思? $ 1吗? €1? ¥1? 1分,乃至?相反,我们该当引入一个Money值类型,它封装了Currency和任何舍入规则(将特定于Currency)。
而且,值工具该当是不可变的,并且该当供应一组无副浸染的函数来操作它们。我们该当写:
Money m1 = new Money(\公众GBP\"大众, 10);Money m2 = new Money(\"大众GBP\公众, 20);Money m3 = m1.add(m2);
将m2添加到m1不会改变m1,而是返回一个新的Money工具(由m3引用),它表示一起添加的两个Money。
值也该当具有值语义,这意味着(例如在Java和C#中)它们实现equals()和hashCode()。它们常日也可以序列化,可以是字节流,也可以是String格式。当我们须要坚持它们时,这很有用。
值工具常见的另一种情形是标识符。因此,(US)SocialSecurityNumber将是一个很好的例子,车辆的RegistrationNumber也是如此。 URL也是如此。由于我们已经重写了equals()和hashCode(),以是这些都可以安全地用作哈希映命中的键。
引入代价工具不仅扩展了我们无处不在的措辞,还意味着我们可以将行为推向代价不雅观本身。因此,如果我们确定Money永久不会包含负值,我们可以在Money内部实现此检讨,而不是在利用Money的任何地方。如果SocialSecurityNumber具有校验和数字(在某些国家/地区便是这种情形),则该校验和的验证可以在值工具中。我们可以哀求URL验证其格式,返回其方案(例如http),或者确定相对付其他URL的资源位置。
我们的其余两个构建块可能须要更少的阐明。实体常日是持久的,常日是可变的并且(因此)方向于具有生平的状态变革。在许多体系构造中,实体将作为行保存在数据库表中。同时,模块(包或命名空间)是确保域模型保持解耦的关键,并且不会成为泥浆中的一大块[6]。在他的书中,埃文斯谈到观点轮廓,这是一个优雅的短语,用于描述如何区分域的紧张关注领域。模块是实现这种分离的紧张办法,以及确保模块依赖性严格非循环的接口。我们利用诸如Uncle“Bob”Martin的依赖颠倒原则[7]之类的技能来确保依赖关系是严格单向的。
实体,值和模块是核心构建块,但DDD还有一些不太熟习的构建块。我们现在来看看这些。
聚合和聚合根如果您精通UML,那么您将记住,它许可我们将两个工具之间的关联建模为大略关联,聚合或利用组合。聚合根(有时缩写为AR)是通过组合组成其他实体(以及它自己的值)的实体。也便是说,聚合实体仅由根引用(可能是可通报的),并且可能不会被聚合外的任何工具(永久地)引用。换句话说,如果实体具有对另一个实体的引用,则引用的实体必须位于同一聚合内,或者是某个其他聚合的根。
许多实体是聚合根,不包含其他实体。对付不可变的实体(相称于数据库中的引用或静态数据)尤其如此。示例可能包括Country,VehicleModel,TaxRate,Category,BookTitle等。
但是,更繁芜的可变(事务)实体在建模为聚合时确实会受益,紧张是通过减少观点开销。我们不必考虑每个实体,而只考虑聚合根;聚合实体仅仅是聚合的“内部运作”。它们还简化了实体之间的相互浸染;我们遵照以下规则:(持久化)引用可能只是聚合的根,而不是聚合中的任何其他实体。
另一个DDD原则是聚合根卖力确保聚合实体始终处于有效状态。例如,Order(root)可能包含OrderItems的凑集(聚合)。可能存在以下规则:订单发货后,任何OrderItem都无法更新。或者,如果两个OrderItem引用相同的产品并具有相同的运输哀求,则它们将合并到同一个OrderItem中。或者,Order的派生totalPrice属性该当是OrderItems的价格总和。掩护这些不变量是root的任务。
但是......只有聚合根才能完备在聚合中掩护工具之间的不变量。 OrderItem引用的产品险些肯定不会在AR中,由于还有其他用例须要与Product进行交互,而不管是否有订单。因此,如果有一条规则不能对已停产的产品下达订单,那么订单将须要以某种办法处理。实际上,这常日意味着在订单交易更新时利用隔离级别2或3来“锁定”产品。或者,可以利用带外过程来折衷交叉聚合不变量的任何毁坏。
在我们连续提高之前退一步,我们可以看到我们有一系列粒度:
value < entity < aggregate < module < bounded context
现在让我们连续研究一些DDD构建块。
存储库,工厂和做事(Repositories, Factories and Services)在企业运用程序中,实体常日是持久的,其值表示这些实体的状态。但是,我们如何从持久性存储中获取实体呢?
存储库是持久性存储的抽象,返回实体 - 或者更确切地说是聚合根 - 知足某些标准。例如,客户存储库将返回Customer聚合根实体,订单存储库将返回Orders(及其OrderItems)。常日,每个聚合根有一个存储库。
由于我们常日希望支持持久性存储的多个实现,以是存储库常日由具有不同持久性存储实现的不同实现的接口(例如,CustomerRepository)组成(例如,CustomerRepositoryHibernate或CustomerRepositoryInMemory)。由于此接口返回实体(域层的一部分),因此接口本身也是域层的一部分。接口的实现(与一些特定的持久性实现耦合)是根本构造层的一部分。
我们搜索的标准常日隐含在名为的方法名称中。因此,CustomerRepository可能会供应findByLastName(String)方法来返回具有指定姓氏的Customer实体。或者我们可以让OrderRepository返回Orders,findByOrderNum(OrderNum)返回与OrderNum匹配的Order(请把稳,这里利用值类型!
)。
更繁芜的设计将标准包装到查询或规范中,类似于findBy(Query <T>),个中Query包含描述标准的抽象语法树。然后,不同的实现解包查询以确定如何以他们自己的特定办法定位知足条件的实体。
也便是说,如果你是.NET开拓职员,那么值得一提的是LINQ [8]。由于LINQ本身是可插拔的,以是我们常日可以利用LINQ编写存储库的单个实现。然后变革的不是存储库实现,而是我们配置LINQ以获取其数据源的办法(例如,针对Entity Framework或针对内存中的工具库)。
每个聚合根利用特定存储库接口的变体是利用通用存储库,例如Repository <Customer>。这供应了一组通用方法,例如每个实体的findById(int)。当利用Query <T>(例如Query <Customer>)工具指定条件时,这很有效。对付Java平台,还有一些框架,例如Hades [9],许可稠浊和匹配方法(从通用实现开始,然后在须要时添加自定义接口)。
存储库不是从持久层引入工具的唯一方法。如果利用工具关系映射(ORM)工具(如Hibernate),我们可以在实体之间导航引用,许可我们透明地遍历图形。根据履历,对其他实体的聚合根的引用该当是延迟加载的,而聚合中的聚合实体该当被迫切加载。但与ORM一样,期望进行一些调度,以便为最关键的用例得到得当的性能特色。
在大多数设计中,存储库还用于保存新实例,以及更新或删除现有实例。如果底层持久性技能支持它,那么它们很可能存在于通用存储库中,但是从方法署名的角度来看,没有什么可以区分保存新客户和保存新订单。
末了一点......直接创建新的聚合根很少见。相反,它们方向于由其他聚合根创建。订单便是一个很好的例子:它可能是通过客户调用一个动作来创建的。
这整洁地带给我们:
工厂如果我们哀求Order创建一个OrderItem,那么(由于毕竟OrderItem是其聚合的一部分),Order知道要实例化的详细OrderItem类是合理的。实际上,实体知道它须要实例化的同一模块(命名空间或包)中的任何实体的详细类是合理的。
假设客户利用Customer的placeOrder操作创建订单(拜会图6)。如果客户知道详细的订单类,则意味着客户模块依赖于订单模块。如果订单具有对客户的反向引用,那么我们将在两个模块之间得到循环依赖。
Figure 6: Customers and Orders (cyclic dependencie
如前所述,我们可以利用依赖性反转原则来办理这类问题:从订单中删除依赖关系 - >客户模块我们将引入OrderOwner接口,使Order引用为OrderOwner,并使Customer实现OrderOwner(拜会图7))。
Figure 7: Customers and Orders (customer depends o
那么另一种办法呢:如果我们想要订单 - >客户? 在这种情形下,须要在客户模块中有一个表示Order的接口(这是Customer的placeOrder操作的返回类型)。 然后,订单模块将供应订单的实现。 由于客户不能依赖订单,因此必须定义OrderFactory接口。 然后,订单模块依次供应OrderFactory的实现(拜会图8)。
可能还有相应的存储库接口。例如,如果客户可能有数千个订单,那么我们可能会删除其订单凑集。相反,客户将利用OrderRepository根据须要定位其订单(的一部分)。或者(如某些人所愿),您可以通过将对存储库的调用移动到运用程序体系构造的更高层(例如域做事或运用程序做事)来避免从实体到存储库的显式依赖性。
实际上,做事是我们须要探索的下一个话题。
域做事,根本构造做事和运用程序做事(Domain services, Infrastructure services and Application services)域做事(domain service)是在域层内定义的域做事,但实现可以是根本构造层的一部分。存储库是域做事,实在现确实在根本构造层中,而工厂也是域做事,实在现常日在域层内。特殊是在适当的模块中定义了存储库和工厂:CustomerRepository位于客户模块中,依此类推。
更一样平常地说,域做事是任何不随意马虎在实体中生存的业务逻辑。埃文斯建议在两个银行账户之间进行转账做事,但我不愿定这是最好的例子(我会将转账本身建模为一个实体)。但另一种域做事是一种充当其他有界高下文的代理。例如,我们可能希望与暴露开放主机做事的General Ledger系统集成。我们可以定义一个公开我们须要的功能的做事,以便我们的运用程序可以将条款发布到总帐。这些做事有时会定义自己的实体,这些实体可能会持久化;这些实体实际上影响了在另一个BC中远程保存的显著信息。
我们还可以得到技能性更强的做事,例如发送电子邮件或SMS文本,或将Correspondence实体转换为PDF,或利用条形码标记天生的PDF。接口在域层中定义,但实现在根本架构层中非常明确。由于这些非常技能性做事的接口常日是根据大略的值类型(而不是实体)来定义的,以是我方向于利用术语根本构造做事(infrastructure service)而不是域做事。但是如果你想成为一个“电子邮件”BC或“SMS”BC的桥梁,你可以想到它们。
虽然域做事既可以调用域实体也可以调用域实体,但运用做事(application service)位于域层之上,因此域层内的实体不能调用,只能反过来调用。换句话说,运用层(我们的分层架构)可以被认为是一组(无状态)运用做事。
如前所述,运用程序做事常日处理交叉和安全等交叉问题。他们还可以通过以下办法与表示层进行调度:解组入站要求;利用域做事(存储库或工厂)获取对与之交互的聚合根的引用;在该聚合根上调用适当的操作;并将结果编组回表示层。
我还该当指出,在某些体系构造中,运用程序做事调用根本构造做事。因此,运用做事可以直接调用PdfGenerationService,通报从实体中提取的信息,而不是实体调用PdfGenerationService将其自身转换为PDF。这不是我的特殊偏好,但它是一种常见的设计。我很快就会商到这一点。
好的,这完成了我们对紧张DDD模式的概述。在Evans 500 +页面书中还有更多内容 - 值得一读 - 但我接下来要做的是突出显示人们彷佛很难运用DDD的一些领域。
问题和障碍履行分层架构这是第一件事:严格实行架构分层可能很困难。特殊是,从域层到运用层的业务逻辑渗透可能特殊暗藏。
我已经在这里挑出了Java的EJB2作为罪魁罪魁,但是模型 - 视图 - 掌握器模式的不良实现也可能导致这种情形发生。掌握器(=运用层)会发生什么,承担太多任务,让模型(=域层)变得血虚。事实上,有更新的Web框架(在Java天下中,Wicket [10]是一个崭露锋芒的例子),出于这种缘故原由明确地避免了MVC模式。
表示层模糊了域层另一个问题是考试测验开拓无处不在的措辞。领域专家在屏幕方面发言是很自然的,由于毕竟,这便是他们可以看到的系统。哀求他们在屏幕后面查看并在域观点方面表达他们的问题可能非常困难。
表示层本身也可能存在问题,由于自定义表示层可能无法准确反响(可能会扭曲)底层域观点,从而毁坏我们无处不在的措辞。纵然不是这种情形,也只须要将用户界面组合在一起所需的韶光。利用敏捷术语,速率降落意味着每次迭代的进度较少,因此对全体域的深入理解较少。
存储库模式的实现从更技能性的角度来看,新手有时彷佛也会稠浊将存储库(在域层中)与实在现(在根本架构层中)的接口分离出来。我不愿定为什么会这样:毕竟,这是一个非常大略的OO模式。我想这可能是由于埃文斯的书并没有达到这个细节水平,这让一些人变得高高在上。但这也可能是由于更换持久性实现(根据六边形体系构造)的想法并不普遍,导致持久性实现渗透到域层的系统。
做事依赖项的实现另一个技能问题 - 在DDD从业者之间可能存在不合 - 就实体与域/根本举动步伐做事(包括存储库和工厂)之间的关系而言。有些人认为实体根本不应该依赖域做事,但如果是这种情形,则外部运用程序做事与域做事交互并将结果通报给域实体。根据我的思维办法,这使我们走向了一个血虚的领域模型。
轻微柔和的不雅观点是实体可以依赖于域做事,但运用程序做事该当根据须要通报它们,例如作为操作的参数。我也不喜好这个:对我而言,它将实现细节暴露给运用层(“这个实体须要这样一个做事才能完成这个操作”)。但是许多从业者对这种方法感到满意。
我自己的首选方案是利用依赖注入将做事注入实体。实体可以声明它们的依赖关系,然后根本构造层(例如Hibernate,Spring或其他一些框架)可以将做事注入实体:
public class Customer {… private OrderFactory orderFactory; public void setOrderFactory(OrderFactory orderFactory) { this.orderFactory = orderFactory; } … public Order placeOrder( … ) { Order order = orderFactory.createOrder(); … return order; } }
一种替代方法是利用做事定位器模式。例如,将所有做事注册到JNDI中,然后每个域工具查找它所需的做事。在我看来,这引入了对运行时环境的依赖。但是,与依赖注入比较,它对实体的内存需求较低,这可能是一个决定性成分。
不得当的模块化正如我们已经确定的那样,DDD在实体之上区分了几种不同的粒度级别,即聚合,模块和BC。得到精确的模块化水平须要一些练习。正如RDBMS模式可能被非规范化一样,系统也没有模块化(成为泥浆的大球)。但是,过度规范化的RDBMS模式(个中单个实体在多个表上被分解)也可能是有害的,过模块化系统也是如此,由于它变得难以理解系统如何作为整体事情。
我们首先考虑模块和BC。记住,模块类似于Java包或.NET命名空间。我们希望两个模块之间的依赖关系是非循环的,但是如果我们确定(比如说)客户依赖于订单,那么我们不须要做任何额外的事情:客户可以大略地导入Order包/命名空间并利用它接口和类根据须要。
但是,如果我们将客户和订单放入单独的BC中,那么我们还有更多的事情要做,由于我们必须将客户BC中的观点映射到BC订单的观点。在实践中,这还意味着在客户BC中具有订单实体的表示(根据前面给出的总分类帐示例),以及通过总线或其他东西实际协作的机制。请记住:拥有两个BC的缘故原由是当有不同的终极用户和/或利益干系者时,我们无法担保不同BC中的干系观点将朝着相同的方向发展。
另一个可能存在稠浊的领域是将实体与聚合区分开来。每个聚合都有一个实体作为其聚合根,对付很多很多实体,聚合将只包含这个实体(“噜苏”的情形,正如数学家所说的那样)。但我看到开拓职员认为全体天下必须存在于一个聚合中。因此,例如,订单包含引用产品的OrderItems(到目前为止一贯很好),因此开拓职员得出结论,产品也在聚合中(不!
)更糟糕的是,开拓职员会不雅观察到客户有订单,以是想想这个意味着我们必须拥有Customer / Order / OrderItem / Product的巨型聚合(不,不,不!
)。关键是“客户有订单”并不虞味着暗示汇总;客户,订单和产品都是凑集的根源。
实际上,一个范例的模块(这是非常粗糙和准备好的)可能包含六个聚合,每个聚合可能包含一个实体和几个实体之间。在这六个中,一个好的数字可能是不可变的“参考数据”类。还要记住,我们模块化的缘故原由是我们可以理解一件事(在一定的粒度级别)。以是要记住,范例的人一次只能保持在5到9个之间[11]。
入门正如我在开始时所说,你可能在DDD之前碰着过很多想法。事实上,我所说过的每一个Smalltalker(我不是一个,我不敢说)彷佛很高兴能够在EJB2等人的荒野岁月之后回归域驱动的方法。
另一方面,如果这些东西是新的怎么办?有这么多不同的办法来绊倒,有没有办法可靠地开始利用DDD?
如果你环顾一下Java领域(对.NET来说并不那么糟糕),实际上有数百个用于构建Web运用程序的框架(JSP,Struts,JSF,Spring MVC,Seam,Wicket,Tapestry等)。从持久性角度(JDO,JPA,Hibernate,iBatis,TopLink,JCloud等)或其他问题(RestEasy,Camel,ServiceMix,Mule等),有很多针对根本架构层的框架。但是很少有框架或工具来帮助DDD所说的最主要的层,即域层。
自2002年以来,我一贯参与(现在是一个提交者)一个名为Naked Objects的项目,Java上的开源[12]和.NET上的商业[13]。虽然Naked Objects没有明确地开始考虑领域驱动的设计 - 事实上它早于Evans的书 - 它与DDD的事理非常相似。它还可以轻松战胜前面提到的障碍。
您可以将Naked Objects视为与Hibernate等ORM类似。 ORM构建域工具的元模型并利用它来自动将域工具持久保存到RDBMS,而Naked Objects构建元模型并利用它在面向工具的用户界面中自动呈现这些域工具。
开箱即用的Naked Objects支持两个用户界面,一个富客户端查看器(拜会图9)和一个HTML查看器(拜会图10)。这些都是功能完备的运用程序,须要开拓职员只编写要运行的域层(实体,值,存储库,工厂,做事)。
Figure 9: Naked Objects Drag-n-Drop Viewer
我们来看看Claim类的(Java)代码(如屏幕截图所示)。首先,这些类基本上是pojos,只管我们常日从便捷类AbstractDomainObject继续,只是为了分解注入通用存储库并供应一些帮助方法:
public class Claim extends AbstractDomainObject {...}Next, we have some value properties:// {{ Descriptionprivate String description;@MemberOrder(sequence = \公众1\公众)public String getDescription() { return description; }public void setDescription(String d) { description = d; }// }}// {{ Dateprivate Date date;@MemberOrder(sequence=\公众2\"大众)public Date getDate() { return date; }public void setDate(Date d) { date = d; }// }}// {{ Statusprivate String status;@Disabled@MemberOrder(sequence = \"大众3\"大众)public String getStatus() { return status; }public void setStatus(String s) { status = s; }// }}
这些是大略的getter / setter,返回类型为String,日期,整数等(只管Naked Objects也支持自定义值类型)。接下来,我们有一些参考属性:
// {{ Claimantprivate Claimant claimant;@Disabled@MemberOrder(sequence = \"大众4\公众)public Claimant getClaimant() { return claimant; }public void setClaimant(Claimant c) { claimant = c; }// }}// {{ Approverprivate Approver approver;@Disabled@MemberOrder(sequence = \公众5\公众)public Approver getApprover() { return approver; }public void setApprover(Approver a) { approver = a; }// }}
这里我们的Claim实体引用其他实体。实际上,Claimant和Approver是接口,因此这许可我们将域模型分解为模块,如前所述。
实体也可以拥有实体凑集。在我们的案例中,Claim有一个ClaimItems的凑集:
// {{ Itemsprivate List<ClaimItem> items = newArrayList<ClaimItem>();@MemberOrder(sequence = \"大众6\"大众)public List<ClaimItem> getItems() { return items; }public void addToItems(ClaimItem item) {items.add(item);}// }}
我们还有(Naked Objects调用的)动作,即submit和addItem:这些都是不代表属性和凑集的公共方法:
// {{ action: addItempublic void addItem(@Named(\"大众Days since\公众)int days,@Named(\"大众Amount\"大众)double amount,@Named(\"大众Description\公众)String description) {ClaimItem claimItem = newTransientInstance(ClaimItem.class);Date date = new Date();date = date.add(0,0, days);claimItem.setDateIncurred(date);claimItem.setDescription(description);claimItem.setAmount(new Money(amount, \公众USD\"大众));persist(claimItem);addToItems(claimItem);}public String disableAddItem() {return \"大众Submitted\"大众.equals(getStatus()) ? \公众Alreadysubmitted\"大众 : null;}// }}// {{ action: Submitpublic void submit(Approver approver) {setStatus(\公众Submitted\"大众);setApprover(approver);}public String disableSubmit() {return getStatus().equals(\"大众New\"大众)?null : \"大众Claim has already been submitted\"大众;}public Object[] defaultSubmit() {return new Object[] { getClaimant().getApprover() };}// }}
这些操作会在Naked Objects查看器中自动呈现为菜单项或链接。而这些行动的存在意味着Naked Objects运用程序不仅仅是CRUD风格的运用程序。
末了,有一些支持方法可以显示标签(或标题)并挂钩持久性生命周期:
// {{ Titlepublic String title() {return getStatus() + \"大众 - \公众 + getDate();}// }}// {{ Lifecyclepublic void created() {status = \公众New\"大众;date = new Date();}// }}
之前我将Naked Objects域工具描述为pojos,但您会把稳到我们利用注释(例如@Disabled)以及命令式帮助器方法(例如disableSubmit())来逼迫实行业务约束。 Naked Objects查看器通过查询启动时构建的元模型来尊重这些语义。如果您不喜好这些编程约定,则可以变动它们。
范例的Naked Objects运用程序由一组域类组成,例如上面的Claim类,以及存储库,工厂和域/根本构造做事的接口和实现。特殊是,没有表示层或运用层代码。那么Naked Objects如何帮助办理我们已经确定的一些障碍?
履行分层架构:由于我们编写的唯一代码是域工具,域逻辑无法渗透到其他层。实际上,Naked Objects最初的动机之一便是帮助开拓行为完全的工具表示层模糊了域层:由于表示层是域工具的直接反响,全体团队可以迅速加深对域模型的理解。默认情形下,Naked Objects直接从代码中获取类名和方法名,因此强烈哀求在无处不在的措辞中得到命名权。通过这种办法,Naked Objects也支持DDD的模型驱动设计事理存储库模式的实现:您可以在屏幕截图中看到的图标/链接实际上是存储库:EmployeeRepository和ClaimRepository。 Naked Objects支持可插入工具存储,常日在原型设计中,我们利用针对内存中工具存储的实现。当我们转向生产时,我们会编写一个实现数据库的实现。做事依赖项的实现:Naked Objects会自动将做事依赖项注入每个域工具。这是在从工具库中检索工具时,或者首次创建工具时完成的(请参阅上面的newTransientInstance())。事实上,这些赞助方法所做的便是委托Naked Objects供应的名为DomainObjectContainer的通用存储库/工厂。不得当的模块化:我们可以通过正常办法利用Java包(或.NET命名空间)模块化为模块,并利用Structure101 [14]和NDepend [15]等可视化工具来确保我们的代码库中没有循环依赖。我们可以通过注释@Hidden来模块化为聚合,任何聚合工具代表我们可见聚合根的内部事情;这些将不会涌如今Naked Objects查看器中。我们可以编写域和根本举动步伐做事,以便根据须要桥接到其他BC。Naked Objects供应了许多其他功能:它具有可扩展的体系构造 - 特殊是 - 许可实现其他查看器和工具存储。正在开拓的下一代不雅观众(例如Scimpi [16])供应更繁芜的定制功能。此外,它还供应多种支配选项:例如,您可以利用Naked Objects进行原型设计,然后在进行生产时开拓自己的定制表示层。它还与FitNesse [17]等工具集成,可以自动为域工具供应RESTful接口[18]。下一步领域驱动的设计搜集了一组用于开拓繁芜企业运用程序的最佳实践模式。一些开拓职员多年来一贯在运用这些模式,对付这些人来说,DDD可能只是对他们现有实践切实其实定。但对付其他人来说,运用这些模式可能是一个真正的寻衅。
Naked Objects为Java和.NET供应了一个框架,通过处理其他层,团队可以专注于主要的部分,即域模型。通过直接在UI中公开域工具,Naked Objects许可团队非常自然地构建一个明确无处不在的措辞。随着域层的建立,团队可以根据须要开拓更加量身定制的表示层。
那么,下一步呢?
嗯,DDD本身的圣经是埃里克埃文斯的原着,“领域驱动设计”[1],建议阅读所有人。雅虎新闻组DDD [19]也是一个非常好的资源。如果你有兴趣理解Naked Objects的更多信息,你可以搜索我的书“利用Naked Objects的域驱动设计”[20],或者我的博客[21](NO for Java)或Naked Objects网站[13 ](对付.NET而言)。快乐DDD'ing!