状态与策略——审批操作的两种方案

审批操作是ERP或OA系统中必不可少的功能之一。这里介绍两种我设计的用于审批操作的方案,并借此就“状态模式”与“策略模式”提出一点自己的理解。
    别问我为什么不使用工作流引擎等工具来实现审批功能。做第一版方案时,我孤陋寡闻得并不知道有这个东西。后来引入工作流框架会导致学习曲线骤然上扬,不太划算。

背景

背景无需过多介绍,不外乎有一些数据/任务/请求,需要由领导们点一下头或者按钮。

思路

由于孤陋寡闻,在得到需求之后,我第一反应不是“工作流”,而是“状态机”。它从“提交”状态开始,流经“已初审”“已终审”或者“初审驳回”“终审驳回”等状态,进入终态。
    这个状态机如下图所示:

    当然,状态机中状态的名称、状态间的流转,是与业务需求紧密相关的。例如,有些业务会要求在“已终审”状态下执行“驳回”操作后进入“终审驳回”状态,而有些则要求返回“已初审”状态。不过万变不离其宗,种种流程最终都能归纳到“状态机”中来。
    在这个思路下,我用了两种不同的设计模式来实现需求——状态模式和策略模式,它们都很好的完成了任务。需要多说一句的是,这是两个不同系统下独立的两次实现,而不是一个系统中的“原始版”和“改进版”。因而,两个方案之间并没有非常显著的优劣对比,本文的重点也不是二者的“优劣”对比。

方案一:状态模式

  • 状态模式
        首先来回顾一下我们常说的状态模式。简便起见,这里只提供类图。
        
        其中的核心是“状态”接口。这个接口中有N个方法,对应的是状态机中的N个状态。每个方法负责从当前状态迁移到另一个状态上——一般是别的状态,也可以仍然是当前状态。
        每个具体的状态都继承自这个接口,并在实现类中封装自己所需要的数据、重写自己的状态迁移操作。
  • 我的方案
        在我的设计方案中,类图则是这个样子的:

        与“教科书”上的类图相似的,是“状态”接口(Examiner),以及各个实际状态所对应的子类。
        与之不同的是,虽然我的状态机中有五个状态,但是由于每个状态最多都只有两个状态迁移操作(通过,或者驳回),因此,状态接口中我只定义了两个方法。
        还有一点不同在于,我在Examiner接口下,加了一个默认的实现类(ExaminerAsDefault)。这个类实际上什么都不做,每个方法都直接抛出UnSupportedOperationException。这个类的作用是简化子类,使得每个子类只需要重写自己关心的方法,而不需要重写无关方法。当然,Java 8为接口引入的默认方法,可以实现同样的功能,这是后话。此外,由于业务需求中每次只做一步状态迁移,因此Examiner接口不需要再返回自己。还有一点不同的是,这个方案中,状态迁移操作与状态数据被拆开了——迁移操作由Examiner定义,状态数据则用Dto来封装。
  • 扩展
        当出现新的状态、或者新的迁移操作怎么办呢?
        出现新的状态时,创建一个新的“状态”子类,并实现对应的“状态迁移”方法就行了。出现新的迁移操作时则更简单,只需要做第二步就可以了。

方案二:策略模式

  • 策略模式
        众所周知的策略模式一般都有这样的类图:
  • 我的方案
        在我的设计方案中,类图则是这样的:
        
        可以说这是一个“标准”的策略模式类图。接口定义从一个状态到另一个状态的迁移动作,不同的子类用不同的“策略”去实现它——例如从“已提交”到“已初审”,或者从“已初审”到“初审驳回”,等等。
        状态相关的数据,仍然由单独的Dto来保存和传递。
  • 扩展
        策略模式下,如何增加新的状态、新的迁移操作呢?
        由于策略模式仅仅定义了“状态迁移”动作,因此,无论是增加新的状态、还是增加新的迁移操作,都只需要增加对应的子类即可。

对比

我并不喜欢比较不同设计模式之间的区别。但这里仍可以多说几句。
    用状态模式实现状态机,大概是一个最直观、最容易想到的设计。但是,标准的状态模式将状态数据也封装到状态类中。这使得这个类无法用单例实现。另外,由于状态接口中,对应每一个状态都有一个方法,这可能会使得部分子类非常的大。
    用策略模式实现状态机,与状态机思想是有冲突的。状态机是以“状态”为本,状态迁移操作为辅;而策略模式却专注于状态迁移操作,“状态”的概念淡化得几乎消失了。此外,与状态模式中的“超级类”相反,策略模式可能导致“类爆炸”。
    两种模式之间的分界线,也许只是概念上的“以状态为本”或“以操作为本”。就实践上来说,像我的方案中那样,将状态模式中的数据与操作拆分开,那么整个方案与策略模式其实相去无几。
    这是我不喜欢比较不同设计模式之间区别的原因。由于设计模式的变化、组合非常多,很多时候不同设计模式之间的界限仅仅存在于概念上、思想上,而不在实践中。费尽心思去分析“如何区分23种设计模式”,只在学习阶段有一点意义。我们更应该关注设计模式适用的业务场景、业务问题,以及如何实现它们。
    毕竟,科学可以满足于“认识世界”,技术必须要以“改造世界”为目标。

