为什么要将我的域实体与我的表示层隔离开来?
域驱动devise的一部分,似乎没有很多细节,是如何以及为什么你应该从你的界面隔离你的域模型。 我试图说服我的同事,这是一个很好的做法,但我似乎没有取得多大进展。
他们在演示和界面层中使用的领域实体。 当我向他们争辩说,他们应该使用显示模型或DTO来隔离域层和界面层时,他们反驳说他们没有看到这样做的业务价值,因为现在你有一个UI对象来维护以及原来的域对象。
所以我正在寻找一些我可以用来支持的具体原因。 特别:
- 为什么我们不应该在表示层中使用域对象?
(如果答案是显而易见的,“解耦”,那么请解释为什么在这方面这很重要) - 我们是否应该使用额外的对象或构造来隔离我们的域对象与接口?
很简单,原因是实施和漂移之一。 是的,您的表示层需要了解您的业务对象才能正确表示它们。 是的,最初看起来两种对象的实现有很多重叠。 问题是,随着时间的推移,双方都会有所增加。 演示文稿的变化,演示文稿层的需求演变为包含完全独立于业务层的事物(例如颜色)。 同时,随着时间的推移,您的域对象也会发生变化。如果您没有从界面中适当的分离,您可能会冒险通过对您的业务对象做出看似良性的更改来搞砸界面层。
就我个人而言,我认为处理事情的最好方法是通过严格强制的接口范例; 也就是说,您的业务对象层公开了一个接口,它是唯一能与之通信的接口; 没有关于接口的实现细节(即域对象)被暴露。 是的,这意味着您必须在两个位置实现您的域对象; 你的界面层和你的BO层。 但是,重新实施,虽然最初可能看起来像是额外的工作,但是有助于实现脱钩,在未来的某个时候可以节省吨数。
我自己一直在努力。 在某些情况下,DTO在现在使用是有意义的。 比方说,我想显示在我的系统公司下拉,我需要他们的ID绑定的价值。
那么,而不是加载可能有订阅引用的公司对象或谁知道什么,我可以发回一个DTO的名称和ID。 恕我直言,这是一个很好的使用。
现在再举一个例子。 我有一个代表一个估计的对象,这个估计可能是由劳动力,设备等组成的,它可能有很多计算是由用户定义的,把所有这些项目汇总起来(每个估计可能会有不同的types的计算)。 为什么我必须对这个对象进行两次build模? 为什么我不能简单地将我的UI列举在计算上并显示出来?
我通常不使用DTO来隔离我的用户界面的域名层。 我确实使用它们来隔离我的域控制层以外的边界。 有人会把导航信息放在他们的业务对象的想法是荒谬的,不要污染你的业务对象。
有人会认为他们的业务对象的想法? 那么我说这是一件好事。 您的用户界面不应该有唯一的责任来validation您的业务对象。 你的业务层必须做自己的validation。
为什么你会把UI生成代码放在一个商业对象中? 在我的情况下,我有独立的对象,从用户界面生成UI代码seperatley。 我有sperate对象,使我的业务对象到Xml,这个想法,你必须分开你的图层,以防止这种types的污染对我来说是如此陌生,因为为什么你甚至会把HTML代码放在一个业务对象…
编辑按照我的想法,有些情况下UI信息可能属于域图层。 这可能会使你所谓的域名层出现问题,但是我在一个多租户应用程序上工作,这个应用程序在用户界面的外观和感觉以及function性的工作stream程上有着非常不同的行为。 取决于各种因素。 在这种情况下,我们有一个代表租户及其configuration的域模型。 他们的configuration恰巧包含UI信息(例如,通用字段的Label)。
如果我必须devise我的对象来使它们持久化,我是否也必须复制对象? 请记住,如果你想添加一个新的领域,你有两个地方添加它。 也许这引发了另外一个问题,如果你使用DDD,都是持久实体域对象? 我知道在我的例子中他们是。
你这样做是因为你把SQL保存在你的ASP / JSP页面之外。
如果只保留一个域对象,以便在演示文稿AND域图层中使用,那么这个对象很快就会变得单一。 它开始包含UIvalidation代码,UI导航代码和UI生成代码。 然后,您很快将所有业务层方法添加到上面。 现在你的业务层和用户界面都混在一起了,他们都在搞乱域实体层。
你想在另一个应用程序重用那个漂亮的UI小部件? 那么,你必须用这个名字,这两个模式和这18个表格创build一个数据库。 您还必须configurationHibernate和Spring(或您select的框架)来进行业务validation。 哦,还必须包含这85个其他非相关类,因为它们是在业务层中引用的,恰好在同一个文件中。
我不同意。
我认为最好的方法是从表示层中的域对象开始,直到它使感知做不到。
与stream行的观点相反,“域对象”和“价值对象”可以愉快地共存于表示层中。 这是做到这一点的最好方式 – 你可以从两个世界中获益,减less域对象的重复(和样板代码); 以及跨请求使用价值对象的剪裁和概念简化。
我们在服务器和用户界面上使用相同的模型。 这是一个痛苦。 我们有一天必须重构它。
这些问题主要是因为需要将领域模型切割成更小的片段才能够在不引用整个数据库的情况下对其进行序列化。 这使得在服务器上难以使用。 重要的链接缺失。 某些types也不可序列化,不能发送到客户端。 例如“types”或任何generics类。 它们需要是非generics的,Type需要作为string传输。 这会产生序列化的额外属性,它们是多余的和混乱的。
另一个问题是UI上的实体并不适合。 我们正在使用数据绑定,许多实体只有为了用户目的才有大量的冗余属性。 另外在实体模型中还有许多“BrowsableAttribute”和其他的。 这真的很糟糕。
最后,我认为这只是一个更容易的事情。 有些项目可能会正常工作,不需要编写另一个DTO模型。
这是关于大部分的依赖关系。 组织的核心function结构有其自身的function需求,用户界面应该使人们能够修改和查看核心; 但是核心本身不应该被要求容纳UI。 (如果需要发生,通常表明核心不是财产devise。)
我的会计系统有一个结构和内容(和数据),可以模拟我公司的运作。 无论我使用什么会计软件,这种结构都是真实存在的。 (不可避免地,给定的软件包本身就包含结构和内容,但是挑战的一部分就是尽量减less这种开销。)
基本上一个人有工作要做。 DDD应该匹配工作的stream程和内容。 DDD是关于尽可能完全和独立地明确所有需要做的工作。 然后,UI有希望尽可能透明地完成工作,尽可能高效地完成工作。
接口是关于为正确build模和不变的function核心提供的input和视图。
该死的,我发誓说这是坚持不懈的。
无论如何,这是一个同样的事情:帕纳斯的法则说模块应该保守秘密,而秘密是一个可以改变的要求。 (鲍勃·马丁有一个规则,这是另一个版本。)在这样的系统中, 演示文稿可以独立于域进行更改。 例如,一家以欧元维持价格并在公司办公室使用法语的公司,但希望以美元和普通话文本来呈现价格。 域是一样的; 演示文稿可以更改。 所以,为了最大限度地减less系统的脆弱性,也就是为了实现需求变更而必须改变的事物的数量,可以将问题分开。
您的演示文稿可能会引用您的域图层,但不应直接从您的用户界面绑定到您的域对象。 域对象不是用于UI的,因为如果devise得当,它们往往是基于行为而不是数据表示的。 UI和域之间应该有一个映射层。 MVVM或MVP是一个很好的模式。 如果你尝试直接绑定你的用户界面到域名,你会为自己制造很多头痛的问题。 他们有两个不同的目的。
也许你没有足够宽泛的概念化UI层。 根据多种forms的反应(网页,语音反应,印刷的信件等)以及多种语言(英语,法语等)来考虑。
现在假设电话呼入系统的语音引擎运行在与运行网站的计算机(也许是Windows)完全不同types的计算机(例如Mac)上。
当然,陷入困境很容易“在我公司我们只关心英文,在LAMP(Linux,Apache,MySQL和PHP)上运行我们的网站,而且每个人都使用相同版本的Firefox”。 但是5年或10年呢?
另请参见下面的“层间数据传播”一节,我认为这些论据是令人信服的:
http://galaxy.andromda.org/docs/andromda-documentation/andromda-getting-started-java/java/index.html
在使用视图的同时,借助像“ 价值注入器 ”这样的工具和表示层中的“ 映射器 ”的概念,更容易理解每一段代码。 如果你有一点点的代码,你不会马上看到优势,但是当你的项目越来越多的时候,你将会非常高兴,同时也不需要进入服务的逻辑,知识库来理解视图模型。 View Model是广阔的反腐败层面的另一个后盾,在长期项目中值得黄金分析。
我看不出使用视图模型的好处的唯一原因是,如果您的项目很小并且足够简单,可以将视图直接绑定到模型的每个属性上。 但是如果在未来,视图中的需求变化和一些控制将不会被绑定到模型上,并且您没有视图模型的概念,您将开始在许多地方添加补丁,并且您将开始拥有一个遗留代码你不会感激。 当然,你可以做一些重构,以view-viewmodel的forms转换你的view-model,如果你不需要的话,可以遵循YAGNI的原则而不添加代码,但是对于我自己来说,更好的做法是添加一个表示层仅暴露视图模型对象。
下面是一个真实的例子,为什么我觉得从视图中分离域实体是一种很好的做法。
几个月前,我创build了一个简单的用户界面,通过一系列3个仪表显示土壤样品中的氮,磷和钾的值。 每个测量仪都有一个红色,绿色和红色的部分,也就是说,每个部件可能太less或太多,但是中间有一个安全的绿色水平。
没有太多的思考,我模拟了我的业务逻辑,提供这3个化学成分的数据和一个单独的数据表,其中包含三个案例(包括使用哪个测量单位,即摩尔或百分比)的可接受水平的数据。 然后,我将我的UIbuild模为使用非常不同的模型,该模型关注的是标签,值,边界值和颜色。
这意味着当我稍后必须展示12个组件时,我只是将额外的数据映射到12个新的量规视图模型,并且它们出现在屏幕上。 这也意味着我可以轻松地重新使用仪表控制,并让他们显示其他数据集。
如果我把这些仪表直接连接到我的领域实体,那么我就不会有上述的灵活性,任何未来的修改都将是头痛的问题。 在UI中对日历进行build模时遇到了非常类似的问题。 如果有10个以上的与会者要求日历约会变为红色,那么处理此业务的业务逻辑应该保留在业务层中,并且UI中的所有日历都需要知道,是否已经指示变成红色,应该不需要知道为什么。
答案取决于你的应用程序的规模。
简单的CRUD(创build,读取,更新,删除)应用程序
对于基础粗粮应用程序,您没有任何function。 在实体顶部添加DTO会浪费时间。 这会增加复杂性而不增加可伸缩性。
适度复杂的非CRUD应用程序
在这个大小的应用程序中,您将拥有几个拥有真正生命周期的实体以及与它们相关的一些业务逻辑。
在这种情况下添加DTO是一个好主意,原因有两个:
- 表示层只能看到实体具有的字段子集。 你封装实体
- 后端和前端之间没有耦合
- 如果你在实体内部有业务方法,但在DTO中没有业务方法,那么添加DTO意味着外部代码不能破坏你的实体的状态。
复杂的企业应用程序
单一实体可能需要多种呈现方式。 他们每个人都需要不同的领域。 在这种情况下,您会遇到与前面示例相同的问题,并需要控制每个客户端可见的字段数量。 为每个客户分开DTO将帮助您select应该显示的内容。
在通用语义和特定于语义的语义之间添加额外映射的唯一明智原因是,您有(访问)基于广义(但可映射)语义的代码(和工具)的现有体。
当与正交的function域框架(如ORM,GUI,工作stream等)结合使用时,域驱动devise的效果最好。 一定要记住,只有在外层邻接中才需要暴露域语义。 通常这是前端(GUI)和持久性后端(RDBM,ORM)。 任何有效devise的中间层可以并且应该是域不变的。