领域驱动设计是软件开发的一种方式,问题复杂的地方通过将具体实现和一个不断改进的核心业务概念的模型连接解决。这个概念是Eric Evans提出的,http://www.domaindrivendesign.org/这个网站来促进领域驱动设计的使用。关于领域驱动设计的定义,http://dddcommunity.org/resources/ddd_terms/,这个网站有很多的描述,DDD是一种软件开发的方式:

  1. 对于大多数的软件项目,主要的精力应该在领域和领域的逻辑。
  2. 复杂的领域设计应该基于一个模型。

DDD促进了技术和领域专家之前的创造性的合作,迭代地接近问题的概念核心。注意,在没有领域专家的帮助时,一个技术专家可能不会完全理解一个领域的错综复杂,当然,没有技术专家的帮助,一个领域专家实际上也不能应用它的知识到项目中。

大多数情况下,一个领域模型对象封装一个内部的状态,本质上是一个系统中某个元素的历史,也就是,对象的操作是有状态的。在那种情况下,对象保持它的私有状态,这个状态最终将影响他的行为。状态设计模式可以干净地代表一个对象的状态,处理它的状态转换。简而言之,状态模式是针对依赖于状态做出行为的问题的解决方案。

很明显,DDD和状态设计模式息息相关。我对DDD是个新手,所以我将让我们最出色的JCG伙伴 Tomasz Nurkiewicz,用一个例子来介绍使用状态设计模式的DDD。

注意:为了提高可读性,原始邮件被稍微重新编辑了下。

一些企业应用中的领域对象包含状态的概念。状态有两个主要的特性:

  1. 领域对象的表现(如何响应业务方法)依赖于它的状态。
  2. 业务方法可能改变对象的状态,在一个特定的调用之后,对象可能表现出不同的行为。

如果你不能想象任何领域对象的例子,想象在租赁公司的一个Car实体。这个Car,尽管是同一个对象,但是它有一个附加的状态标识,这对公司来说至关重要。这个状态标识可能有三个值:

  1. AVAILABLE
  2. RENTED
  3. MISSING

很明显,当一个Car处于RENTED或者MISSING的时候,是不能被租出去的,rent()方法应该失效。但是当Car被还回来的时候,它的状态时AVALIABLE,对这个Car实体调用rent()方法应该与之前租借这个Car的用户无关,将车的状态改为RENTED。状态标识(可能是一个字符或者是数据库中的int类型)是对象状态的一个例子,因为它影响着业务方法,反之亦然,业务方法也可以改变状态。

现在,思考一个问题,你将怎么实现这个场景,我相信,这个场景你在工作中遇到过很多次了。你有很多依赖于当前的状态的业务方法和很多种的状态。如果你喜欢面向对象编程,你可能立即想到继承然后创建一个继承自Car的AvailableCar,RentedCar和MissingCar。这看上去很好,但是不切实际地,特别是当Car是一个持久化对象的时候。实际上,这种继承的方式不是一种好的设计:我们想要的不是改变整个对象,仅仅是对象的一条内部状态,也就是说,我们不会替换一个对象,仅仅是改变它。也许你想在每一个依赖于状态执行不同的任务的方法中用if-else-if-else这种层叠的方式。。。不要这么做,相信我,那是代码维护的地狱。

相反,我们将不使用继承和多态,但这是一个更聪明的方式:使用状态模式。举个例子,我选择了一个叫做Reservation的实体,这个实体有下面这些状态。

这个生命周期是非常简单的“当Reservation被创建,它是NEW状态。然后一些被授权的人可以接受这个Reservation,导致像座位被短暂地保留的事件发生,然后给人发送一个e-mail,让其为Reservation付费。再然后,用户执行转账,钱到账,打印票据,然后发送第二封邮件给客户。

你肯定已经意识到一些动作依据Reservation 当前的状态有不同的效果。例如,你可以在任意时间取消reservation,但是依赖于Reservation当前的状态,取消动作可能导致退款然后取消reservation,或者仅仅是发送给用户一个e-mail。一些动作不依赖于特定的状态(如果用户为一个已经取消的reservation付账会怎么样)或者应该被忽略。如果你在每个状态和每个业务方法中用了if-else结构,现在想象一下依据上边的状态机写出业务方法将有多难。

为了解决这个问题,我将不解释原始的GoF状态设计模式。而是介绍一些在使用了java ENUM的功能之后,我对这个设计模式的一些改变的地方。代替为状态抽象创建一个抽象的类或接口,然后为每一个状态写实现这种方式,我简单的创建一个包含了所有可用的状态的enum。

