【问题标题】:Spring and the anemic domain modelSpring 和贫血领域模型
【发布时间】:2009-08-20 06:06:02
【问题描述】:

所以,我注意到我肯定倾向于像这样对 Spring/Hibernate 堆栈对象进行模式化:

  • Foo 控制器调用“FooService”
  • FooService 调用 FooRepository.getById() 方法来获取一些 Foo。
  • FooRepository 进行一些 Hibernate 调用以加载 Foo 对象。
  • FooService 与 Foos 进行一些交互。它可以使用相关的 TransactionalFooService 来处理需要在事务中一起完成的事情。
  • FooService 要求 FooRepository 保存 Foo。

这里的问题是Foos没有任何真正的逻辑。例如,如果每次 Foo 过期时都需要发送电子邮件,则不需要调用 Foo.expire()。调用了 FooService.expireFoo(fooId)。这有多种原因:

  • 从 Foo 获取其他服务和对象很烦人。它不是 Spring bean,它是由 Hibernate 加载的。
  • 让 Foo 以事务方式做几件事很烦人。
  • 很难决定 Foo 是否应该负责选择何时保存自己。如果你调用 foo.setName(),foo 应该坚持这个改变吗?它应该等到你调用 foo.save() 吗? foo.save() 应该只调用 FooRepository.save(this) 吗?

因此,出于这些原因,我的 Spring 域对象基本上是带有一些验证逻辑的美化结构。也许这没关系。也许 Web 服务可以作为过程代码。也许随着新功能的编写,创建以新方式处理相同旧对象的新服务是可以接受的。

但我想摆脱这种设计,我想知道 Spring 的其他用途对此做了什么?您是否使用加载时间编织(我不太习惯)等花哨的技巧来对抗它?你还有什么妙招吗?你觉得程序没问题?

【问题讨论】:

  • 我一直在思考的好问题。这里也有一些很好的答案。

标签: spring oop


【解决方案1】:

您可以让 Spring 使用 AOP 将您的服务注入到您的 Hibernate 实例化实例中。您也可以使用拦截器让 Hibernate 执行相同的操作。

http://www.jblewitt.com/blog/?p=129

关于“让 Foo 以事务方式做几件事很烦人”,我希望您的服务实现会知道/关心事务,如果您现在在域模型中使用服务接口,那么现在应该不要那么烦人。

我怀疑何时应该保存域模型取决于它是什么以及你正在用它做什么。

FWIW 我倾向于产生相同类型的贫乏结构,但我已经做到了,现在我知道可以以更明智的方式做到这一点。

【讨论】:

  • Hibernate 拦截器是一个有趣的想法。我喜欢不必对我的 Tomcat 类加载器进行看起来很吓人的编辑。
  • 为什么要在对象中使用服务?听起来很糟糕。
  • @Jess,因为这就是 OOP 的思想……存放数据的同一个对象知道如何操作实例。其他任何东西都只是来自 C 的花哨结构。
  • @ArtB - 对象中的小型自包含方法不是问题,我专门指的是大多数人认为的服务方法(即事务脚本)。例如,让 BankAccountObject 知道如何获取余额、更新自身等就可以了。利用同一个对象来创建年度报表、执行对账等似乎很可笑。与您的域对象分开的一组单独的事务脚本似乎是执行这些冗长操作的更明智的方式。
  • @Jess Utilizing the same object to create annual statements, perform a reconciliation, etc seems ludicrous. 真的吗?为什么不给 BankAccount 一个 StatementFactory (或让它创建 Statment 的实例),以便 BankAccount 可以生成它自己的语句,而无需在 BankAccount 中拥有该逻辑本身?
【解决方案2】:

听起来您的应用程序是围绕过程编码原则设计的。仅此一项就会阻碍您尝试进行的任何面向对象的编程。

Foo 可能没有它控制的行为。如果您的业务逻辑很少,使用Domain Model 模式也是可以接受的。 Transaction Script 模式有时很有意义。

当这种逻辑开始增长时,问题就出现了。将事务脚本重构为域模型并不是最简单的事情,但肯定也不是最困难的。如果您有大量围绕 Foo 的逻辑,我建议您转向域模型模式。封装的好处可以很容易地理解发生了什么以及谁参与了什么。

如果您想拥有Foo.Expire(),请在您的Foo 类中创建一个事件,例如OnExpiration。在创建对象时连接您的foo.OnExpiration += FooService.ExpireFoo(foo.Id),可能通过FooRepository 使用的工厂。

真的先想想。 非常可能一切都已经在正确的位置......现在。

祝你好运!

【讨论】:

    【解决方案3】:

    我认为有一个简单的重构模式可以解决您的问题。

    1. 将您的服务注入您的存储库。
    2. 在返回您的 Foo 之前设置它的“FooService”
    3. 现在让您的 FooController 从 FooRepository 请求适当的 Foo
    4. 现在在你的 Foo 上调用你想要的方法。如果它不能自己实现它们,让它调用 FooService 上的适当方法。
    5. 现在通过我喜欢在 Foo 上称为“桶桥”的方法删除对 FooService 的所有调用(它只是将参数传递给服务)。
    6. 从现在开始,只要您想添加一个方法,就将它添加到 Foo。
    7. 仅当您出于性能原因确实需要时才向服务添加内容。与往常一样,应通过模型对象调用这些方法。

    这将帮助您向更丰富的领域模型发展。它还保留了单一职责原则,因为所有依赖于数据库的代码都保留在 FooService 实现中,并帮助您将业务逻辑从 FooService 迁移到 Foo。如果您想将后端切换到另一个数据库或内存中或模拟(用于测试),您不需要更改任何东西,但 FooService 层。

    ^ 我假设 FooService 执行的 DB 调用会太慢而无法从 ORM 中执行,例如选择与给定 Foo 共享属性 X 的最新 Foo。这是我见过的大多数工作的方式。


    例子

    代替:

    class Controller{
        public Response getBestStudentForSchool( Request req ){
            Student bestStudent = StudentService.findBestPupilForSchool( req.getParam( "schlId" ).asInt() );
            ...
        }
    }
    

    你会朝着这样的方向前进:

    class Controller{
        public Response getBestStudentForSchool( Request req ){
            School school = repo.get( School.class, req.getParam( "schlId" ).asInt() ); 
            Student bestStudent = school.getBestStudent();
            ...
        }
    }
    

    我希望你会同意,这似乎已经更丰富了。现在您正在进行另一个数据库调用,但是如果您将 School 缓存在会话中,则惩罚可以忽略不计。恐怕任何真正的 OOP 模型都会比您正在使用的贫血模型效率低,但是通过代码清晰减少错误应该是值得的。一如既往,YMMV。

    【讨论】:

      【解决方案4】:

      我向您推荐 Doug Rosenberg 和 Matt Stephens 所著的Use Case Driven Object Modeling with UML一书。它谈到了 ICONIX 过程,一种软件开发方法,也谈到了贫血的领域模型。这也是 Martin Fowler 在其网站https://www.martinfowler.com/bliki/AnemicDomainModel.html 中开发的主题。但是我们在使用 Spring Framework 和/或 Spring Boot 时如何实现也是我想弄清楚的。

      【讨论】:

        猜你喜欢
        • 2010-12-26
        • 2012-02-04
        • 2010-12-20
        • 2014-01-05
        • 2020-01-29
        • 1970-01-01
        • 2011-12-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多