持续建模,关注业务抽象,以任务分派执行跟踪系统为例

前言:我认为,抽象和封装是面向对象编程思想的精华,这在两年前我已经发过这方面的帖子了。现实中,给无OO建模概念的人员直接交流OO建模是何等困难!他们恪守着数据库建模,代码优先的律令,无论我如何强调关注业务本身,对业务抽象,实现业务软件模型和现实世界映射的重要都无济于事。经常发生的现象可能是:我说的理论性强一些,会被视为花架子,难以实现;我说个简单的需求作为例子,还没有说出如何抽象建立OO模型,别人就数据建模出来了,表示出数据建模方法的高效率,而对OO建模带来的参与者沟通的便利、团队协作开发的保障、应对需求必定变化的优势等优点视而不见。发生这种尴尬现象来自于我的原因,还是笃定的这套理论系统落地实现案例太少,关注业务变化本身的编程思想还不为多数人认同。因此,本人斗胆借用“Just Do It”精神发此贴,将讨论重点放在业务上,说明——挖掘业务需求,抽象归纳是如何适应需求的变化。
但是,我并不否认面向过程编程方法论,并不否认软件实现最终还是要数据库建模。我只是认同坛子里很多人的观点:将数据库建模的战略地位降低一点,作为对象持久化来看待;将代码编写阶段拖后一些,先看看客户需求,以及应对变化需求的解决方法。

正文:唯一不变的是变化。
需求一:
客户要求做一个任务分派跟踪系统,业务流程大致如下:公司经理可以给部门职员分派任务;任务包括任务说明、要求、完成时间等内容;部门职员收到分派的任务之后限期完成,报公司经理办理情况;系统根据任务限期时间,对于未完成任务的经办人发送提醒;系统对于超期完成的任务执行情况进行记录。
一个具体的例子:部门经理要求所有职员在2013年1月1日之前报一份2012年度工作总结。
数据库建模可能是这个样子:任务表(包涵任务Id、任务要求、任务期限、分派人、执行人、完成时间等字段);任务提醒设置表(包涵Id、任务Id、距离任务限期提醒周期(比如距离任务结束还有1小时、8小时、12小时等)、提醒内容);成员表(包涵Id、成员名、账户、密码、职位(经理和职员))。
由于需求明确、简单,代码部分就是实现以上数据库模型的crud操作,那些简单的业务流程完全可以写入crud操作中,例如判断当前时间是否到了该发送提醒未完成任务的经办人时间。
这样做出的软件效率高,同时满足了需求一,假设这个软件成为软件系统一。

需求二:
某个单位需要做一个任务分派和执行跟踪系统,业务流程大致如下:单位分两级,上级单位经办人建立一个任务,并分派下级单位负责人;下级单位负责人收到任务之后,指定下级单位具体经办人;系统要告知哪个任务分配给哪个下级经办人;下级经办人根据任务要求完成任务;系统根据任务限期时间,对于未完成任务的下级单位负责人和经办人发送提醒;系统对于超期完成的任务执行情况进行记录。
一个具体例子:上级经办人要求14个下级单位2013年1月1日前报送本单位人员花名册;14个下级单位负责人接到任务分派给本单位成员具体经办;下级单位成员上传本单位的花名册。

假如软件系统一(也就是满足需求一的软件)的研发者,得到需求二之后,无论如何都会期望能在软件系统一的基础上稍作修改以满足需求二。但是,以笔者不多的编程经验来看,这种“稍作修改”满足需求二的愿望很难实现。
首先,数据库模型就会有一些区别,这些区别大致如下:增加单位表,包涵单位名称、单位级别字段;增加单位级别表,级别Id、级别名称字段;增加单位职位表,包涵单位级别Id,职位名称(对应下级单位负责人和职员、上级单位只有职员)……
剩下的代码编写阶段需要对现有软件系统一做更大改动:1、新增表crud操作;2、大量的业务控制代码的添加。具体而言,需求二会有大量的权限控制代码需要加入到原有系统中,这就要求代码编写者熟知原有的代码,知道如何将权限控制代码加入到原先系统中穿插在众多crud操作中的业务控制部分。
当软件研发者咬牙在原有系统中加入了满足需求二的代码之后,新的需求犹如一头怪兽肆虐着研发者,考验着研发者的忍受底线。

