`
taowen
  • 浏览: 193578 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

领域模型的价值与困境

阅读更多
很久以前大家就关于这个方面有很多讨论了。前两天我又挖了一个坑来集思广益,非常感谢没有把我的帖子投为新手帖的同志。我不是在装傻,只是想让大家跳出自己的立场,从根本的价值出发来考虑问题。之前有很多讨论,都是在讨论我又发明了一种新方法可以让领域模型充血啦,等等之类的。当提出一个解决方案的时候,一定要有明确的问题。那么领域模型的价值是什么?为什么没有被广泛应用,其困境在哪里?

价值

数据,一定是数据。做企业系统,最核心的东西一定是数据。关于数据,人们有许多需求,但是最根本的一点就是,数据要是对的。在关系数据库的上下文下,为了保证数据是对的,我们有外键,我们有COLUMN的数据类型,我们有主键,我们有constraint,我们有很多很多。但是很多时候还不够,一堆数据在业务上是不是合法的,超过了上述的检查方法的能力范畴。这个时候,以DBA为中心的思考就会导致:我作为DBA,管理这些数据,如果数据出了问题,那就是我的责任了。所以我必须要阻止愚蠢的事情,而我显然是最知道什么是正确数据的人,所以你们(程序员)要访问我的数据,就必须通过我的存储过程。

这种方式显然遇到了问题。问题是很多方面的,有人员素质问题,有工具支持问题。更重要的是,虽然存储过程起到了防火墙的作用,阻挡了外界可能的对数据一致性的破坏,但是其内部却是脆弱的。数据对于包裹它的存储过程都是开放的,写存储过程A的人,可能对数据的假设与写存储过程B的人对数据假设是不一致的。两个人必然只有一个是正确的,但是从数据出发找到修改它的地方并不容易,从而给数据的质量埋下了隐患。

存储过程的问题,就是面向过程的代表。面向对象的主要特征,封装就是为了解决这个问题发明的。把数据放置于对象内部,要修改对象所封装的数据,就必须通过对象所提供的外在行为。有如下图所示。



回到数据的正确性这个问题。程序员不同于DBA,给出的解决方案是领域模型。其实领域模型,只是面向对象的另外一个名字而已。通过把数据封装在领域模型的内部,我们就可以限制模型的使用者对数据的修改,什么值是对的,什么样的值是不对的。具体列出来有:

构造函数
可以确保在创建的时候已经有了所有的必填项
public class Person {
  public Person(String firstName, String lastName) {
  ...
  }
..
}


无Set方法
不能任意的改变值,必须通过特定的合法性检验
public class Publication {
  private State currentState;
  public State publish(Channel to) {
  ...
  }
...
}


关联
可以保证外键,以及强制约束两个表之间数据的关系
public class Cargo {
  public void attachItinerary(final Itinerary itinerary) {
    // Decouple the old itinerary from this cargo
    itinerary().setCargo(null);
    // Couple this cargo and the new itinerary
    this.itinerary = itinerary;
    this.itinerary.setCargo(this);
  }
...
}


一致性
冗余字段的同步更新得到强制
public class ShoppingChart {
  private List<OrderItem> items;
  private int sum; //冗余字段
  public void dropIntoChart(OrderItem newItem) {
    sum += newItem.sum();
    items.add(newItem);
  }
  ...
}


当然,面向对象不光是封装一个特性,它还有继承和多态。所以作为面向对象的另外一个名字,它自然也有继承和多态这个好处。具体到程序里就是

枚举值
不要通过对枚举值的判断来决定程序的路径
// 过去
public void publish(ChannelType channelType, Publication publication) {
  if (channelType.equals(ChannelType.RETUERS)) {
    ...
  } else if (channelType.equals(ChannelType.BLOOMBERG)) {
    ...
  } ...
}
//现在
public interface Channel {
  void publish(Publication publication);
  ...
}
public class RetuersChannel implements Channel {
  ...
}
public class BloombergChannel implements Channel {
  ...
}


数据的含义
另外一个好处是,对数据的访问被集中起来了。所以,从数据出发,很容易发现计算出值并修改它的地方。这就方便了我们去理解数据的含义。数据本身是没有任何意义的,数据只有被使用才有意义。只有理解了数据的上下文的含义,才能编写更多的行为去操作数据。在写新的行为的时候,我们必然要参考过去的行为是怎么理解数据的含义的。这个查找的过程越容易,越有助于我们写出正确的逻辑,也越有助于我们发现过去已经写过一样的行为了,那我就不用写了,也就是所谓的复用。

所以,理论上来说,面向对象或者说领域模型是非常适合我们的日常的企业信息系统开发工作的。但是,实践中,却遇到了很多问题。

困境

框架的约束
如Robin所言
robin 写道
如果你用的是Spring,没啥说的,必须贫血,你想充血也充不起来;
如果你用的是RoR,也没啥说的,直接充血,你想贫血也未必贫得下来;

这就是一个基本事实。Spring作者也坦言(Rod Johnson, JAOO, 2006),Spring的编程模型基本上是EJB的延续。从架构和分层的角度,它们是一脉相承的。这种分层的架构决定了,行为在Service里,数据在Entity里。这种做法成为“最佳实现”,不是偶然的,是框架设计给你的必然结果。矛盾的集中体现在于Entity无法被注入(当然你可以注入,但是这是不推荐的做法)。Spring后来尝试修复这个问题,引入了AspectJ来做Entity的注入。不过仍然不是主流,因为框架的阻碍只是一个小问题,更大的问题在于这个Java的OOP实现本身就有问题。

语言的约束
在很多的讨论中,反对“充血”领域模型的同志都提到。把逻辑集中到领域模型中,会造成类的膨胀。在我个人的实践中,近千行的Entity类定义也是有的。而且这些行为往往不稳定,往往流程高度相关,复用程度并不是想象的那么高。
原因是因为,数据并没有所谓的内在行为。你不用它,它就没有行为。所以数据有什么行为,其实是使用者赋予的。而同样的数据往往会在不同的场合(context)下被使用。正如这位同志所言:
coolnight 写道
我们的系统有很多模块组成, 各模块基本上通过数据库来共享信息。
主要的模块大致有: 核心系统(非web), 网站、 bbs、 网站后台,核心系统后台,BI, 推广员等等
原来的打算是写个rich domain model供各模块来使用,以便代码重用减轻个模块开发的工作量
一个简单的例子, User 原有 changePassword, getFriends, addFriend ... 等等方法撇开配置以及获取User对象的复杂性不谈, 实际开发中发现, 这些东西重用的地方太少了,对网站来说很好的rich domain model, 在网站后台里面就麻烦,很多方法在那里根本就是对后台开发人员的干扰,而很多方法对核心系统、BI等等根本就毫无用处。

所以上面所画的图就得改一下了:



同样,很多同志也发现了这个问题,并给出了解决方案
Quake Wang 写道
用mixin实现领域模型是最简洁的做法, 但是受到Java语言的限制,得用大量mark性质的interface和composite模式,反而会让整个结构变得复杂。
相比C#的namespace或ruby自定义dsl(acts_as_xxx),在java玩领域模型需要更多的功力,推荐一下Rickard Öberg的qi4j,它将composite oriented发挥到了极致:
http://www.qi4j.org/

ray_linn 写道
我建议用C#的扩展方法,或者Java用mimix(不知道打错没有),Domain还是POJO,方法在适当的时候“黏合”(C#通过 namespace引用)到POJO上,让POJO “充血”,而在充当VO,DTO的时候,POJO又可以“放血”回到“贫血的”状态上。

这个问题基本上是暴露了Java的OOP实现的缺陷。C#的Partial Class, Extension Method和Ruby的Mixin就是对这种“一个class定义一切”的做法的改进,允许行为被分片定义,而不是集中定义在一个文件中。

现状
很多同学都谈到现状不是完全的没有领域模型,而是所谓的贫血的领域模型。之所以会这样,就是前面所说的困境。正如很多朋友所说的,这未必是一件坏事,比如可以避免核心的domain过度膨胀。不过也未必是一件好事,理由我能想到这么几点:

越俎代庖
这是我们的DAO经常干的事情。比如我们有两个domain,Publication(一份文档)和Distribution(一次分发)。它们两者之间是聚合关系,也就是distribution必然属于一份publication。如果是重写的领域模型,我们可以通过publication来控制对distribution数据的添加。比如
public void distribute() {
  if (isDeleted()) {
     throw new InvalidDistributionException();
  }
  distributions.add(new Distribution(this));
  lastDistributionDate = new Date();
}

但是一旦有了DistributionDao,就不一样了。
distributionDao.save(new Distribution(somePublication));

哈哈,啥控制都管不了我了吧。很多时候Dao就是这么无法无天的。如果说domain model是数据的防火墙,那么dao就是顶级黑客。由于Entity不能注入等实现和宗教信仰上的限制,往往导致了service对DAO的滥用,就会造成上面这样的by-pass domain model的情况。

舍近求远
明明我有一个contact
public class Contact {
  private List<ContactNote> contactNotes;
...
}

但是我不能直接去取我的contactNotes(性能啊性能,hibernate会load all啊)。所以我得写个查询
from ContactNote where contacted = thisContact and ...

我有一个contact就应该能够让我
contact.allMeeetingContactNotes();

但是不行,你得
contactNoteDao.findMeetingContactNoteByContact(thisContact);

舍近求远也

职责混杂
根据我理解的Service层的现状是:有一部分是作为domain model对外的接口,提供了事务安全等服务,真正是一个“service”对web层提供服务;有的呢,则是当作一些可重用的行为被注入到其他的service之中;而有的呢,则纯粹是一些utils。当然,不过可能是我用的不对吧,或者理解有误。

距离啊距离
数据和行为距离很远。造成了不容易理解数据含义,因为你光看entity压根不知道它会被怎么用。漏掉一些servie中细微的用法,就可能会造成很大的bug。
同时这也更加让可重用的逻辑更难被发现,从一定程度上鼓励了大家各自发明,一份逻辑写n遍,还n遍都不一样。。。
当然啦,这个问题没有想象的那么大,毕竟贫血的领域模型,还是鼓励大家把“只和这个对象,不和外部接口有关的”的逻辑放到对象本身的。问题的大小,取决于团队的稳定性,素质和职业操守。

术语
如nihongye同学所说,xxxService不是domain language。当然啦,讨论这种问题最终是仁者见仁智者见智的。

解困

你真的要解困吗?其实你未必被困住了。你可能根本不需要领域模型,特别是在Java/Spring这种实现下,难以实现。正如很多同志所言,我用贫血模型用得很好。那就行,自己好,就是真的好。但是有朝一日,觉得贫血模型不再适合你了,不妨去了解了解qi4j(如果你不想换语言),或者投入rails的怀抱吧。
  • 大小: 7.9 KB
  • 大小: 12.8 KB
分享到:
评论
5 楼 taowen 2008-11-28  
gamix 写道

我记得DDD上说如果是聚合的话,访问聚合的任何元素都应该从根开始,所以这个例子中Contact得到ContactNote我觉得并不是舍近求远,而是封装的体现。

老兄,你把我的意思看反了……
gamix 写道

另外,关于.net中使用partial的方式来定义类,我确实有考虑过,但实际使用中特别依赖于团队规范,否则很容易让开发人员找不到北。扩展方法也是同理,而且扩展方法我认为是不到万不得已都不应该去用的东西。

这是一个很好的问题。AOP,Extension Method, Mixin都会导致对行为的隐式聚合。也就是在没有一个地方告诉我,这段代码都有啥玩意实现了它,修改了它。qi4j做法是最终有一个Composite接口声明了所有的实现(Mixin)和修改者(Modifier)。我个人也比较倾向于把行为分开来写,最后一个地方再把所有的东西集中在一起。
4 楼 gamix 2008-11-28  
引用
舍近求远
明明我有一个contact
Java代码 复制代码

   1. public class Contact { 
   2.   private List<ContactNote> contactNotes; 
   3. ... 
   4. } 

public class Contact {
  private List<ContactNote> contactNotes;
...
}


但是我不能直接去取我的contactNotes(性能啊性能,hibernate会load all啊)。所以我得写个查询
Java代码 复制代码

   1. from ContactNote where contacted = thisContact and ... 

from ContactNote where contacted = thisContact and ...


我有一个contact就应该能够让我
Java代码 复制代码

   1. contact.allMeeetingContactNotes(); 

contact.allMeeetingContactNotes();


但是不行,你得
Java代码 复制代码

   1. contactNoteDao.findMeetingContactNoteByContact(thisContact); 

contactNoteDao.findMeetingContactNoteByContact(thisContact);


舍近求远也


我记得DDD上说如果是聚合的话,访问聚合的任何元素都应该从根开始,所以这个例子中Contact得到ContactNote我觉得并不是舍近求远,而是封装的体现。


另外,关于.net中使用partial的方式来定义类,我确实有考虑过,但实际使用中特别依赖于团队规范,否则很容易让开发人员找不到北。扩展方法也是同理,而且扩展方法我认为是不到万不得已都不应该去用的东西。
3 楼 taowen 2008-11-28  
downpour 写道

请问,isDeleted()方法由谁来实现?是框架嘛?我想无论如何你都无法避免框架对你的帮助吧。

publication是软删除的。所以isDeleted只是判断自身的一个属性是不是true而已。

downpour 写道

至于说到你数据层面的业务校验,这些可能会在Service中完成,因为他们需要有更多的外部依赖。这样写你是不是觉得也可以?你认为越俎代庖了嘛?

当然是可以的,只是约束数据的地方不在单单是在Domain Model上,还包括Service。别人要修改数据的时候,通不通过你的service就是职业操守的问题了。service给数据添加的约束不是强制的,而domain model可以添加强制的约束。就是这么一个问题。当然啦,你大可以说这是理论。我也说过了,贫血不贫血不是问题,适不适合自己才是问题。

downpour 写道

这点更加是无从谈起了,为什么不写个字段,把hibernate的where用上?简单实用。事实上很多项目我们都是这么使用的。

你是指formula property吗?
2 楼 downpour 2008-11-28  
理论啊理论,哲学啊哲学,什么时候才能回到实践的道路上来?

简单谈谈你所说的现状:
taowen 写道

越俎代庖
这是我们的DAO经常干的事情。比如我们有两个domain,Publication(一份文档)和Distribution(一次分发)。它们两者之间是聚合关系,也就是distribution必然属于一份publication。如果是重写的领域模型,我们可以通过publication来控制对distribution数据的添加。比如
public void distribute() {
  if (isDeleted()) {
     throw new InvalidDistributionException();
  }
  distributions.add(new Distribution(this));
  lastDistributionDate = new Date();
}

但是一旦有了DistributionDao,就不一样了。
distributionDao.save(new Distribution(somePublication));

哈哈,啥控制都管不了我了吧。很多时候Dao就是这么无法无天的。如果说domain model是数据的防火墙,那么dao就是顶级黑客。由于Entity不能注入等实现和宗教信仰上的限制,往往导致了service对DAO的滥用,就会造成上面这样的by-pass domain model的情况。


请问,isDeleted()方法由谁来实现?是框架嘛?我想无论如何你都无法避免框架对你的帮助吧。

事实上,多数情况下,如果你想通过publication来控制对distribution的数据添加,我们会这样写代码:

// this could be in Publication
public void addDistribution(Distribution distribution) {
    this.distributions.add(distribution);
    distribution.setPublication(this);
}

// this could be in Service
publication.addDistribution(new Distribution(somePublication));
publicationDao.saveOrUpdate(publication); // this could be ignore using hibernate


至于说到你数据层面的业务校验,这些可能会在Service中完成,因为他们需要有更多的外部依赖。这样写你是不是觉得也可以?你认为越俎代庖了嘛?

taowen 写道

舍近求远
明明我有一个contact
public class Contact {
  private List<ContactNote> contactNotes;
...
}

但是我不能直接去取我的contactNotes(性能啊性能,hibernate会load all啊)。所以我得写个查询
from ContactNote where contacted = thisContact and ...

我有一个contact就应该能够让我
contact.allMeeetingContactNotes();

但是不行,你得
contactNoteDao.findMeetingContactNoteByContact(thisContact);

舍近求远也


这点更加是无从谈起了,为什么不写个字段,把hibernate的where用上?简单实用。事实上很多项目我们都是这么使用的。


1 楼 ronghao 2008-11-28  
我试着小结一下:
领域模型的价值:
1、从业务使用的角度保证数据的准确性(因为贴近业务,所以比单纯的数据库约束更完善)
2、行为与数据一致,不再对数据显式的判断(内置)
3、在领域模型里集中对数据的访问,方便理解数据的含义。(不再被分隔到多个service里)



相关推荐

    我国上市公司财务困境预测模型实证研究

    财务困境预测模型是财务管理领域的重要研究方向,其核心目的是通过对上市公司的财务数据进行分析,提前发现企业可能面临的财务困境风险,并给出相应的预警。财务困境又称财务危机,通常表现为公司出现严重的资产折现...

    (00-22年)财务困境-MertonDD模型

    《财务困境-MertonDD模型》是金融领域一个重要的理论模型,它主要用于评估企业违约的风险,通过对企业的市场价值和负债的分析,预测企业可能面临的财务困境。Merton模型由诺贝尔经济学奖得主罗伯特·默顿提出,是...

    博弈论中的“囚徒困境”模型1

    "囚徒困境"模型是博弈论中的一个经典案例,由Tucker在1950年提出,用于描述在非合作博弈环境下,个体最优...在实际应用中,这个模型广泛用于解释社会、经济和政治领域的许多现象,如市场竞争、环境保护和国际关系等。

    架构师特刊:联邦学习在金融领域的实践和落地困境.pdf

    因此,联邦学习既保护了数据的隐私性,又通过合作提升了模型的性能,实现了数据的隐私保护和价值利用的双重目标。 在金融领域,联邦学习的落地实践尤为关键。金融数据通常非常敏感,涉及大量个人隐私和机构机密。...

    复杂网络囚徒困境博弈matlab源程序

    在IT领域,尤其是在复杂系统建模与仿真方面,囚徒困境博弈(Prisoner's Dilemma)是一个重要的理论模型,它常被用来研究合作与背叛的行为动态。此模型结合了数学、经济学和计算机科学,而Matlab作为一种强大的数值...

    2业价值研究院-人工智能行业应对AI数据困境:恰当的数据集成方法、治理和工具.rar

    "2业价值研究院-人工智能行业应对AI数据困境:恰当的数据集成方法、治理和工具"这篇报告深入探讨了这一主题,旨在为AI从业者提供宝贵的指导。 首先,报告强调了数据集成的重要性。在AI系统中,数据是模型训练的基础...

    基于matlab实现规则网络模型囚徒困境的模拟 可以作为对演化博弈感兴趣的一个例子学习.zip.rar

    囚徒困境是博弈论中的一个经典模型,用来研究合作与背叛的决策问题。两个囚犯面临两个选择:合作(双方都沉默)或背叛(一方揭发另一方)。如果两人都合作,他们会得到较小的惩罚;如果两人都背叛,他们都会受到较大...

    练习2附件1面向电子商务的企业信息化导入模型与企业.doc

    4. **理论与应用价值**:此研究拓宽了企业信息化研究领域,从技术角度转向经济管理角度,探索如何将企业信息化与电子商务有效结合,构建理论框架和决策模型。其应用前景包括提高企业信息化投资效率,加速电子商务的...

    博弈模型.pdf

    雪堆模型与囚徒困境不同的是,遇到背叛者时合作者的收益高于双方相互背叛的收益。因此,一个人的最佳策略取决于对手的策略:如果对手选择合作,他的最佳策略是背叛;反过来,如果对手选择背叛,那么他的最佳策略是...

    财务困境与财务欺诈综述.pptx

    在识别和预防财务困境与财务欺诈方面,现代技术扮演着关键角色。数据分析和人工智能技术能帮助企业及早发现财务困境的预警信号,通过大数据分析,找出可能导致财务困境的模式和趋势。区块链技术的应用则可以提高财务...

    完美版资料集MM模型ModiglianiMillerModels米勒一莫迪利安尼模型.doc

    总之,MM模型是公司财务管理领域的一个基础理论,它揭示了资本结构与企业价值之间的关系,并在理论与实践中持续发挥着重要作用,尽管其假设可能与现实世界存在一定的差距。通过对MM模型的理解和应用,企业和管理者...

    传统Z-Score模型与人工智能视角下的财务风险预警比较.pdf

    本文档主要探讨了在财务风险预警领域,传统的“Z-Score”模型与人工智能视角下新兴的财务风险预警模型之间的比较。文中首先概述了财务风险预警的重要性,并指出了在大数据、云计算、移动技术和人工智能(大智移云)...

    论文:基于修正Jones盈余管理模型的财务危机预警研究报告报告.doc

    基于修正Jones盈余管理模型的财务危机预警研究报告主要探讨了修正Jones模型在财务危机预警领域的应用可能性及其对财务危机预警模型预测效果的影响。该研究首先引入了盈余管理理论,通过单变量分析初步证实了修正...

    财务模型和公司估值培训.pptx

    最终定价可能受到市场情况和谈判实力的影响,而投资决策则基于价值与价格的比较。 现值和贴现公式是理解公司估值的核心概念,任何资产的价值都等于其未来现金流的现值之和。这强调了现金流的重要性,因为它直接影响...

    财务模型和公司估值概述.pptx

    总的来说,财务模型和公司估值是金融领域中必不可少的工具,用于量化公司的经济价值,为投资决策提供依据。无论是投资银行还是投资管理,都需要掌握这些方法,以便做出明智的交易决策,并在复杂多变的金融市场中保持...

    奥特曼模型在公司财务预警分析中的运用共41页.pdf.zip

    当企业面临财务困境时,其负债的价值可能会降低至零,而股权价值则会受到影响。因此,通过评估企业的负债与股权之间的关系,可以估算出企业破产的可能性。 在公司财务预警分析中,奥特曼模型的应用主要体现在以下几...

    ChatGPT的应用场景与潜在价值.docx

    ChatGPT 是一个由 OpenAI 团队开发的语言模型,具有广泛的应用场景与潜在的巨大价值。本文将就 ChatGPT 的应用场景与潜在价值进行详细的讨论。 1. 客服场景:ChatGPT 模型可以通过学习大量的客户服务对话,掌握如何...

    基于作业成本法的与油价联动的第三方物流合同费用调整模型

    该模型特别针对第三方物流服务的特点,结合近年来我国成品油价格频繁上涨的现状,分析了当前物流企业在服务费用调整方面所面临的困境,并提出了解决方案。 首先,成品油价格的频繁波动对物流企业运作成本的影响极大...

    数据挖掘技术在上市公司财务困境预测中的应用.pdf

    研究发现,这些因子对于预测上市公司是否陷入财务困境具有重要价值。 在构建预测模型时,文章采用了基于极大似然估计的向前筛选策略,并通过Logistic回归分析,发现了五个重要的因子变量F1、F2、F3、F4、F5。这些...

Global site tag (gtag.js) - Google Analytics