【系统设想】范畴驱动设想简介

3小时前 (20:37:08)阅读1回复0
dyyh
dyyh
  • 管理员
  • 注册排名7
  • 经验值118995
  • 级别管理员
  • 主题23799
  • 回复0
楼主

今天的企业利用法式无疑是复杂的,并依靠一些专门手艺(耐久性,AJAX,Web办事等)来完成它们的工做。做为开发人员,我们倾向于存眷那些手艺细节是能够理解的。但事实是,一个不克不及处理营业需求的系统对任何人都没有用,无论它看起来多么标致或者若何很好地构建其根底设备。

范畴驱动设想(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)的可序列化表达,凡是每个“视图”一个。假设那些被修改,那么表达层会将任何更改发送回利用法式层,而利用法式层又确定已修改的域对象,从耐久层加载它们,然后转发对那些域对象的更改。

分层系统构造的一个缺点是它定见从表达层不断到根底构造层的依靠性的线性堆叠。但是,我们可能期看在表达层和根底构造层中撑持差别的实现。假设(正如我认为的那样!)我们想要测试我们的利用法式就是那种情状:

例如,FitNesse [4]等东西容许我们从最末用户的角度验证我们系统的行为。但是那些东西凡是不会通过表达层,而是间接进进下一层,即便用层。所以从某种意义上说,FitNesse就是另一种看察者。同样,我们可能有多个耐久性实现。我们的消费实现可能利用RDBMS或类似手艺,但是关于测试和原型设想,我们可能有一个轻量级实现(以至可能在内存中),因而我们能够模仿耐久性。我们可能还想区分“内部”和“外部”层之间的交互,此中内部我指的是两个层完全在我们的系统(或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验证其格局,返回其计划(例如的资本位置。

我们的别的两个构建块可能需要更少的阐明。实体凡是是耐久的,凡是是可变的而且(因而)倾向于具有一生的形态改变。在许多系统构造中,实体将做为行保留在数据库表中。同时,模块(包或定名空间)是确保域模子连结解耦的关键,而且不会成为泥浆中的一大块[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 ListClaimItem items = newArrayListClaimItem();@MemberOrder(sequence = "6")public ListClaimItem 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!

References[1] Domain Driven Design Community

0
回帖

【系统设想】范畴驱动设想简介 期待您的回复!

取消