时间: 2024-10-12 08:14:58

状态与策略——审批操作的两种方案的相关文章

WCF 客户端调用服务操作的两种方法

本节的主要内容:1.通过代理类的方式调用服务操作.2.通过通道的方式调用服务操作.3.代码下载 一.通过代理类的方式调用服务操作(两种方式添加代理类) 1.手动编写代理类,如下: 客户端契约: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; namespace y.WcfFirst.Client.Proxys { [Se

PHP页面静态化:真静态的两种方案

---------------------------------------------------------------------------------------------- 方案1:如果静态文件存在,且生成时间30秒内,直接返回静态页面(有时间延迟)/*|------------------| <www.chenwei.ws>|------------------*/header('content-type:text/html;charset=utf-8'); $id = $_

Linux系统中修改用户名的两种方案整理

在安装系统的时候,经常会无意识的随便起个用户名,后面如果发现该用户名不好或因为环境需要须重起个用户名,经过查找资料和亲自测试发现有两种方案可选:手动修改和使用usermod等命令自动修改. 方案一:手动修改各个关联用户文件 以root身份登录系统文件的修改需要root权限,这里不管是原root用户登录还是普通用户切换到root下都可以,只要有修改权限就行. 修改/etc/passwd这个文件中的用户名部分.用户组部分和主目录部分如我本来的用户名为hadoop,现在想修改为seed,则要修改下面部

java调用matlab函数接口的两种方案

Java 调用matlab 函数接口 Java调用matlab函数接口有两种方式: l 一种是通过matlab把函数打成jar包: l 一种是把matlab编译成dll后,用C++再封装成java能支持的数据类型的dll. 注意:不论用这两种方式中的哪一种,最终部署时都需要matlab环境(MCR,在matlab安装路径下有). 1. 方式一:matlab直接打jar包 1.1. 利用matlab自带工具打jar包 1.1.1. 建立jar包 在matlab的Commond Window中输入d

【SSH进阶之路】一步步重构容器实现Spring框架——解决容器对组件的“侵入式”管理的两种方案--主动查找和控制反转(九)

目录 [SSH进阶之路]一步步重构容器实现Spring框架--从一个简单的容器开始(八) [SSH进阶之路]一步步重构容器实现Spring框架--解决容器对组件的"侵入式"管理的两种方案--主动查找和控制反转(九) [SSH进阶之路]一步步重构容器实现Spring框架--配置文件+反射实现IoC容器(十)(未更新) [SSH进阶之路]一步步重构容器实现Spring框架--彻底封装,实现简单灵活的Spring框架(十一)(未更新) 对于IOC的原理,我们曾经写过一篇博文,[SSH进阶之路

HBase存储时间相关多列数据的两种方案

所谓“时间相关多列数据”指的是同一个Key下具有多个属性数据,并且这些数据与时间具有相关性,多数场景是该Key对应在不同时间时刻的行为数据.在实际应用中,这类数据很多,例如电子商务网站上用户最近一段时间浏览的宝贝集合.访问的URL列表等. 使用HBase存储此类数据时,有以下两种常用的方案: 多行单列 表结构设计 Row Key:用户标识ID + (Long.MAX_VALUE - timestamp) Column Family:’cf’ Column Qualifier:’’ Value:宝

Linux下实现秒级定时任务的两种方案

Linux下实现秒级定时任务的两种方案(Crontab 每秒运行): 第一种方案,当然是写一个后台运行的脚本一直循环,然后每次循环sleep一段时间. while true ;do command sleep XX //间隔秒数 done 第二种方案,使用crontab. 我们都知道crontab的粒度最小是到分钟,但是我们还是可以通过变通的方法做到隔多少秒运行一次. 以下方法将每20秒执行一次 crontab -e * * * * * /bin/date* * * * * sleep 20;

apache两种方案三种方式实现反向代理tomcat

目录 1.概述 2.方案一:以proxy_module方式反向代理 3.方案二:以mod_jk方式反向代理 4.总结 1.概述 在前一博客(http://zhaochj.blog.51cto.com/368705/1639740)中实现了tomcat的在standalone模式下的部署,这样tomcat就身兼职两职,一方向要对http的请求作出响应,又要处理JSP程序,而处理http请求不是tomcat的强项,所以这样的请求就交给httpd.nginx这样的的专业处理http请求的套件来处理,而

Eclipse中注释方法操作(两种)

Eclipse 中的两种注释方法:(1)多行注释 /* */ (2)单行注释 // 多行注释操作方法. 选中注释部分-菜单栏右上角 source: Add block comment.必须选中需要注释的部分才可以. 也可以利用快捷键 ctrl + shift  + / 第二种方法就是 选中注释的行,ctrl + shfit + c: 取消注释的方法:对于ctrl + shift  + / , 可以ctrl + shift  + \, 如果是ctrl + shfit + c,则只需要在选中 ctr