客户提出新需求:为了提高效率,下级单位负责人可以手机接收任务短信,并通过回复短信的方式指定本单位经办人。
软件研发者:好的,可以实现。
客户新需求:上级单位只关心具体哪个下级单位未完成任务。但是,下级单位负责人关心未完成上级单位分派的任务以及该任务的经办人。
软件研发者:好的,可以实现。
客户新需求:下级单位分行政负责人和党务负责人,行政负责人负责行政任务的分派,党务负责人负责党务任务的分派。
软件研发者:嗯……好的。
客户新需求:单位分为三级,上中下。上级单位分派任务给中级单位,中级单位可以选择转发下级单位完成,也可以直接作为经办人完成。当然,上级单位可以直接越过中级单位分派任务给下级单位。
……
估计这个时候,软件研发者已经失去改造原有系统的想法了。但是,上述痛苦的过程对于软件研发者却时常重现。而导致这种现象发生的原因是什么?我任务主要原因有两点,一是没有挖掘,总结,抽象需求,导致软件不能适应需求变化;二是没有OO建模,实现对功能修改的关闭,对功能扩展的开放(开闭原则)。原有软件编写模式,随着数据库模型的修改,意味着可能产生大量关联数据的业务规则、流程。实现这些业务规则、流程需要在原有系统做“硬”代码修改,而这些修改很有可能就是“压死”改造原有系统想法的 “最后一根稻草”。
那么,适应需求不断变化的应对之策就是——如何挖掘,总结,抽象需求。
挖掘,总结,抽象需求说起来简单,实际做起来完全是一项经验活。不过,好在这个纯属脑力劳动,可以从手上现有小系统开始做脑力风暴式想象。比如,分析需求一的时候,可以扩展设问:公司就只有经理能分派任务吗?公司有没有上级机构?
由此可以抽象得出以下业务:
1、机构可分级。每个级别机构有不同的职位,具备不同的权限。
2、上级机构可创建,分派任务给下级机构。任务包括任务内容、要求、执行期限等。
3、下级机构接受任务,如果还有下级机构还可以分派。
4、机构管理人可以指定任务执行人。
5、系统根据未完成任务期限,提醒任务执行人。
6、系统接受任务执行人完成任务信息,如反馈文字或文档。
接下来,还可以展开设问:除了创建、分派、完成任务,还有什么关于任务的操作?每种任务是不是固定进行创建、分派、完成等操作,是否不同任务具备不同任务操作组合?
这样可以得出一个更为抽象的业务:
1、机构可分级。每个级别机构有不同的职位,具备不同的权限。
2、不同级别机构可对任务进行不同操作,例如上级机构可以创建任务,并分派给下级机构。
3、机构中不同职位人员可以进行不同的任务操作,例如机构负责人可以指定任务执行人;机构职员具体执行、完成任务。
4、本系统根据预先设定的任务操作序列来执行,例如一个任务操作序列是1、任务创建2、分派3、指定4、执行5、反馈等,并跟踪记录任务执行情况。
好了,当业务抽象到这一级别,可以发现,这个抽象业务能很大程度上适应需求的变化,无论是机构级别的改变、职位权限的不同、任务操作的变化等等。剩下的就是如何对这个抽象业务进行OO建模,代码实现了。
总结:为了适应变化的需求,就要在需求分析阶段开始总结、归纳、抽象业务。这个过程就像画同心圆,每一次抽象,得出的就是一个适应需求变化的更大同心圆。当然,每次抽象后,实现的难度会加大,抽象到什么层次结束,应该是受到成本、风险、系统愿景等因素控制。不管怎样,挖掘、总结、抽象业务这个过程使得需求分析,以及在此基础上进行软件设计,的重点回归到业务本身上。随着软件越来越复杂,花费在这方面的时间和精力产生的成果就越有价值。

2013-09-08 20:16 "@showerxp
"的内容
原有软件编写模式,随着数据库模型的修改,意味着可能产生大量关联数据的业务规则、流程。实现这些业务规则、流程需要在原有系统做“硬”代码修改,而这些修改很有可能就是“压死”改造原有系统想法的 “最后一根稻草”。 ...

总结得非常好。需求一是一个以查询报表为导向的系统,而需求二已经转向以任务为导向的系统。