1 public enum ReservationStatus {
2 NEW,
3 ACCEPTED,
4 PAID,
5 CANCELLED;
6 }

然后我为所有依赖于状态的业务方法创建了一个接口。把这个接口当做所有状态的抽象基类,但是我们将以稍微不同的方式使用它。

1 public interface ReservationStatusOperations {
2 ReservationStatus accept(Reservation reservation);
3 ReservationStatus charge(Reservation reservation);
4 ReservationStatus cancel(Reservation reservation);
5 }

最后,Reservation领域对象,恰巧同时也是一个JPA实体(省略getters/setters)。

1 public class Reservation {
2 private int id;
3 private String name;
4 private Calendar date;
5 private BigDecimal price;
6 private ReservationStatus status = ReservationStatus.NEW;
7 //getters/setters
8 }

如果Reservation是一个持久的领域对象,他的状态(ReservationStatus)很明显也应该被持久化。这个观察结果将使我们第一次体会到使用enum代替抽象类的巨大好处:JPA/Hibernate可以很容易地使用enum的名字或者顺序的值(默认)序列化和持久化java enum到数据库中。在原始的GoF模式中,我们将直接把ReservationStatusOperations 对象放到领域对象中,然后状态改变时切换不同的实现。我建议使用enum然后仅改变enum的值。使用enum的另一个优势(不是以框架为中心的但是更重要的)是所有可能的状态在一个地方列出。你不必在你的源码中爬行来寻找所有的状态基类的实现。所有的东西都能在一个地方被看到,一个逗号分隔的列表。

OK,深呼吸。现在我解释一下所有的部分如何在一起工作,ReservationStatusOperations 中的业务操作为什么返回ReservationStatus。首先,你必须回忆一下, enum究竟是什么。他们不仅仅是像C/C++那样的多个常量在一个命名空间下的集合。在JAVA中,enum是多个类的闭集,继承自一个公共的基类(例如ReservationStatus),最后继承自enum类。所以当使用enum的时候,我们可能就使用了多态和继承。

01 public enum ReservationStatus implements ReservationStatusOperations {
02 NEW {
03 public ReservationStatus accept(Reservation reservation) {
04 //..
05 }
06 public ReservationStatus charge(Reservation reservation) {
07 //..
08 }
09 public ReservationStatus cancel(Reservation reservation) {
10 //..
11 }
12 },
13 ACCEPTED {
14 public ReservationStatus accept(Reservation reservation) {
15 //..
16 }
17 public ReservationStatus charge(Reservation reservation) {
18 //..
19 }
20 public ReservationStatus cancel(Reservation reservation) {
21 //..
22 }
23 },
24 PAID {/*...*/},
25 CANCELLED {/*...*/};
26 }

虽然以上边的方式写一个ReservationStatusOperations 类很容易,但是从长远来看,这是一个坏主意。不仅enum源代码会极其的长(所有要实现的方法的数量等于状态的数量乘以业务方法的数量),而且是一个坏的设计(所有状态的业务逻辑在一个类中)。一个enum也可以实现一个接口,这个奇特的语法可能与没有参加过SCJP exam 考试的人的直觉相反。我们将提供一个简单的中间层,因为计算机科学中的任何问题都可以被另一个中间层解决。

01 public enum ReservationStatus implements ReservationStatusOperations {
02 NEW(new NewRso()),
03 ACCEPTED(new AcceptedRso()),
04 PAID(new PaidRso()),
05 CANCELLED(new CancelledRso());
06 private final ReservationStatusOperations operations;
07 ReservationStatus(ReservationStatusOperations operations) {
08 this.operations = operations;
09 }
10 @Override
11 public ReservationStatus accept(Reservation reservation) {
12 return operations.accept(reservation);
13 }
14 @Override
15 public ReservationStatus charge(Reservation reservation) {
16 return operations.charge(reservation);
17 }
18 @Override
19 public ReservationStatus cancel(Reservation reservation) {
20 return operations.cancel(reservation);
21 }
22 }

这是我们的ReservationStatus enum的最终的源代码(实现ReservationStatusOperations 不是必须的)。把事情变简单:每一个enum值都自己特定的ReservationStatusOperations 实现(简写为Rso)。ReservationStatusOperations 的实现作为构造函数的参数,然后赋给一个命名为operations的final类型的域。现在,不管enum中的业务方法什么时候被调用,调用将被委托给特定的ReservationStatusOperations 实现。

1 ReservationStatus.NEW.accept(reservation);       // will call NewRso.accept()
2 ReservationStatus.ACCEPTED.accept(reservation);  // will call AcceptedRso.accept()

