大神三分钟搞懂领域驱动设计
今天的企业应用程序无疑是复杂的,并依赖一些专门技术(持久性,AJAX,Web服务等)来完成它们的工作。作为开发人员,我们倾向于关注这些技术细节是可以理解的。但事实是,一个不能解决业务需求的系统对任何人都没有用,无论它看起来多么漂亮或者如何很好地构建其基础设施。
领域驱动设计(DDD)的理念 - 首先由Eric Evans在他的同名书[1]中描述 - 是关于将我们的注意力放在应用程序的核心,关注业务领域固有的复杂性本身。我们还将核心域(业务独有)与支持子域(通常是通用的,如金钱或时间)区分开来,并将更多的设计工作放在核心上。
域驱动设计包含一组用于从域模型构建企业应用程序的模式。在您的软件生涯中,您可能已经遇到过许多这样的想法,特别是如果您是OO语言的经验丰富的开发人员。但将它们一起应用将允许您构建真正满足业务需求的系统。
代码和模型......
使用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验证其格式,返回其方案(例如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允许团队非常自然地构建一个明确无处不在的语言。随着域层的建立,团队可以根据需要开发更加量身定制的表示层。
「干货分享」vSAN常见错误故障排错
摘要
本次主要分享vSAN常见故障排除,其中包括:vSAN创建VM全过程介绍,vSAN排错方法论和vSAN常用排错工具。
vSAN Software Architecture
About vSAN
vSAN是软件定义的对象存储,VMware的对象存储和虚拟化的产品是紧密的结合在一起的,它实际上是将本机磁盘组中的硬盘聚集起来打造的虚拟的软件定义的共享存储。这个环境中只有主机、服务器,没有第三方的硬件存储。
传统存储如果用的是共享存储,服务器连接到LUN,然后在LUN中创建VMFS文件系统,文件系统中有虚拟机的文件夹,由vmkernel进行虚拟机文件I/O。
vSAN中不再以文件的形式进行数据存取,vSAN创建之后有个vSAN Datastore,这个DataStore中存放着5类虚拟机的对象,分别是NameSpace、VMDK、快照、内存以及交换文件。
vSAN数据保护和性能提升主要通过软件层面的策略来实现,由策略定义性能和可用性等。上图是创建vSAN存储策略的界面,可以在此进行各种策略的配置。
Virtual Machine Storage Policy Capabilites for vSAN
可用性最基本的指标就是数据有多个副本,比如RAID 1可以有两个数据副本。在vSAN中通过PFTT策略来保证可用性,即容忍错误的数量是多少,如果为0 就表示不能容错,数据只有一份拷贝,1表示容忍出错1次,数据有两份拷贝。PFTT默认为1,相当于实现了RAID 1的效果,最大可以设置为3。
在RAID中性能的提升需要依靠RAID 0,RAID 0是将数据切成多个条带来进行保存。vSAN中也能将数据切分成多个条带,最多12份进行同时写。
vSAN Architecture Components
vSAN中有这样几个软件组件。CLOM(集群级别的对象管理器),DOM(分布式对象管理器),LSOM(本地日志结构对象管理器),CMMDS(集群成员监视和目录服务)。
更形象一点的描述,CLOM可以理解为架构师,DOM是承包商,LSOM则是Worker,最后的CMMDS为项目经理。
CLOM and Its Role: Architect
CLOM会根据创建的存储策略决定对象是否能基于策略被创建出来,即策略会不会生效。比如副本数是3,要生成4分拷贝,但是集群中只有3台主机,很明显此时的策略无法生效,因为没有充足的主机提供使用。CLOM还会检测整个集群范围内主机的负载情况,将对象及其组件分散到不同的主机上,并且当组件出现问题要进行修复的时候将决定该组件在哪些主机上重建。
CLOM组件在后台有着一个进程,所以一定要保证主机上的这个进程没有出现问题。由于该进程是运行在每个vSAN的节点之上,因此可以通过/etc/init.d/clomd来查看它的当前状态。
DOM and Its Role: Contractor
DOM也是运行在集群中的每台主机上。DOM会接收来自CLOM的指令,并着手实施,它会找LSOM来真正的干活。前面提到的5类对象中都有一个DOM owner,用来审核针对该对象的IO操作,决定是否能够执行,如果IO操作被允许就由DOM client来执行。
LSOM and Its Role: Worker
实际的I/O操作会被DOM分配到LSOM上,由于LSOM会对设备直接进行I/O,所以它是运行在某个主机的内核空间中的,而没有进程。
CMMDS and Its Role: Project Manager
CMMDS能够告诉我们整个vSAN集群拓扑的全貌和对象的状态,包括集群中的服务器、网络、硬盘设备,对象元数据信息,新增或删除主机等,它还会定义集群中的三个角色master、backup、agent,master负责管理整个vSAN集群的业务,backup是master的备份。
我们简单的梳理下这几个组件之间的交互,首先由CLOM接到请求创建对象开始,如果根据策略能创建它就会将需求发给DOM,由DOM进行组件的创建,DOM决定好要创建哪个组件之后将需求发送给LSOM,LSOM跟存储层(SSD、硬盘)进行交互执行具体的I/O。另外的CMMDS会枚举出整个集群中的可用资源,以及这些资源的拓扑和可用情况。
Virtual Machine Creation
虚拟机创建的时候,首先vCenter的vpxd进程会和主机进行通信,选择某个主机创建虚拟机存储。主机上的vpxa进程接收到vpxd发出的请求后,CMMDS会创建策略,主机根据策略创建虚拟机及其关联的vmdk。由于vmdk是对象,因此要由CLOM根据策略来决定是否能创建该对象及其组件,当组件的创建的位置被决定好之后CMMDS会更新CLOM发出的组件拓扑信息。
另外主机上的DOM接收到CLOM发出的信息后,将创建对象组件的要求下发到本地LSOM上,最后LSOM通过本地存储来创建虚拟机的存储对象。
About Object
Home namesace对象其实是一个小的虚拟机文件夹VMFS文件系统,VMDK、Swap、Snapshot deltas、VM memory这4个对象对应的是原来系统中的4个大文件。
这些对象不是直接放在硬盘上,而是分成若干个组件的形式写入存储,这是为了实现RAID、性能以及可用性。具体的切分方式和存储策略相关,比如要实现RAID 1就将数据复制成两个组件来写(未计入Witness组件),既实现RAID 0又实现RAID 1则要4个组件。
上图是RAID 1的组织结构,很明显的看到有两个Component组件,细心的朋友可能发现了这里还多了个witness(仲裁组件)。RAID 1的两个副本中如果其中之一损坏了,就无法进行读,因为此时不能确定哪个副本是完好的。Witness的存在正是为了解决这一问题,它的投票直接决定了哪个组件可用。
Componet Count
下面我们结合具体的例子来看下不同策略下对象和组件到底是如何创建的。(以下组件的计算都不包含witness)。
首先是PFTT等于0(容错为0),FTM为RAID 1,条带为1的情况,此时的硬盘会写1个组件,因为只有1份拷贝。
PFTT等于1(容错为1),FTM为RAID 1,条带为1的情况下,硬盘会写2个组件(拷贝为2)。
PFTT等于2(容错为2),FTM为RAID 1,条带为1的情况下,硬盘会写3个组件。需要注意的是这里的witness会有两个。
PFTT等于1(容错为1),FTM为RAID 1,条带为2的情况下。因为这里的数据有2份拷贝,所以有2个Mirror,同时条带又为2,因此Mirror将会被拆成两份。总结起来一共有4个组件。
PFTT等于2(容错为2),FTM为RAID 1,条带为3的情况下.。根据上面的计算规律可以很轻松的计算出,此时的组件数量应该为9。
需要提到的是默认情况下组件最大为255G,如果某个VMDK对象大小超过255G,就会被平均拆成多份。
同样是PFTT等于1(容错为1),FTM为RAID 1,条带为1的情况。此时由于硬盘大小为400G,超过了默认的255G,所以每个盘会被拆分成两份,每份200G。一共是4个组件。
这里是PFTT等于0(容错为0),FTM为RAID 1,条带为1的情况,因为是600G的硬盘,所以要被平均拆分成3份(注:是每个不超过255G)。
Object Inaccessibility
虚拟机无法启动有各种原因,如果是vSAN存储问题就可能是由于VMDK对象无法访问引起的。组件能否使用依赖于DOM,DOM会确认对象或组件是在线还是离线,如果是离线就无法访问。离线原因可能是组件自身发生损坏,也可能与组件的健康状态有关,比如LSOM组件或数据出现问题。数据的问题有两方面的原因,一方面是数据本身被破坏,另一方面是数据同步有问题。所有一定要清楚组件和哪些对象关联,当前状态如何。
Torubleshooting Methodlogy
Defining the problem
定义问题不能仅限于表层的描述,要能够具体的找出引发问题的关键点。比如有关资源竞争的问题,在vSAN集群中ESXi主机上不仅会运行虚拟机还会进行硬盘的I/O,由于主机是分布式存储集群的一员,因此除了给虚拟机提供CPU和内存资源之外,还会额外的消耗资源在硬盘I/O上。如果I/O特别密集且虚拟机负载又高的话,两者之间就会产生竞争冲突。所以在出现资源竞争问题的时候,需要先看下CPU和内存的使用率是否过高。
要想解决问题,首先应尽可能的收集额外的详细信息。在遇到任何问题时候,第一举措就是保护好现场,比如拍照或截屏,因为有些提示可能会一闪而过不会再重现。有了这些信息后,再根据自身掌握的知识体系结构列出可能原因,然后依次排除。
这里我们对定义问题做更详细的描述。首先是问题能否重现,如果能重现解决起来就相对容易。其次是逐渐缩小问题范围,从集群到主机再到组件依次排查。另外在问题出现之前是否对系统做过改动,通过日志查看有哪些变动。由于VMware的用户基数很大,因此我们可以在相关论坛和官方网站中搜索是否有遇到同样问题的线索。通过每次新版本发布的Release Notes,也能判断问题是否由BUG引起。
Identifying the Root Cause
问题定义完之后接下来就要找寻问题的原因,根据现有产品中环境的状态进行判断,比如检查当前集群、对象和组件的健康状态,硬盘以及虚拟机关联对象是否存在问题等。上图就是vSAN集群的健康状态监控,直观的展示了当前集群的各种情况。
除了图形界面外,还可以通过一些vSAN的命令或脚本在控制台中查看当前状态。
Resolving the problem
最后要做的是构造解决方案,下面通过一些具体的例子来描述。比如主机进入维护模式造成虚拟机不可用,一般在有多份拷贝的情况下进入维护模式并无太大影响,但只有两份拷贝的时候,如果其中一个副本已损坏,另一个正常的副本却进入了维护模式,那么在退出维护模式的时候这两份数据副本就都不是最新状态。所以在进维护模式之前一定要运行vsan.check_state脚本检查对象的所有组件是否健康正常。
虚拟机I/O出错很有可能是由于其相关的组件有问题,可以通过vsan.vm_object_info脚本来检查对象信息,它会显示出对象具体存在的问题并进行修复。也有可能是主机进入维护模式引起的,这时可以退出维护模式以进行修复。
性能问题同样值得关注,比如磁盘组离线导致虚拟机出错。一般性能出问题,有可能是CPU和内存性能不够也有可能与驱动器有关,硬盘是否兼容也要考虑到。
为防止DataStore空间耗尽,在它达到70%临界值的时候,就该计划扩容分配加主机、磁盘组或硬盘。
Troubleshooting Tools
ESXICLI Commands
上图列出是与esxcli相关的一些命令,可以在主机本地shell或者通过ssh远程连接到主机使用。这些命令并不需要强记,只要输入esxcli就会列出后续的子命令列表,如下图是使用esxcli storage后的帮助列表。
Useful Log Files
日志文件是最常用的辅助手段之一,推荐大家关注vobd.log、vmkernerl.log、vmkwarning.log、clomd.log这个四个日志文件。这些日志文件可以配合python脚本来使用。
上图的vsanDiskFaultInjection.pyc脚本是用来模拟vSAN集群出现问题后的情况,通过 —help列出相关的帮助信息,比如 -u是模拟硬盘的热拔出。
这是具体的执行命令,-d指明了要拔出的设备。
命令执行完之后在日志中就展示出了错误信息。
设备重新上线后,日志中的信息会进行更新,可以看到下方已经显示online了。
ESXCLI Namespaces in vSAN
最后我们通过一个具体的例子来演示下如何使用esxcli相关的命令。假如集群中的某台服务器的系统损坏,但是硬盘没有问题还保存着vSAN的数据,这时我们要做的是对系统进行重装,重新加入到vSAN集群中。那么如何加入呢,其实可以通过esxcli vsan命令来完成。
在vSAN集群的其他正常主机上运行 esxcli vsan cluster get命令得到当前集群的信息,这里有一个关键的条目——集群的UUID(图中红色标识的)。
获取到UUID之后,就可以在新装主机上执行esxcli vsan cluster join -u “UUID”命令加入到集群中,然后在当前主机上使用esxcli vsan cluster get就会看到它已经正常加入到集群中了。
相关问答
出现 “Mathematica不能建立与 内核 的连接。”,的错误?重装.记得做好如下事情,Mathematica的注册文件放在了一个叫做mathpass的文件中:有如下两个地方:1,C:\DocumentsandSettings\AllUsers\Appl...
为何我用vi/etc/hosts,提示我comand not found-ZOL问答276075891尝试一下:/sbin/vi/etc/hosts有用(0)回复misaoer“vi/etc/hosts”应该是"vi/etc/hosts"应该有个...
为什么接交换机的网线看 不到 共享文件?regsvr32netshell.dllregsvr32netcfgx.dllregsvr32netman.dll14、设置帐号和密码由于WinNT内核的操作系统,在访问远程计算机的时候...
Mshome 无法访问。您可能没有权限使用网络资源。这是怎么回事...所帮助!在局域网内XP不能和98的电脑互相访,2K和2K、XP与XP也不能互相通信。在工作站访问服务器时,工作站的“网上邻居”中可以看到服务器...在局域...
ndsi不读卡问题~-ZOL问答刚入手的新机~就是不读卡~AK2i的内核打了好几遍了~换了三张TF卡~都不读~一点AK2就黑屏~提示一篇英文“Anerrorhasoccurred.PressandholdPowerBu...
【主机出的提示uhhuh.nmireceived.dazedandconfused,buttryi...[回答]你的内存有问题,内核错误
这是什么BIOS啊,好多东西没法调? - ?大蕾? 的回答 - 懂得其设置可以清楚地掌握电脑的运行状态,准确地分析各种硬件信息。鉴于有很多朋友对BIOS的设置不甚了解,而不同的主板有不同的BIOS,设置方法也...鉴于...
CIA/DNA/FBI/GMT/PhD/USSR写出英文的全称,中文意思.】作业帮[回答]ABC=AbridgedBuildingClassificationforArchitects,Builders&CivilEngineers建筑师、建筑商和土木工程师用节略本建...
局域网为什么老是提示你没有权限访问,请与管理员联系?在局域网内安装了WindowsXP的电脑不能与安装了Windows98的电脑互相访问,安装了WindowsXP的电脑与安装了WindowsXP的电脑也不能互相通信。在工作...
我在玩CS的时候 怎么不能用AWP麻烦你说清楚一点.你是在局网里自己建主机不能玩还是进入别人的服务器不能玩?怎么个不能玩法?是不能选择职业或者是怎样?补充一下你的问题.我会给你...