如果说报表查询可以通过数据建模容易解决,那么以任务为导向则应该去发现业务中规则,要以业务规则为导向,也就是发现领域中的规则,需要以领域专家为主导,而不是数据分析了。

最近看到一篇文章:AI,我们创造出来的异类智能,文中谈了基于规则和机器学习是两种不同思路,前者规则由领域专家发现制定,ddd领域驱动设计过程是一个规则发现过程。而机器学习则是由机器自己学习建模。


机器学习大数据包括OLTP 或基于数据库的查询报表其实都是一种数据挖掘,这是一种不事先假设规则的任意挖掘,是摆脱领域专家任何人的操控,自行进行的数据分析和挖掘,一种不同于人类设定思路的挖掘,而管理者或用户等有时就希望从这些报表分析中得到一些自己意想不到的分析结果,弥补人类思考的不足,人类一思考,上帝就发笑啊。

数据建模和OO建模比如DDD都是基于规则的模式,模型代表规则。数据建模是介于机器学习和OO建模之间的一个中间阶段,针对小型局域网系统这个方法比较简洁快捷,兼顾了领域专家和报表查询分析两个方面。可惜随着需求不断涌出,系统不断复杂,我们就需要进行专业分工,将领域规则和报表数据分析分离开,分别关注。

[该贴被admin于2013-09-09 16:34修改过]



将“创建任务”、“分派任务”、“指定执行人”、“完成任务”抽象成“任务操作”。“任务操作”归纳为:具有某种角色的人对于任务的业务操作。
这样,不同类别的任务其实就是不同任务操作的排列组合。而不同的任务操作又要调用到其他的类,比如如果单位是一级结构,就只需要“创建任务”、“指定执行人”、“完成任务”这三个操作。

这样一个模型,其实任务操作就像是DCI中的context?不同的context组合就是不同类型的任务。怎么看都像是saga,process manager之类的东西了。

2013-09-16 16:14 "@showerxp
"的内容
这样一个模型,其实任务操作就像是DCI中的context?不同的context组合就是不同类型的任务。怎么看都像是saga,process manager之类的东西了。 ...

任务操作应该是任务这个聚合根的方法行为,任务中封装了创建下一个任务 分派任务 指定执行人和完成任务等方法。我建议不要将任务操作单独封装为独立对象,合并到任务中。

这每个行为都是有业务约束的,比如创建下一个新任务和当前已经执行的任务是有关的,分派任务和是否为单位有关的,等等。

外界对任务的触发调用可以使用事件消息。

2013-09-17 10:16 "@banq
"的内容
任务中封装了创建下一个任务 分派任务 指定执行人和完成任务等方法。我建议不要将任务操作单独封装为独立对象,合并到任务中。 ...

我相反,倾向于将任务操作抽象分离出来。好处是:不同的任务由不同的操作组成,抽象后可以满足这个需求。比如,有的任务只有创建和完成操作;更复杂的可能是,有的任务创建之后要审核,经办人完成之后也要部门领导审核……如此,不论有多复杂的任务,只要增加对应“任务操作”之后,由这些“任务操作”的执行序列即可满足。
将“任务操作”单独抽象出来我觉得真是本案设计的重点。问题是这种抽象出来之后,满足DDD的设计模式却感觉很困惑。
由此多少反映出——思想上的天马行空,到落地的具体行动是有很大的落差。本案我会尽量实现它以丰富我的建模经验。后续还归纳出“任务活动”:任务相关活动,与“任务操作”不同,这些活动并不会改变任务的状态,比如任务催促、互动交流。




更新了一下设计模型。

其中,聚合根:1、任务(任务单);2、任务类型;3、单位(成员)4、单位类型(职位)

不知道“任务操作”算不算服务。

php简易实现DDD模型方案。php有魔法方法——__call(),网上有篇介绍用这个方法实现简易的aop。这里我稍加修改,实现懒加载和脏标记。