最后一个要实现的部分是包含业务方法Reservation领域对象。

01 public void accept() {
02 setStatus(status.accept(this));
03 }
04 public void charge() {
05 setStatus(status.charge(this));
06 }
07 public void cancel() {
08 setStatus(status.cancel(this));
09 }
10 public void setStatus(ReservationStatus status) {
11 if (status != null && status != this.status) {
12 log.debug("Reservation#" + id + ": changing status from " +
13 this.status + " to " + status);
14 this.status = status;
15 }

这里发生了什么?当你在一个Reservation领域对象实例上调用任何业务方法,ReservationStatus  enum的某个值的相应的方法就会被调用。依据当前的状态,一个不同的方法(不同ReservationStatusOperations 实现的)将会被调用。但是没有switch-case 和if-else结构,仅仅使用了多态。例如,如果当status域指向ReservationStatus.ACCEPTED,你调用了charge()方法,AcceptedRso.charge() 将会被调用,消费者将会被要求付款,付款之后,Reservation状态改变成PAID。

但是如果我们在同一个实例再次调用charge()会发生什么?status域现在指向ReservationStatus.PAID,所以PaidRso.charge() 将会被执行,这将会抛出一个业务错误(为一个已付款的Reservation付款是无效的)。没有条件判断的代码,我们实现了一个业务方法状态敏感的领域对象。

我还没有提到的一件事是如何从一个业务方法改变Reservation的状态。这是与原始的GoF模式第二个不同的地方。我从业务方法简单的返回一个新的状态,而不是传递一个StateContext 实例给每一个状态敏感的操作(像accept()或者charge()方法),这种方式经常被用来改变状态。如果给定的状态不是null而且与先前的状态不同(setStatus方法中实现),Reservation对象将转变为给定的状态。让我们看一下在AcceptedRso 对象中是如何工作的(Reservation对象在ReservationStatus.ACCEPTED 状态,它的方法将要被执行)。

01 public class AcceptedRso implements ReservationStatusOperations {
02 @Override
03 public ReservationStatus accept(Reservation reservation) {
04 throw new UnsupportedStatusTransitionException("accept", ReservationStatus.ACCEPTED);
05 }
06 @Override
07 public ReservationStatus charge(Reservation reservation) {
08 //charge client's credit card
09 //send e-mail
10 //print ticket
11 return ReservationStatus.PAID;
12 }
13 @Override
14 public ReservationStatus cancel(Reservation reservation) {
15 //send cancellation e-mail
16 return ReservationStatus.CANCELLED;
17 }
18 }

在ACCEPTED 状态的Reservation 可以通过上边的类源码很容易地理解:当一个Reservation已经被accept时,试图accept第二次将会跑出一个错误,收费将使用客户的信用卡,打印给他一个票据然后发送一个email等等。同时,付费操作将返回一个PAID状态,这将使Reservation转换成这个状态。这意味着第二次调用charge将被不同的ReservationStatusOperations 实现处理(PaidRso),没有条件判断。

上边是关于状态模式的全部。如果你不相信这种设计模式,比较一下使用条件判断的代码这种传统的方式的工作量和容易出错的代码。

我没有展示所有的ReservationStatusOperations 的实现,但是如果你将在基于Java EE的String或者EJB中引入这种方式,你可能已经看到一个弥天大谎。我描述了每一个业务方法应该发生的事情,但是没有提供具体的实现。我没有的原因是因为我遇到了一个大问题:一个Reservation实例通过手工(用new)或者持久化框架像hibernate创建。 It uses statically created enum which creates manually ReservationStatusOperations implementations.没有办法去注入依赖,DAOs和service.对于这个类来说,它的整个生命周期都在spring或者ejb的容器管辖之外。实际上,有一个简单有效的解决方案,使用Spring和AspectJ。但是耐心点,我将在下一封邮件中详细的解释,如何给应用增加一点领域驱动的味道。

就这样。一个非常有趣的邮件,解释了如何在DDD方式中使用状态模式,作者是我们的JCG伙伴,Tomasz Nurkiewicz.。我非常期待这个教程的下一个部分。下一个部分是:Domain Driven Design with Spring and AspectJ.

译者注:有两张图片没有权限上传,可以查看原文链接中的文章的图片

Related Articles :
Domain Driven Design with Spring and AspectJ
Spring configuration with zero XML
10 Tips for Proper Application Logging
Things Every Programmer Should Know
Dependency Injection – The manual way

  • 转载自 并发编程网 - ifeve.com

状态模式在领域驱动设计中的使用相关推荐

  1. 深入理解领域驱动设计中的聚合

    简介:聚合模式是 DDD 的模式结构中较为难于理解的一个,也是 DDD 学习曲线中的一个关键障碍.合理地设计聚合,能清晰地表述业务一致性,也更容易带来清晰的实现,设计不合理的聚合,甚至在设计中没有聚合 ...

  2. 何时使用领域驱动设计

    何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了.领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architectu ...

  3. 【转载】何时使用领域驱动设计

    何时使用领域驱动设计 转载自:https://www.cnblogs.com/daxnet/p/15177443.html 何时使用领域驱动设计? 其实当你的应用程序架构设计是面向业务的时候,你已经开 ...

  4. 何时使用领域驱动设计(DDD)

    何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了.领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architectu ...

  5. 万字长文解析何时使用领域驱动设计

    何时使用领域驱动设计 转载自:https://www.cnblogs.com/daxnet/p/15177443.html 何时使用领域驱动设计? 其实当你的应用程序架构设计是面向业务的时候,你已经开 ...

  6. 领域驱动设计,让程序员心中有码(七)

    领域驱动设计- 让程序员心中有码(七) -设计原则和设计模式,互联网开发者们共同的追求 前言 多年来,笔者一直从事传统软件企业的软件开发和项目管理工作.笔者发现在众多的传统软件企业中,评判优秀开发者的 ...

  7. 如何使用ABP框架(2)三层架构与领域驱动设计的对比

    本文来自长沙.NET技术社区,原创:邹溪源.全文共有8500字,读完需耗时10分钟. 题图来自@pixabay 简述 上一篇简述了ABP框架中的一些基础理论,包括ABP前后端项目的分层结构,以及后端项 ...

  8. 如何运用领域驱动设计 - 领域事件

    开篇 距离发布上一篇该系列的文章好像已经过了快一个半月了,好吧,我托更了????.一晃就已经到了3月份,在这樱花????盛开的季节,终于得重新连载该系列了.在停更的期间时不时会收到大家关于DDD的留言 ...

  9. 如何运用领域驱动设计 - 存储库

    概述 在上一篇文章<如何运用领域驱动设计 - 聚合>中,我们已经了解过领域驱动设计中一个很核心的对象-聚合.在现实场景中,我们往往需要将聚合持久化到某个地方,或者是从某个地方创建出聚合.此 ...

最新文章

  1. 面对“超人革命”,我们是否已做好准备?
  2. php向指定文件发送消息,PHP-将文件发送给用户
  3. android 安全讲座第三层 linux权限基础知识
  4. 面型对象 (包package)
  5. 表格为一条细线的html代码,html制作细线表格的简单实例
  6. KB-Modal Dialog Mini FAQ[收藏]
  7. MySQL Return JSON Value Attributes
  8. 设计模式04_抽象工厂
  9. SSH与EJB 比较
  10. java包含_【Java】判断字符串是否包含子字符串
  11. 珍惜生命 远离中国足球
  12. 南京邮电大学离散数学实验一利用真值表求主析取范式和主合取范式
  13. 问卷设计中的常见问题
  14. 简单无迹kalman的matlab程序,卡尔曼滤波原理及应用——MATLAB仿真
  15. 汽车行业(车厂)常见英文缩写及其中文含义(不断完善中)
  16. 嵌入式硬件从接杜邦线起-杜邦头接线实操①
  17. Mac打包dmg文件(更换背景图)
  18. WebApp最佳实践用户体验篇之如何针对多种屏幕尺寸设计合理的移动应用
  19. RAS 和 NDIS 拨号模式
  20. STM32智能小车------PWM驱动直流电机

热门文章

  1. MNE-Python教程汇总
  2. 蓝色起源送90岁《星际迷航》舰长扮演者上太空,刷新太空旅客最高年龄纪录...
  3. 加州理工让无人机长出腿:走路飞行无缝切换,还能玩滑板、走钢丝|Sicence子刊...
  4. 最新技术前沿与产业风向标来了,百度研究院发布2021年十大趋势
  5. AI性能基准测试从此有了「中国标准」!英伟达、谷歌可以试试这套算力卷
  6. 马斯克说要开放自动驾驶和电池技术,上周被特斯拉起诉的公司已哭晕
  7. 首家A股云计算公司背后:黑客大神创办,2019上半年净利润下跌84%
  8. 量产加速!干线物流创新中心迎地平线入伙,嬴彻地平线达成战略合作
  9. 为什么Android变得对商业世界至关重要?
  10. dedecms后台左侧菜单500错误怎么处理