状态模式在领域驱动设计中的使用(Using the State pattern in a Domain Driven Design)

领域驱动设计是软件开发的一种方式,问题复杂的地方通过将具体实现和一个不断改进的核心业务概念的模型连接解决。这个概念是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。

public enum ReservationStatus {
 NEW,
 ACCEPTED,
 PAID,
 CANCELLED;
}

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

public interface ReservationStatusOperations {
 ReservationStatus accept(Reservationreservation);
 ReservationStatus charge(Reservationreservation);
 ReservationStatus cancel(Reservationreservation);
}

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

public class Reservation {
 private intid;
 privateString name;
 privateCalendar date;
 privateBigDecimal price;
 privateReservationStatus status = ReservationStatus.NEW;

 //getters/setters

}

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

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

public enum ReservationStatus implementsReservationStatusOperations {
 NEW {
 publicReservationStatus accept(Reservation reservation) {
 //..
 }
 publicReservationStatus charge(Reservation reservation) {
 //..
 }
 publicReservationStatus cancel(Reservation reservation) {
 //..
 }
},
ACCEPTED {
 publicReservationStatus accept(Reservation reservation) {
 //..
 }
 publicReservationStatus charge(Reservation reservation) {
 //..
 }
 publicReservationStatus cancel(Reservation reservation) {
 //..
 }
},
PAID {/*...*/},
CANCELLED {/*...*/};
}

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

public enum ReservationStatus implementsReservationStatusOperations {

 NEW(newNewRso()),
 ACCEPTED(newAcceptedRso()),
 PAID(newPaidRso()),
 CANCELLED(new CancelledRso());

 privatefinal ReservationStatusOperations operations;

 ReservationStatus(ReservationStatusOperationsoperations) {
 this.operations = operations;
 }

 @Override
 publicReservationStatus accept(Reservation reservation) {
  returnoperations.accept(reservation);
 }

 @Override
 publicReservationStatus charge(Reservation reservation) {
  returnoperations.charge(reservation);
 }

 @Override
 publicReservationStatus cancel(Reservation reservation) {
  returnoperations.cancel(reservation);
 }

}

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

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

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

public void accept() {
 setStatus(status.accept(this));
}

public void charge() {
 setStatus(status.charge(this));
}

public void cancel() {
 setStatus(status.cancel(this));
}

public void setStatus(ReservationStatus status) {
  if (status!= null && status != this.status) {
    log.debug("Reservation#" + id + ": changing status from" +
  this.status+ " to " + status);
    this.status = status;
  }

这里发生了什么?当你在一个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 状态,它的方法将要被执行)。

public class AcceptedRso implementsReservationStatusOperations {

 @Override
 publicReservationStatus accept(Reservation reservation) {
  throw newUnsupportedStatusTransitionException("accept",ReservationStatus.ACCEPTED);
 }

 @Override
 publicReservationStatus charge(Reservation reservation) {
  //chargeclient's credit card
  //sende-mail
  //printticket
  returnReservationStatus.PAID;
 }

 @Override
 publicReservationStatus cancel(Reservation reservation) {
  //sendcancellation e-mail
  returnReservationStatus.CANCELLED;
 }

}

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

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

我没有展示所有的ReservationStatusOperations 的实现,但是如果你将在基于JavaEE的String或者EJB中引入这种方式,你可能已经看到一个弥天大谎。我描述了每一个业务方法应该发生的事情,但是没有提供具体的实现。我没有的原因是因为我遇到了一个大问题:一个Reservation实例通过手工(用new)或者持久化框架像hibernate创建。 It uses statically created enum which creates manuallyReservationStatusOperations
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

时间: 2024-10-06 00:24:56

状态模式在领域驱动设计中的使用(Using the State pattern in a Domain Driven Design)的相关文章

领域驱动设计 ——一种将概念模型化的方式

原文发布于:http://www.gufeng.tech/ 1.引子 2004年Eric Evans 发表了一本书:<Domain-Driven Design: Tackling Complexity in the Heart of Software>(中文名:<领域驱动设计:软件核心复杂性应对之道>),在这本书中作者提出了领域驱动设计(DDD)的概念,到现在已经10多年的时间了. 1.1 面向对象与面向对象语言 面向对象思想已经存在相当长的历史了(相对于软件的历史),我而们使用的

EntityFramework之领域驱动设计实践

EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领域驱动设计实践 (二):分层架构 EntityFramework之领域驱动设计实践 (三):案例:一个简易的销售系统 EntityFramework之领域驱动设计实践 (四):存储过程 - 领域驱动的反模式 EntityFramework之领域驱动设计实践 (五):聚合 EntityFramewor