abstract class AOP
{
protected $instance,$lazyFlayArray,$dutyFlay=false,$dutyFlayArray;
/**
* @return the $dutyFlay
*/

public function getDutyFlay() {
return $this->dutyFlay;
}

/**
* @param boolean $dutyFlay
*/

public function setDutyFlay($dutyFlay) {
$this->dutyFlay = $dutyFlay;
}

public function __construct($instance,$lazyFlayArray=null,$dutyFlayArray=null)
{
$this->instance = $instance;
$this->lazyFlayArray = $lazyFlayArray;
$this->dutyFlay = false;
$this->dutyFlayArray = $dutyFlayArray;
}
public function __call($method, $argument)
{
if(! method_exists($this->instance, $method))
{
throw new Exception('未定义的方法:' . $method);
}
$callBack = array($this->instance, $method);

//get懒加载
if ($this->lazyFlayArray!=null) {
foreach ($this->lazyFlayArray as $aLazyMethod) {
if (false !== strpos($method, $aLazyMethod)) {
//判断是否有懒加载函数
if (method_exists($this,
"lazy".ucfirst($method))==true){
call_user_func_array(array($this,
"lazy".ucfirst($method)), $argument);
}
}
}
}

//set 脏标记
if ($this->dutyFlayArray!=null){
foreach ($this->dutyFlayArray as $aDutyFlagMethod) {
if (false !== strpos($method, $aDutyFlagMethod)) {
$this->dutyFlay=true;
}
}

}

//以下一般函数调用
//if (method_exists($this->instance,"before".$method)) call_user_func_array(array($this->instance, "before".$method),array($this->instance));

$return = call_user_func_array($callBack, $argument);
return $return;
}
}

比如Biz是某个领域类,bizAOP则继承上述AOP类,并把Biz注入。而懒加载的具体实现代码放入bizAOP中。类似如下:


class bizAop extends AOP{
public function __construct($instance,$lazyArray,$dutyFlagArray){
parent::__construct($instance,$lazyArray,$dutyFlagArray);

}
public function lazyGetI(){//懒加载具体内容,命名getLazyI就是针对属性I的懒加载函数
if ($this->instance->getI() ==null) {
echo
"lazy";
$this->instance->setI(
"aa");
}
}
}



class Factory
{
public static function getBizInstance()
{
return new bizAop(new BIZ(),array("getI"),array('setI'));
}
}

好了,客户端获得一个bizAop对象,其中设置getI懒加载函数,setI脏标记函数。具体落地实现则是bizAOP中的“lazyGetI()”函数。


嗯。好了简单的很,不过能解决很多问题了。coding……

基本与我的类似,
我只是把任务操作不列为领域模型,而作为辅助类。

2013-09-30 10:16 "@13yan
"的内容
基本与我的类似,
我只是把任务操作不列为领域模型,而作为辅助类。 ...

任务操作涉及到具体的业务,如何作为辅助类呢?

发一些代码晒一晒。懒加载直接写到aop中去,直接在aop中调用dao对象访问数据库。感觉很大精力在做懒加载了。


class UnitAOP extends LazyAndDutyAOP{
public function __construct($unitId){
$unitRepo = new UnitRepo();
parent::__construct($unitRepo->getById($unitId),array("getChildIds","getDirectlyChildIds","getMembers",
"getParentIds"),array());
unset($unitRepo);
}
public function lazyGetMembers(){
$dao = new DAO('member');
$rs = $dao->getBy('unitId', $this->instance->getId());
$unitRepo = new UnitRepo();
foreach ($rs as $aMemberId) {
$this->instance->addMember($unitRepo->getMemberById($aMemberId['id']));
}
}
public function lazyGetDirectlyChildIds(){
$dao = new DAO('unit');
$rs = $dao->getBy('parentIds', $this->instance->getId());
$tempArray = array();
foreach ($rs as $aUnit) {
array_push($tempArray, $aUnit['id']);
}
$this->instance->setDirectlyChildIds($tempArray);
}
public static function getUnitAOP($unitId){
$unit = new UnitAOP($unitId);
return $unit;
}
public function getChildByFatherId($fatherId){
$dao = new DAO('unit');
$rs = $dao->getBy('parentIds', $fatherId);
foreach ($rs as $aChild) {
$this->instance->addChildId($aChild['id']);
$this->getChildByFatherId($aChild['id']);
}
}
public function lazyGetChildIds($aChilds=null){
$this->getChildByFatherId($this->instance->getId());
}
}

[该贴被showerxp于2013-10-10 09:38修改过]