领域驱动设计系列(转)

曾经参与过系统维护或是在现有系统中进行迭代开发的软件工程师们,你们是否有过这样的痛苦经历:当需要修改一个Bug的时候,面对一个类中成百上千行的代码,没有注释,千奇百怪的方法和变量名字,层层嵌套的方法调用,混乱不堪的结构,不要说准确找到Bug所在的位置,就是要清晰知道一段代码究竟是做了什么也非常困难.最终,改对了一个Bug,却多冒出N个新Bug.同样的情况,当你拿到一份新的需求,需要在现有系统中添加功能的时候,面对一行行完全过程式的代码,需要使用一个功能时,不知道是应该自己编写,还是应该寻找是否已

领域驱动设计系列:澄清一些基础概念

要研究DDD,必须认清DDD的核心是通用语言和模型驱动设计.即使是DDDLite(技术上的DDD),也必须清楚DDD在架构中的位置和必须的架构知识,否则一路跑到哪里能否回来都是未知了.我们先了解常用架构分层,再了解DDD的所在层次和范畴,然后强调DDD的核心.包括从架构到领域模型设计方面的决策和自己的些许实践. 1.三层作为基础:表示层.业务逻辑层.数据访问层是所有讨论的基础.有2个最重要的决策和实践等待你应用和发现: (1)在假设存在多个表示层的基础上,你可以据此将分散在表示层中的业务逻辑代码

领域驱动设计和实践

软件系统面向对象的设计思想可谓历史悠久,20世纪70年代的Smalltalk可以说是面向对象语言的经典,直到今天我们依然将这门语言视为面向对象语言的基础.随着编程语言和技术的发展,各种语言特性层出不穷,面向对象是大部分语言的一个基本特性,像C++.Java.C#这样的静态语言,Ruby.Python这样的动态语言都是面向对象的语言. 但是面向对象语言并不是银弹,如果开发人员认为使用面向对象语言写出来的程序本身就是面向对象的,那就大错特错了,实际开发中,大量的业务逻辑堆积在一个巨型类中的例子屡见不

拨开迷雾,找回自我:DDD(领域驱动设计)应对具体业务场景,Domain Model(领域模型)到底如何设计?

写在前面 阅读目录: 迷雾森林 找回自我 开源地址 后记 毫无疑问,领域驱动设计的核心是领域模型,领域模型的核心是实现业务逻辑,也就是说,在应对具体的业务场景的时候,实现业务逻辑是领域驱动设计最重要的一环,在写这篇博文之前,先总结下之前关于 DDD(领域驱动设计)的三篇博文: 我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践:伪领域驱动设计,只是用 .NET 实现的一个“空壳”,仅此而已. 一缕阳光:DDD(领域驱动设计)应对具体业务场景,如何聚焦 Domain Model(领域模

(转)EntityFramework之领域驱动设计实践

EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领域驱动设计实践 (二):分层架构 EntityFramework之领域驱动设计实践 (三):案例:一个简易的销售系统 EntityFramework之领域驱动设计实践 (四):存储过程 - 领域驱动的反模式 EntityFramework之领域驱动设计实践 (五):聚合 EntityFramewor

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

1      从搬砖谈领域对象 有一个古老的故事,大概是这样的.作者问三个建筑工地上的工人他们在干什么?有一个没精打采的说,我在挖洞!而另一一个人却说,我在盖一座房子.还有一个人说,我在建立一座巨大的城市.不同的思维模式决定了不同的发展,十年过后,第一个工人,还是在挖洞,而第二个则成为了工头.第三个最终却成为了大设计师. 在软件开发领域,往往会使用搬砖这个词来形容我们所开发的每个功能模块,实际上也确实如此,如果把我们需要完成的每个项目,比作一座高楼大厦,那么在项目中所完成的各种模块,也确实是我们

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

领域驱动设计- 让程序员心中有码(七) -设计原则和设计模式,互联网开发者们共同的追求 前言 多年来,笔者一直从事传统软件企业的软件开发和项目管理工作.笔者发现在众多的传统软件企业中,评判优秀开发者的标准往往是技能的熟练程度,基本上都是以梭代码的速度论英雄.有人评价说,这种开发可以称之为cv编程,即ctrl+c和ctrl+v编程为主.这种开发往往对开发者的技能要求并没有想象中的那么高,由于工时和合同的限制,不得不压缩开发时间,通过靠密集的劳动力资源.较高的工作强度来完成项目的开发.这种模式,通过