Chinaunix首页 | 论坛 | 博客

pacman2000的ChinaUnix博客

在编程的影子传说

分类: 信息化

2016-10-21 14:00:08

前言
==========================================

偶然的机会里,看到了呆伯特法则,除了常常捧腹大笑以外,总觉得自己在软体界待过以后,看到的现象,其实跟书上写的还真的差不了太多。唯一的例外是我的老板们每个都有其独到之处,很难跟书上写的主管一般愚蠢。各位英明的老板们,看到这句话,我是不是该加薪了呢?How about 50%?

接着开始有了写作的念头。一开始的时候,我想把自己在软体业界观察到的现象,形诸于文字,与三五好友分享。所以一开始时,我是想单纯地描述现象,接着好好地让大家发出会心的微笑,并不想提出太多解决的方法。至于我自己工作上的一些微薄经验,自己也觉得没什么大不了的,不足为外人道。

不过开始发表文章以后,我多少会听到读者的反应,多半都是觉得我点出了台湾软体开发的黑暗面(我个人倒是不觉得有多黑暗啦。),可是没有提出解决的方案。偶尔蜻蜓点水式地写了一些解决的方案,就有人批评,我没有清楚地交代完整的思考体系,或者解法不切实际。

这大概是中国人的天性吧,写文章就一定要文以载道。诊断出问题来的人,还一定要负责提供解药,不然就没有职业道德。嗯,看来呆伯特的作者Scott Adams先生就没有这个困扰。

看来,写一本类似呆伯特的书还不足以满足市场上的需求,在这样的情况下,我只好开始试着把自己一些不是很成熟的想法,还有微薄的经验,透过笔端来分享与交流。希望可以达到一个拋砖引玉的效果。不过因为只有三脚猫功夫,所以很容易就会贻笑大方。如果写出来的东西,对于各种高深玄妙的理论,有认识不清之处,或是根本就是满纸荒唐言,还希望各位大师先进多多包涵,不吝指正。

此外,因为工作上的关系,我所熟悉的,是一些跟使用资料库有关的商用程式。其他的领域像是通讯、文书处理、防毒软体、绘图、作业系统…这些系统,就只有单纯作为使用者的经验。所以对于这些类型的系统呢,因为超出本人能力范围,我顶多只能给一些不负责任,纯属臆测的建议。

当然,既然是经验分享,那么必然地,就会天马行空,随手拈来,信笔所之,缺乏完整的体系架构。还好这对于一个专栏来说,不算是什么太严重的坏事。

 

第一章:Data Oriented Analysis & Design
==============

我记得,我还在念书的时候,那时系上的教授,提到系统分析师,莫不认为这是一个需要高度专业素养的工作。只有头脑清楚,思虑周密,看起来玉树临风的翩翩美少男,或是玲珑有致的性感美少女,才能胜任这么高档的工作。所以我在求学时候所立下的志向,除了要推翻满清,解救中华,对抗日本鬼子的侵略,解救受苦受难的同胞以外,就觉得当个系统分析师也不赖。那时总觉得,像我这种才高八斗,学富五车,英姿焕发的人,只要减肥成功,绝对是举世罕见的系统分析师。

开始工作以后才发现,这个年头,好像只要写过几年程式,做过几个系统,按照正常的升迁管道,即使是个像我这种其貌不扬的胖子,很容易就会因为公司人手不够,就硬着头皮升任到系统分析师这样子的职位。做一个工作做久了,久而久之,也就会自我膨胀一下,觉得自己真是一世雄杰。不过随着年岁增长,慢慢见闻广博之后,就不敢太过嚣张。反倒是现在看到不少年轻的小朋友们,虽然挂着系统分析师的头衔,俨然一副仙风道骨,天纵英明的模样,可是骨子里,根本就抓不着系统分析该做些什么事情。

这些所谓的系统分析师,不少也是怀着戒慎恐惧的心理,所以想要找个补强的方法。大多数人接着就会把重点放在domain knowledge上面。于是乎有些念资讯的人,就去多拿一个商学的学位。想要藉着这个学位,取得比别人更有利的地位。即使没有真的飘飘然有出尘之感,最少也比较听得懂使用者的语言。

不过对于大多数的商用系统来说,背后隐含的观念,其实并没有什么太过高深之处。只是对于大多数的程式设计师来说,会计科目,或是人事法规…种种枯燥无聊的东西,远远比不上J2EE、.Net这种新科技来得吸引人。

正因为大多数人都没有花时间下去钻研与整理开发系统时一些共通的观念。这也就间接造成很多系统其实都有同样的毛病,而开发的人员,也没有意识到这些共有的问题,其实属于同一种类型。因此在不同的状况下,就可以看到不同的菜鸟,犯下相同的毛病。

这种事情看多了要能忍住不讲话,实在是蛮困难的,光是憋气都会憋到内伤。所以我打算把我自己的一些小小心得,拿出来分享。以免继续忍受下去,如果病入膏肓就很难医治了。

在这一章里面,我打算分享一些我觉得很重要而且很基本的概念。为了讲解上的方便,我会拿一般公司常常会用到的请假系统,里头经过精简过后的一小块来当做例子。我想对于大多数没做过商用系统的人来说,里面牵涉到的专业知识比较少,应该还算是容易理解。

商用系统的系统分析,其实与资料库的设计工作息息相关。所以在这里我们先忘掉use case…那一大堆东西吧。让我们先把重点放在探索使用者的需求,以及设计database的table schema。


通常我们在设计table schema时,我大概会把握几个原则:

我们打算储存什么资讯?
我们打算对这些资讯进行什么样的运算或处理?
为了进行这样的运算或处理,还要增加什么样的资讯,才会让整个运算的速度变快?
 

至于会进行什么样的运算或处理,这里就牵涉到了系统到底需要提供什么功能。通常我考虑的因素是:

user会需要透过什么功能来存取资料?
与其他系统是否有整合的需求?
user所要存取的资料,是否是从现在系统的资料运算出来的?如果是的话,那这些运算的演算法是什么?
 

一个系统里面的资料,通常有四个来源:

从其他系统整合过来:遇到这种状况,我们需要撰写整合的程式
使用者输入:需要有GUI让user可以keyin
系统一开始建立时,programmer建立:我们需要针对这些initial data,找出一套良好的管控机制
系统内部运算所产生:需要有相关的程式。重点应该要着重在演算法的描写
 

我们就先假设你是一个新的系统分析师,拿到了前人与客户留下来的这么一小段会议记录,你想要开始做系统分析了,那你该从什么地方下手?


前人留下来的片纸只字======================================

使用者线上填写假单。送出后,依照签核权限,送给相关的主管核决后,假单就生效。
生效以后的单据,只要还在期限内,使用者都可以线上进行修改或是作废。
修改与作废的期限是由系统管理员进行设定。
修改或是作废单据经过核准后,原始单据就失效,若是修改或是作废单据被主管驳回,原始单据依然有效。
员工任职后,该年度便享有特休假7天,依照该年度任职天数的比例来计算。员工服务满1年未满3年给特休假7天,服务3年未满5年给特休假10天,服务5年未满10年给特休假14天,服务满10年,每满1年加1天,唯上限是30天。年资与特休天数对应可以弹性设定。
每年未休完的特休假,可保留至次年使用。
使用者可以依照月份与假别,查询当月各种休假的时数统计资料。
请假时数月份的归属,应该视使用者申请假单时的班表,来判断请假时数应该算是哪一天的请假,再依据这个日期进行时数的切割。例如6/30上夜班(20:00 ~ 05:00)的人,如果请假从7/1 01:00请假到 7/1 05:00,那么他的请假时数应该算是6/30的请假,因此时数应该归到6月份。

前人留下来的片纸只字(完)======================================

如果你看了没什么感觉,这也没关系。在后面的章节里面,这一小段话会不断重复地出现。有听过iterative的开发方式吗?这就是啦。

当你消化完了上面的资讯以后,我们先来看看幼稚园组的同学,会怎么样来规划这套系统。


幼稚园组
--------

1. 使用者线上填写假单。送出后,依照签核权限,送给相关的主管核决后,假单就生效。

看完第一条以后,想了一下,嗯,我们会需要记录请假单。

请假单看起来需要可以新增、修改、删除、查询。所以我们应该要有相对应的功能来完成这些事情。嗯,依据经验来说,每张单子都有个编号什么的。我们要记录是谁请的假,请了什么假,从什么时候请到什么时候,总共请了多少个小时…

如果请假单就是一个database里面的table,最少就应该要有下列栏位:(请假单编号,员工编号,请假日期,假别,请假开始时间,请假结束时间,请假时数)。

主管要可以签核?嗯,主管要签核的时候,应该要把还没签过的单子选出来。那我们需要在请假单里面增加一个栏位,这样才知道这张单子签完了没有。

经过这样的分析,我们应该加上签核状态这个栏位,来描述这份文件现在的状态是怎么样。经过讨论,合理的签核状态应该是(新增,审核中,核准,驳回)。『新增』表示这张单子还在编辑中,算是草稿啦;『审核中』则是当单子被送出来了以后,开始躺在主管的电脑里面,等着被签;『核准』是老板们都签完了;『驳回』则表示你遇到一个吹毛求疵的家伙。

根据我们小时候的经验告诉我们,签核的主管可能会有很多人,毕竟企业里面总是有太多人没事做需要盖盖橡皮图章,来赚取他们丰厚的薪水。所以我们最好把签核的人,放到另一个table里面去。所以我们可以看到另外一个存放着签核纪录的table:(请假单编号,签核主管,签核日期,签核状态,主管意见)。没有意外的话,这个table跟请假单应该会有个一对多的关系。

2. 生效以后的单据,只要还在期限内,使用者都可以线上进行修改或是作废。

『只要在期限内,任何时候,使用者都可以修改跟作废?』这句话有什么特殊之处?小时候听老师在讲系统分析时,常常都会叫我们先把名词圈出来。这个时候才发现,国文程度不好的人,不怎么适合当分析师。

这句话的名词是什么?单据、期限、使用者。好吧,我们有记录单据的table了。使用者就是user嘛,用脚想也知道系统会有一个user table。嗯,那问题就剩下『期限』了。

什么叫做期限内呢?这就要问user啦。打电话给user确认了之后,得到的答案是,假单的开始时间一个月内的单子,都可以自由的修改或是做废。如果超过了一个月,那就不可以改了。

好吧,那我们就让使用者可以直接修改吧。每次使用者要修改的时候,就把先前的签核纪录都清掉,重新签一次。这样应该就可以了吧。作废?那就直接删掉吧。反正这张假单他都不要了,干什么还要替他保存下来?


葛拉芙:小艾,你帮我看看,这张单子怎么不见了?

艾佛森:待我观来。嗯,我看到log里面写着,这张单子已经在昨天被作废了。老板核准了以后,我们就把单子删掉了。

葛拉芙:什么?那我们在系统里面通通都没有留下任何他曾经请过假,接着把单子作废的记录吗?

艾佛森:你可以看log啊。Log里面有完整的记录。我跟你说,我们用了最新的技术来记录log。每个动作都被记录的一清二楚啊?

葛拉芙:log怎么看?user怎么查?

艾佛森:你可以telnet到server上面来看啊。这个档就在logs这个目录下面嘛。

葛拉芙:……

此时一片冷风吹过,霎时只觉空气中凝结着一片死寂般的宁静。

艾佛森很小声地说:你们的requirement里面又没有说…我想,他作废都已经不要了,干嘛还留着占空间?没有事先讲当然就没有做啊…

很多没有经验的工程师都会抱持着这种愉快的观念,既然不要了,就直接delete掉吧,这样不是干净利落,不留痕迹?一直到了与使用者谈完需求之后,才会赫然发现:『什么,作废的单子还要留下来?』

好吧,如果一张单子作废了,你还要留下来,那表示你不能直接从资料库里面把它砍掉。这个问题其实也蛮简单的。就把目前的status栏位多加一种状态就好了嘛。

所以我们就在请假单的签核状态这个栏位,多加了一种状态,就叫他(作废)好了。

有些时候,user所谓的修改期限,会是那种当你单子送出来了以后,一个月内可以改,超过一个月就不准改。也就是说,修改和作废的期限是跟这张单子送出来的时间有关。遇到这种状况,如果我没纪录原始单据是什么时候建立的,当user想要修改或作废这张单子的时候,我怎么判断这个时间是在修改或作废的期限之内?所以我们会在请假单里面,多放个CreateTime的资讯。

即使user的修改与作废期限与单据create的时间无关,记录create的时间还是有很大的好处。特别是当你需要debug,需要看log时,如果你记录了详细的create time,对你去找log会有很大的帮助。通常不管domain上的需求是什么,我们都会记录这个资讯。

Hint 1:新增修改删除查询vs.新增修改作废查询

※※※※※※※※※※※※※※※※※※※※※※※※※※

对很多没做过商用系统的人来说,一般的商用系统无非就是资料的新增修改删除查询,没啥大学问。事实上,光是这段人人朗朗上口的『新增
修改删除查询』里面就埋了个大陷阱。

在商用系统里面,凡走过就必须留下痕迹。每个人做过什么事情,都要一步一脚印的详实纪录下来。不然他一不小心,做了什么逾矩的事情,就不会有人知道了。如果你没有详实的记录user的每一个动作,这样子如果有人要来auditing,就没有业绩可以做,这些auditor就只好回家吃自己。想也知道这种挡人财路的事情是做不得的。所以资料怎么可以说删除就删除呢?

除了auditing以外,另外的问题则是在于这笔资料对于系统到底造成了什么效应,应该要详实的记录下来。以免看到错乱的资料时,完全找不出方向。

如果你用delete,把一笔资料砍掉了,这笔资料就会从地球上消失,你就完全无法证明它曾经存在于这个系统之中。如果其他相关的资料,出了什么后续的问题,你根本就无法追踪这件事情的来龙去脉。所以在商用系统里面,系统要真正删除(delete)一笔资料,背后通常会有一整套完整的控制机制。

其实我们指的删除,大多数时刻指的其实不是delete而是mark as deleted。也就是说我们做的动作其实是database里面的update,而非delete。在这种情况下,几乎每个table都有一个标准栏位:『status』。我们通常会拿这个栏位来记录这笔资料目前是否还是有效的资料,或是加入其它可能的状态。

另外通常会加在table中的标准栏位就是像是CreateDate。我通常拿它来记录我们是在什么时候create了这笔资料。

我个人比较喜欢用『新增修改作废查询』。当你不要看到一笔资料了,你就把它作废吧。当我们讲作废一张单子时,我们的思考通常就是mark the status as inactive.而不是delete。

※※※※※※※※※※※※※※※※※※※※※※※※※※

3. 修改与作废的期限是由系统管理员进行设定。

修改与作废要有期限让系统管理员进行设定?不是说是一个月吗?还要可以改喔。好吧,那我们就多一个table来记录这件事情好了。我们增加一个系统参数档,来放这个资讯。里面就先放期限的资料好了:(修改期限,作废期限)。

4. 修改或是作废单据经过核准后,原始单据就失效,若是修改或是作废单据被主管驳回,原始单据依然有效。

修改跟作废如果被驳回,原来的单据还要有效?第一个想法就是,那我们把假单原始的资讯存下来,才有办法rollback回来。所以我们可以把table改成是(请假单编号,员工编号,请假日期,假别,请假开始时间,请假结束时间,请假时数,CreateTime,签核状态,原始请假日期,原始假别,原始请假开始时间,原始请假结束时间,原始请假时数)。新的单据要送出来时,就把原有资料放在相对应的『原始』栏位中。这样一来,如果新的单据被核准了,那么就用新的值取代这张既有的单子,否则要把资料rollback回去,我们只要从原始栏位那里把资料再抄回来就好了。

如果是作废的话,那就在主管核准时,直接把状态改成是『作废』就好了。连原始的值是什么都不用特别记录。

咦,这样看来,还得要增加一个栏位喔,这样才能清楚地知道现在这张单子是一张新申请的单子,还是一张修改中的单子,或是一张要作废的单子。所以我们把假单的table,再增加一个『文件申请类别』的栏位,里面合理的值就放(新增、修改、作废)好了。

如果一个人把一张单子修改过了以后送出来,结果被老板退件了,你会做什么事情?把资料通通从原始栏位里面抄回来,像是用原始假别的资料把假别的资料盖过去,文件申请类别也改回来变成『新增』…反正你想改这张单子,可是你老板们不准,那就把它恢复原貌就好了。

可是通通改回来以后,你曾经修改这张单子的记录,又跑到哪里去了?咦,这样子不work喔,得要跟user好好谈谈。接着再想下去,如果使用者卯起来修改以后,要再修改,这该怎么办?此外,单子改过以后还想再改,这听起来还蛮合理的。User的智商就是会做这种事情。可是已经作废的单子,还可以再作废或修改吗?如果可以这样做,就太诡异了吧。

这其实是每个系统分析师都应该要与客户确认的问题。很多人在这个地方就会自由心证的作了一些判断,然后只要客户到了测试阶段,发现系统与他们预期不符合,就会开始争论。Development team照惯例,一定认为这是一个change requirement,客户通常会认为这原本就在scope里面。

不过就大多数的企业而言,已经作废而且经过签核通过的单据,就不能再进行其他的动作。不能继续修改,当然也没有再作废这回事。有些人可能会来个『取消作废』的功能吧,不过就大多数公司来说,既然单子已经作废了,有什么要改的,你就再补一张新的单子就好了。修改过的单子倒是有可能后续进行再次的修改,甚至作废。

好吧,你跟user confirm过了,修改完了以后,还可以再次修改,甚至作废;不过已经作废的单子,就没办法再做什么其他的动作了。

嗯,因为一张单子可能被修改很多次,而我们也没有办法确定它到底可以被改多少次。可是如果我们要保留完整的资讯,就得要详细的记录每一次user改过的值。那这时候table该怎么design呢?我们总不能跟user说,你最多就是可以改个10次,我们就把table改成,原始请假日期1,原始请假日期2…原始请假日期10,…原始请假时数10。

仔细想了想,原来的design不好,我们应该换一个方法来处理修改跟作废。咦,那就干脆用一张单子来记录这些新送出来的资料,接着只要描述这张单子与原始单据之间的关系就好了。

所以整个请假单的table应该要有下列栏位:(请假单编号,文件申请类别,原始请假单编号,员工编号,请假日期,假别,请假开始时间,请假结束时间,请假时数,CreateTime,签核状态,文件状态)。

我们用『原始请假单编号』来记录,要进行修改或是作废的单子,是base on哪一张单子进行修改还是作废。这个栏位在新增文件时,应该是null,可是如果是修改还是作废单据时,就记录着它到底是base on哪一张单子进行修改或是作废。此外,既然原始文件在修改与作废后就失效,我们就应该用一个栏位来表示它的状态。『文件状态』就是为了这个目的而存在的。它标明这份文件是active还是inactive。

Hint 2:如果你需要让一个table里面的某些资料,因为某种资料上不同的特性而需要与其他资料区分开来时,你应该考虑增加一个栏位。

※※※※※※※※※※※※※※※※※※※※※※※※※※

当你在设计资料库里面的栏位时,如果你发现你需要让某一部份的资料,具有一组目前没有的特性,而与其他的资料区分开来时,通常你就会需要再增加一个栏位。有些人喜欢hardcode资料库的值,或是将原有的资料重新进行排列组合,可是最简单也是应该要做的方法,就是再增加一个栏位。

例如像我们遇到的问题,(签核状态,文件状态)的组合有(新增,active),(审核中,active),(核准,active),(核准,inactive),(驳回,active),(作废,active)…这些组合,可是如果要用一个栏位来表示这些不同的组合的话,例如status 1表示(新增,active),status 2 表示(审核中,active)…这样其实会很不清楚,也很难下出适当的SQL statement。我觉得你应该考虑使用两个栏位来表示这些不同的特性,这会让你的程式以及SQL statement都单纯很多。

有些人为了怕如果一开始的分析与设计没有考虑好,每次为了解决这样的更动,就会去增加栏位,这样子就会牵动到database schema,一旦schema有所改变,可能就会impact到implementation,所以就会对于这样的做法感到迟疑。老实说,不好的database design对于整体的开发,带来负面的效应远远大于所省下来的开发时间。

如果真的怕database design的变动,造成coding的困扰,可以考虑在一开始设计table时,就为每个table预留一堆保留栏位,等到开发到了一半时,再依照需求赋予这些栏位意义。例如char_1, char_2, num_1, num_2 …不过这虽然可以解决问题,也保有了一定的弹性,我个人还是比较偏好每个栏位的命名都具有它的意义。这样会让要去maintain这套系统的人,比较容易了解各个table,以及每个栏位背后所蕴含的意义。

另外有种常见的做法,则是增加一个新的table。这个table的栏位由下列三种不同的资讯组成:(原始table的primary key,Name,Value)。也就是说当你有一个没有考虑到的栏位存在时,为了不要去更动schema,你就更改你的data dictionary,在这个新增的table里面呢,多一种(Name, Value) pair的排列组合,就可以保留弹性。例如我不想增加一个叫做『原始请假单编号』的栏位,我就把资讯放在这个table里面。Name就指定成『原始请假单编号』,Value就放真正该放的请假单编号。

老实说这是一种慢性的毒药。原本应该在设计资料库时,就要考虑的很周详,采用了这个的solution,designer就会倾向于草率地设计table,发现了问题就挪到implementation时再解决。有些人设计到后来,就把太多的资讯放在(name, value) pair里面。这样子你的schema design,根本就变得毫无意义。当你performance不好,或是想要让新人了解整个data model时,就会发现问题丛生。

不过如果你不想对系统的架构做大幅度的修改,只是想做一些细微的调整时,确实是可以考虑用这样的方法,来记录一些不会常常被query到的资料。不过用时要谨慎,得要确定对performance的影响不会太大。

※※※※※※※※※※※※※※※※※※※※※※※※※※

在此先依照目前的设计整理相关的规则。

1.新增一张单子时,原始请假单编号应该是null;当使用者点选某一张单子,想要对它进行修改(作废)时,系统应该要create一张新的单子,这张新的单子的原始请假单编号应该就是要记录他们要修改(作废)的单子的请假单编号。

2.新增一张单子时,文件申请类别是‘新增’;修改一张单子时,文件申请类别是‘修改’;作废一张单子时,文件申请类别是‘作废’

3.当使用者对一张单子进行修改(作废)时,原始的单据的文件状态,就应该设成是inactive。如果修改(作废)被驳回时,这时候就把原始单据的文件状态,设回active,而新增加的修改(作废)的单子的文件状态,则是设成inactive。

如果一张单子的编号是A001,那么经过主管核准后,它的(文件申请类别,原始请假单编号,签核状态,文件状态)应该是(新增,null,核准,active)。

如果user修改了这份单子,产生了另外一张A002的单子,那么当这张新的单子成立时,原始的单子就被mark成inactive,也就是说A001的(文件申请类别,原始请假单编号,签核状态,文件状态)应该是(新增,null,核准,inactive)。

如果A002被核准了,它的(文件申请类别,原始请假单编号,签核状态,文件状态)应该是(修改,A001,核准,active),而A001的(文件申请类别,原始请假单编号,签核状态,文件状态)则依然是(新增,null,核准,inactive)。

如果A002被退回去,它的(文件申请类别,原始请假单编号,签核状态,文件状态)则会是(修改,A001,驳回,inactive)而A001的(文件申请类别,原始请假单编号,签核状态,文件状态)则依然是(新增,null,核准,active)。

如果A002被核准了,可是user又base on A002,再次进行修改,产生了另外一张单子A003。而且A003也被核准了,则A003的(文件申请类别,原始请假单编号,签核状态,文件状态)应该是(修改,A002,核准,active),而A002的(文件申请类别,原始请假单编号,签核状态,文件状态)应该是(修改,A001,核准,inactive),A001的(文件申请类别,原始请假单编号,签核状态,文件状态)则依然是(新增,null,核准,inactive)。

如果是作废的话,这里有两种不同的做法。

有些人认为,如果为了要作废A001,user申请了A002这张单据。等到A002通过以后,它的文件状态应该要设成是inactive。它的(文件申请类别,原始请假单编号,签核状态,文件状态)应该是(作废,A001,核准,inactive),而A001的(文件申请类别,原始请假单编号,签核状态,文件状态)则是(新增,null,核准,inactive)。所以采取这种做法的话,前后两张的文件状态通通设成inactive。

有些人则认为,如A002的文件状态应该要设成是active,因为A002是这个系列里面,最后依然active的文件。所以A002通过以后,它的(文件申请类别,原始请假单编号,签核状态,文件状态)应该是(作废,A001,核准, active),而A001的(文件申请类别,原始请假单编号,签核状态,文件状态)则是(新增,null,核准,inactive)。采取这种做法的话,只有原始单据的文件状态设成inactive,已经被withdraw的单据,则是设成active。

基本上,如果系统不需要提供取消作废,这种作废文件的反向operation的话,我会建议你采取前者;可是如果你需要提供还原作废文件的功能的话,那么纪录后者就变成是势在必行了。因为你跟user confirm过了,作废了的单子就是死翘翘了,所以你决定采用前面的方法,也就是说作废的单子如果通过审核之后,你会把这系列的单子通通设成inactive。

Hint 3: 同一张单子进行修改的相关历史单据,应该要透过一次SQL查询,就可以select出来。

※※※※※※※※※※※※※※※※※※※※※※※※※※

从最后的这个例子可以看出来,如果我要把A003跟A001的关系找出来,我变成要先透过A003的原始请假单编号找到A002,再透过A002的原始请假单编号找到A001。有些人就会想啦,那如果我改了很多次,那不就是要做很多次这样的查询?如果每次查询都是一个资料库的存取,那累积起来不是很可怕吗?

所以我们会在请假单这个table再加一个栏位,叫做『起始请假单编号』。不管是A003,A002,还是A001,这个栏位通通都存放着A001。这样如果我要找系出同门的单据,只要透过一个SQL的select就可以办到了。如果已经看出这一点的人,应该就具备从幼稚园组直升小学组的实力了。

※※※※※※※※※※※※※※※※※※※※※※※※※※

这里还有另外一个关于时间点的问题。原始单据的『文件状态』,应该在什么时间被update成为inactive呢?是在新的单据送出来的时候呢?还是在老板已经核准了以后才去update呢?

这其实也是要问user才知道。仔细想想,你想应该是在新的单据送出时,就去update原始单据的『文件状态』比较合理。所以你需要跟user确认这整个系统的行为是否符合它们的预期:

4.1 一张单子只要开始修改或作废,一旦文件送出以后,在还没有结果前,没有办法继续进行后续的修改或是作废。简单说,就是single thread啦。同一时间你不可以同时针对同一份文件送出5,6个修正版,同时间只能有一个修正版,除非它被退回来,否则当有修正版或是作废的单据还没签核完时,你不能再送出一份修正版,要不然系统就会变成一团混乱。

4.2 只有(文件申请类别,签核状态,文件状态) = (新增,核准,active)或是(修改,核准,active)的单子,才能进行修改或作废。也就是说,如果一张单子一开始就被老板退回来的话,也不要提什么修改跟作废啦。

5.员工任职后,该年度便享有特休假7天,依照该年度任职天数的比例来计算。员工服务满1年未满3年给特休假7天,服务3年未满5年给特休假10天,服务5年未满10年给特休假14天,服务满10年,每满1年加1天,唯上限是30天。年资与特休天数对应可以弹性设定。

嗯,这看起来需要记得每个人一年有几天的假,现在还剩几天的假。所以我们来个table来记录每个人一年有多少的quota,以及还剩下多少假吧。所以quota table应该包含了(年度,员工编号,假别,今年可休时数,目前余额)。一般在请假时,可能是用小时做单位,所以可休天数要转化成可休的时数。

每次user要去请假之前,我们就到quota table里面先检查这个假的余额还够不够,如果余额已经不足了,这张假单就应该要送不出去。如果余额还足够,就重新计算一次这张假单的余额,把请假的时数从余额中扣除。当然,如果user的假单被驳回或是作废了,原本扣掉的时数就要还回来。如果假单被修改了,原有单据的时数要还回去,新单据的时数则是要扣掉。

除此之外,还得要纪录年资与特休天数的关系喔。好吧,系统再多开一个table纪录年资与quota的对应关系。我们就叫它特休年资设定档好了。应该有下列栏位:(年资,可休时数)。

嗯,看来会需要一支程式,每年年底的时候,都依据特休年资设定档,以及每个员工的年资,来产生每个员工第二年的quota。

6. 每年未休完的特休假,可保留至次年使用。

我就知道会需要一支程式,每年产生员工的quota。这简单,就把这个年度的余额设成0就好了,下个年度的资料则是把这个年度的特休假余额加进去就ok啦。

Hint 4:确认每个data item的合理范围(domain),并且澄清每一个你对于requirement的假设。

※※※※※※※※※※※※※※※※※※※※※※※※※※

通常优秀的幼稚园生,还会问一下特休假的余额,有没有可能变成是负的,也就是超休。你可能会问,怎么可能会超休?老实说,我也不知道。这通常就是要去问user的事情。如果有可能变成负的,又该怎么处理?这当然就要顺便问user啦。你如果有任何假设,其实都应该找user澄清。

※※※※※※※※※※※※※※※※※※※※※※※※※※

7.使用者可以依照月份与假别,查询当月各种休假的时数统计资料。

这没有问题,我们的table里面有假别,也有请假开始时间跟请假结束时间,也有请假时数,这只要动态计算出来就好了。

8.请假时数月份的归属,应该视使用者申请假单时的班表,来判断请假时数应该算是哪一天的请假,再依据这个日期进行时数的切割。例如6/30上夜班(20:00 ~ 05:00)的人,如果请假从7/1 01:00请假到 7/1 05:00,那么他的请假时数应该算是6/30的请假,因此时数应该归到6月份。

当你看到这一条,再看到第7条,或许你就会发现这个问题好像比想像中来得复杂。你需要有一个table来记录每天的班表,接下来,还要去按照班表去拆解时数,归属到不同的月份。每个不同的班别,还有不同的开始时间与结束时间。嗯,所以你会需要有一个纪录班表的table,原则上就会是(员工编号,日期,班别),而且还会另外有一个纪录每个班别开始与结束时间的table,应该有这几个栏位:(班别,开始时间,结束时间)。

这里牵涉到一个困难的问题,如果这个人的班表变了该怎么办?所以你在这里做了一个假设。既然是要看统计资料,当然是要看最准的那一份。如果user的班表变了,我们就按照最新的班表,来动态产生每个假别每个月份的统计数字。

于是乎你写下了这个假设,并且忽然警觉到,这个问题好像比你想像中的还要严重。那如果这张假单请了一个很长很长的假,那计算不会很复杂吗?你要考虑每天的班表,还要拆解每天的时数,如果这中间有假日怎么办?如果这张假单跨了好几个月怎么办?

嗯,看来你的班表table需要再多一个栏位『假日』。合理的值就是(1,0)。1是假日,0是上班日。

至于计算可能会很复杂很慢的这个问题呢?嗯,为了要看到最即时,最正确的资料,浪费一点CPU time是必要之恶,User一定会很认同这样的看法。于是乎,你放下了心头的不安,开始进行其他的设计工作。

基本上呢,幼稚园组的人可能到这里就会告一段落,认为大多数的需求都已经考虑到了,已经可以开始design与coding了。等到开始要进行coding了,这才发现,咦,好像requirement还是有很多不清楚,不了解的地方。整个design也不是说错,不过就是觉得不太容易implement,整个程式好复杂。嗯,这是因为这套系统本来就很复杂。好啦,不管怎么说,我就是要让你从幼稚园毕业啦。让我们进入下一个阶段。


小学组
======
有些幼稚园生遇到implementation出问题的时候会回头看看,看看是不是还有什么遗漏的地方。这样的人可以从幼稚园组毕业,进入小学生组。基本上幼稚园组所考虑到的因素,小学生都考虑到了。不过呢,小学生大概会发现下面这些应该要注意的地方。

1. 使用者线上填写假单。送出后,依照签核权限,送给相关的主管核决后,假单就生效。

除了请假单以外,那员工的基本资料呢?我们要怎么找出这个员工的主管?这是不是跟部门档有关?签核权限又会是什么呢?如果有好多人要签,那他们签核的顺序是怎么排出来的?是每签完一个人,看看是否已经签完了,还是要一开始就决定应该要签核的主管呢?

所以小学生在经过思考以后会觉得,应该要有一个员工基本资料档,有一个部门档,跟使用者确认后,发现这两个table应该跟人事系统进行整合。每天sync一次资料。这样子看来,应该要有一支独立的程式来负责这件事情。

整个签核的名单,则是在使用者送出假单时,就已经决定的,主管依照所管理的部门,在组织图上的高低顺序来签核。嗯,所以原本签核纪录档的设计还不周详,还应该要增加一个『签核顺序』的栏位。

此外,主管看到的功能,是不是跟一般的员工又不一样呢?因为他们有单子要签啊,总要有画面给他们进行审核吧。

所以另一个小学生一定会发现的东西,就是权限控管(access control)。对一个全新的系统来说,权限控管通常是在估计scope时会被忽略掉的部分,不过在implementation就会冒出来。

一般最简单的做法呢,就是把每个人对应到一个角色上,再依据不同的角色,来决定他可以看到的功能。基本上需要两个table。一个是纪录user – role的关系,栏位通常是(员工编号,角色)。另一个则是纪录role – function的关系。通常就是(角色,可以使用的功能)。

依据这样的设计的话,每个程式的进入点,通常就是使用者会看到的menu,就应该要有一个功能编号,每次要产生user看到的menu时,就依据这个user所扮演的各个角色,来决定user是否可以看得到这个功能。等到使用者点选这项功能时,再呼叫相关的程式产生画面进行处理。

2.生效以后的单据,只要还在期限内,使用者都可以线上进行修改或是作废。

嗯,看起来原本的设计无懈可击。

3.修改与作废的期限是由系统管理员进行设定。

小学生看到这个statement,心中不禁窃喜,果然role-function的设计是有用的。看到没有,系统管理员!这分明就是一种很特别的role。对于这种特别的role,有些人会觉得应该要特别create一个table,并且增加一个栏位,来说明这个role是系统管理员喔。因为系统管理员,是跟其他的role完全不一样的role喔。所以我们会建立一个role table:(角色编号,说明,是否为系统管理员)。

当然啦,有些人会说,干嘛增加栏位?我们就hard code来解决这个问题好了。例如role table一定要有一个role叫做Admin。这个role就是系统管理员。通常这样子implement的人,还会遭到内部的批评,因为这样子是hardcode,如果user要改程式的逻辑,就会缺乏弹性…

其实对于大多数的使用者来说,改变role function,或是哪个role当作是admin,其实差异并不大。他们care的只是角色的设定要符合他们的预期,以及每个user要扮演哪些角色才是对的。所以这部分要hardcode或是要保留弹性,其实对真正的user来说,差异并不大。

很多软体公司都喜欢强调他们在access control方面做得多有弹性,多么地复杂。因为对他们来说,这些功能虽然对于使用者来说差别并不大,不过这其实是他们唯一擅长的地方。

4.修改或是作废单据经过核准后,原始单据就失效,若是修改或是作废单据被主管驳回,原始单据依然有效。

嗯,看起来原本的设计无懈可击。不过原先的推论4.1, 4.2还没有跟user确认。这次就再多花一些时间,跟user确认这个部分。

4.1 一张单子只要开始修改或作废,一旦文件送出以后,在还没有结果前,没有办法继续进行后续的修改或是作废。简单说,就是single thread啦。同一时间你不可以同时针对同一份文件送出5,6个修正版,同时间只能有一个修正版,除非它被退回来,否则当有修正版或是作废的单据还没签核完时,你不能再送出一份修正版,要不然系统就会变成一团混乱。

4.2 只有(文件申请类别,签核状态,文件状态) = (新增,核准,active)或是(修改,核准,active)的单子,才能进行修改或作废。也就是说,如果一张单子一开始就被老板退回来的话,也不要提什么修改跟作废啦。

User听到你要确认这些规则,觉得很奇怪。因为这里所描述的做法,不就是一般business的常态吗?为什么还需要确认呢?其实在进行需求分析时,双方常常会对于整个business flow会有不同的假设。User可能会认为你已经清楚地知道这些假设,就在访谈的过程中省略了。在进行需求访谈的过程里面,对于不清楚的假设,应该要随时记得加以确认。每一条规则,当然是越清楚明白越好啰。既然user确认了这几条规则,以后就可以照着implement啰。

5.员工任职后,该年度便享有特休假7天,依照该年度任职天数的比例来计算。员工服务满1年未满3年给特休假7天,服务3年未满5年给特休假10天,服务5年未满10年给特休假14天,服务满10年,每满1年加1天,唯上限是30天。年资与特休天数对应可以弹性设定。

咦,年资如果跟特休天数要可以弹性设定的话,那是否会有什么时候生效,什么时候失效的问题?对于小学生来说,有些比较见闻广博的人就有听过生效日跟失效日这两个名词。毕竟系统做久了,就会有些user特别强调:『你们在设计参数设定档时,要可以输入生效日跟失效日喔。』

所以这些小学生们,就会在原先的特休年资设定档增加两个栏位,变成了(年资,可休时数,active date,expire date)。这样一来,如果user想要改变每个设定档的active date还有expire date,就直接下去改就好了。

有了这两个栏位,我们就依照目前的系统日,来决定我们在判断特休时,应该抓哪一笔资料。Ok,这样看起来应该可行。

重新看了一下quota table的设计,嗯,看起来应该没有问题。

6.每年未休完的特休假,可保留至次年使用。

再次回想这条规则,仔细想想quota table的设计,看起来虽然没有问题。不过总觉得有什么令人不comfortable的地方…

想想看,什么时候会用到这条规则?不就是年底或年度开始的时候吗?对了!什么时候产生第二年的quota,这就会是一个问题。

如果我们的系统允许你事先请假的话,那么什么时候产生第二年的quota,就会是一个要考虑的因素。因为到了过年可能有很多人会想出国去玩,所以他们会想要请明年年初的假,可是如果他们想请假的时候,却还没有quota可以请,这不就完蛋了吗?所以你打算在每年的十二月中旬,产生第二年的quota…

啊,这个时候这一年还没有过完啊,我们怎么确定每个人今年的特休假还剩多少天?

事实上你要等到user没有办法再送今年的假单,或者对于今年的假单进行修改或作废时,这个时候才知道今年没有休完的特休假,有多少要保留到明年。

这样子看起来,你需要在十二月中旬产生第二年的quota,并且要在明年的1月下旬,大家都不能再送今年的单子时,这时候再把今年的特休假余额,结算到明年的quota中。

嗯,这里又多了一个没有澄清的一个问题。修改或作废有一定的期限,那么新增假单呢?我可以到12/31才补一张9/1的假单吗?不然要是系统没有进行任何控制,等到明年我已经结算了今年的特休假余额之后,user又送了一张今年的特休,那不是完了吗?

所以你拿这个问题跟user确认。结果发现,他们的确有一条不成文的规定,请假手续需要在请假开始的5天内完成请假手续。只是前人在进行访谈时,并没有纪录这一点。这还不简单,既然知道了这个需求,我们再把系统参数档增加一个栏位,(新增期限)。这不就搞定了?

除此之外,如果进一步考虑特定时间点的话,到了年底时,有些跨年的假,就有可能会分别用到两个不同年度的quota。这部分的逻辑,看来得要进一步修改。系统应该要特别处理这种刚好跨年时,会用到两个不同quota的状况。

Hint 5:如果使用者的操作,在不同的时间点发生,有没有什么要特别处理的地方?

※※※※※※※※※※※※※※※※※※※※※※※※※※

当我们请假时,会去检查quota,所以quota table里面的资料,在请假前就应该要存在,这是我们在设计申请假单的相关功能时的一个基本假设。

每个系统在设计时,都会有一些假设,所以我们要常常问自己的事情就是,这个假设会不会在某个时间点时不成立?如果不成立的话,会有什么影响?为了处理这些问题,你要进行什么样的处理来避免这些special case的发生?

看起来没有问题的data model,经过时间轴的作用,发生各式各样不同的event之后,常常会叙说一个完全不一样的story。这也就是为什么传统的结构化系统分析,会建议我们画资料流程图(Data Flow Diagram, DFD)。

当你要分析某一项功能时,你得要想想跟这个功能有关的资料,是否已经建立起来?这些资料又是由哪些程式负责建立的?按照目前的设计,会不会在某个特殊的时间点时,该负责create data的程式没有create你所需要的data?在分析商用程式时所要考虑的重点,其实就是在验证你心目中的系统要能正确地运作,每一个必要的假设是否成立。一般来说,你可以优先考虑boundary condition,这跟我们在设计测试个案时的guideline是一样的。

所以请假时,要考虑如果user请那种今年年底到明年年初的假单,或是在年度结算特休假的批次作业(batch job or cron job)执行之前,与执行之后的假单,有没有办法请得过?会不会有什么边际效应?也就是说,在思考时要想想各个不同的event发生前与发生后,对于系统的影响会是什么?有没有什么是需要特别处理的?

※※※※※※※※※※※※※※※※※※※※※※※※※※

7.使用者可以依照月份与假别,查询当月各种休假的时数统计资料。

8.请假时数月份的归属,应该视使用者申请假单时的班表,来判断请假时数应该算是哪一天的请假,再依据这个日期进行时数的切割。例如6/30上夜班(20:00 ~ 05:00)的人,如果请假从7/1 01:00请假到 7/1 05:00,那么他的请假时数应该算是6/30的请假,因此时数应该归到6月份。

每次都动态去计算,这好像又太过辛苦了一点,速度也太慢了一些。每次都要动态去计算的话,这样CPU会受不了。我们应该create一个休假时数统计档。每次要显示统计资料时,就直接从这个table里面抓就好了。嗯,一般都是照月份来统计,所以table的栏位应该是:(员工编号,年月,假别,请假总时数)。

这个休假时数统计档既然已经建立起来,那每次请假时,就把时数就加到这个档的总时数;每次单子被驳回还是作废时,就把时数从该月的总时数里头扣掉;遇上修改的状况时,则是把旧单子的时数扣掉,把新单子的时数加到相对应的资料中。嗯,看起来轻而易举。

经过小学生的调整之后,通常系统已经可以正常运作了。不过有些时候,如果系统有了bug,就会让很多资料造成错乱。每个错误是怎么造成的呢?老实说,如果你只有看到结果,却没有看到计算的过程,那么到底哪个环节出了错,就会很难看得出来。例如你看到一个人,今年还有10天的特休假可以休。依照他的年资,你知道他今年应该有15天的特休假,去年还剩下3天的特休,所以照理说他应该有18天的特休可以休。可是你怎么看他今年的假单,怎么加就只有请了5天的休假。你不知道是今年算错了呢?还是去年的余额没有加进来?还是请哪一张假单时多扣了?还是修改与作废的时候没有把quota还回去?

这种问题很有可能等到测试阶段才会一一浮现。你遇到一个又一个的bug,并且一个一个就发生的徵兆加以分析,常常花了很多时间下去追,却还是找不出来,为什么quota table的余额会出问题。

也有可能当user查询统计数字时,发现统计数字与单据不合,可是你看了半天,怎么也看不出来为什么统计数字会变成现在这个数字。每次遇到这样的问题,丢回去给programmer,programmer也不明所以,最后只好埋很多资讯在log里面,希望当问题发生时,可以从log解读出到底系统发生什么问题;或者一方面尝试reproduce,一步一步去trace变数的值,以便诊断出问题的成因,再去修改相对应的程式。

用听的也知道这种做法不怎么样。有没有其他的解法呢?这个时候就应该是从小学毕业进到国中的时候啦。


国中组
======
在此先介绍一个我个人觉得,商用系统最重要,也最基本的一个观念,也就是:『余额档与交易档』,或者说是『存量与流量』的关系。我想从下面的这个夫妻吵架的场景开始。

女王:你现在身上还剩多少钱?

奴隶:我还有三十块。

女王:什么?你的钱都花到哪里去了?你今天出门时,我不是才给过你一百块钱吗?你昨天明明身上还有五十块,上下班坐公车,总共三十块,吃公司的便当,固定就是五十块,就算你买包饮料好了,也不过就是花个十块钱。你说,你身上的钱到底花到哪里去了?

奴隶畏畏缩缩地说:我…我不知道?

女王:好啊,你的胆子越来越大了。男人啊,一有了钱就想到外头乱搞,真不老实,居然还想藏私房钱,一定是勾搭上哪个狐狸精了?要说谎也不打草稿,不是跟你说每一笔开支都要清清楚楚地记帐吗?你敢情是皮在痒了,太久没有跪主机板了?

奴隶:我…我今天很忙,从早上一直开会到下班,我忘了记。

女王:哼,你把身上的东西都掏出来我看看。你跟天借胆子啦。连规定你要做的事情都敢不做,忙,忙就可以不记帐啊?一点规矩都没有,待会儿有你好受的……

追查『你现在身上还剩多少钱?』以及『你的钱都花到哪里去了?』,这并不是母老虎的专属权利;很多大男人也会用同样的问题来质问可怜的小女人。很多系统,其实也是为了解答这两个问题,而设想出来的。

为了解答这些问题,通常我们会创造一个余额档,来显示现在还剩下多少钱;另外则会是创造一个交易档,详细纪录发生过哪些交易。我们拿存折来当例子好了。余额档指的就是在不同的时间点,你的户头里面还有多少钱(存量)。交易档指的则是一笔一笔的存入,与提领的记录(流量)。也就是说余额档记录了那个时候系统的状态,而交易档则是描述了系统是怎么从一个状态转变到另外一个状态。

如果我们把这个观念加以延伸,其实不只是余额资料。大多数的summary资料,例如财务报表,每月的时数统计资料…其实也有这样的特点。在会计总帐系统里面,存量指的就是每个会计科目的余额,流量指的则是每一张不同的传票分录;在库存管理系统里面,存量则是某个库存项目在某个储存位置目前的数量,流量则是各种出库、入库…单据的明细资料;对于请假系统来说,每个人各种假别的quota还剩多少余额的资料是余额档,记载的是存量;请假单则是交易档,记载的是流量。

让我们先回顾一下目前的设计。

请假单table:
(请假单编号,文件申请类别,原始请假单编号,起始请假单编号,员工编号,请假日期,假别,请假开始时间,请假结束时间,请假时数,CreateTime,签核状态,文件状态)。

quota table:
(年度,员工编号,假别,今年可休时数,目前余额)。

休假时数统计档:
(员工编号,年月,假别,请假总时数)。

咦,我们现在的设计没问题啊。Quota table跟休假时数统计档纪录的是系统的存量,请假单是流量啊。怎么还会有需要修正的地方?

还记不记得有跨年,跨月的请假这回事?当我们跨年时,可能会同时从两笔不同年度的quota table里面的资料,减去目前的余额,每一笔减去多少,这件事情我们纪录下来了吗?跨月份请假时,我们会update两笔不同月份的休假时数统计档,每月分别增加了多少,这件事情我们纪录下来了吗?那又纪录在哪里呢?

看来我们势必要增加table来记载这些资讯。我们可以增加两个table,分别称为quota-transaction,以及statistics-transaction。用这两个table来记录每张假单,对于quota table的余额所做的增减,以及对于休假时数统计档的时数所做的增减。

每次新增假单时,我们就把每张假单到底用了多少quota记录下来。所以quota-transaction里面的栏位应该要包含quota table与假单table的primary key,以及对于quota table的余额所造成的增减,栏位应该是:(请假单编号,年度,员工编号,假别,时数)

同样的道理,statistics-transaction就应该是:(请假单编号,年月,员工编号,假别,时数)

增加了这两个table后,如果我们发现quota table里面的余额,或是休假时数统计档的时数,与假单不符合时,我们就可以验证下面几点:

1.单一假单在跨年,跨月时,quota-transaction以及statistics-transaction的时数是否拆解正确?

2.单一假单所属的quota-transaction以及statistics-transaction,时数总和是否与假单的请假时数相符?

3.quota table的余额演变是否与quota-transaction相符?休假时数统计档的时数变化,是否statistics-transaction相符?

第一点是domain上的问题。主要是跨年跨月时的系统的逻辑是否正确。透过适当的test case来验证,可以很容易就验证出来,系统在处理boundary condition时的程式是否正确。第二点、第三点则是在验证,资料的一致性。很多时候,因为程式的bug,或是没有处理好race condition,整个balance的值可能会错乱。这个时候,如果可以先验证第一点以及第二点,确定每一张假单的时数拆解没有错误时,总和的加总与请假单相符时,我们就可以依据请假单,quota-transaction,以及statistics-transaction来重新计算出正确的quota table以及休假时数统计档里面的资料。

看起来没问题…且慢,那这个设计要怎么处理修改与作废的状况?嗯,我们要先找出原始请假单。接着从资料库里面找出那一张单子所对应的quota-transaction还有statistics-transaction的资料,先把用掉的数量还给相对应的quota table,与休假时数统计档,接着再依据新的单据,新增新的quota-transaction还有statistics-transaction的资料,再update相对应的quota table,与休假时数统计档,这样应该就可以了。

咦,既然想到了修改与删除,一张单子如果被修改或是删除了,哪么原始单据的quota-transaction应该要怎么办呢?嗯,反走过必留下痕迹。我们还是留下来好了。只是日后在推算余额档时,我们就依据请假单的(签核状态,文件状态)来决定,这些quota-transaction是否会影响quota table好了。

Hint 6:对于任一余额档,应该用单一的table逐一记载,每笔不同的交易,所造成的余额变化

※※※※※※※※※※※※※※※※※※※※※※※※※※

这好像是基本常识,何必特别说明呢?

第一个重点是单一的table。有些时候,设计上的考量可能会把不同的交易,放在不同的table中,例如你的存款帐户,可能每个月会自动扣缴你的信用卡金额,也有可能会每个月扣缴你的贷款金额。就设计上来说,每个不同的交易,都应该产生相对应的资料。只是这个时候可能会join到不同系统的不同table中。

于是有些人会把跟信用卡有关的流量变化,放在一个table里,跟放款有关的放在另外一个table里面。到了要计算帐户余额时,再从各个table里面重新把资料整合出来。这样子乍看之下也没什么不好的。

不过当你要进行运算时,其实就要知道各个不同table的关联性,到各个地方兜资料。你得要把所有有关的资料,通通都找出来了,才可以看得出每个余额档里面目前的余额,到底是怎么演变成现在这个样子。这样子在进行运算以及处理时,就变得很不方便。一旦遗漏掉一个,就很麻烦。

所以为了让系统的运作变得更单纯,我们应该把所有与这个余额档有关的所有流量变化资料,放在同样的一个table里面。这样子才有办法很轻易地,去描绘出来某一段期间里面,到底发生了什么事情。

所以拿存款系统来说,不管是偿还信用卡的款项,还是利息的计算,不同的sub-system所进行的不同交易,都应该要产生一笔独立的存/提款记录,记录在存折上。这样子一来,任何时候只要我们看到存折上面的说明,就可以了解我们的存款余额,是在经历了什么样的浩劫以后,才会剩下这么一丁点儿。

有一个统一的地点来记载以后,剩下的就是要确定每次进行余额的异动时,都要在这个table里面可以找到一笔相对应的资料。

※※※※※※※※※※※※※※※※※※※※※※※※※※

当你体会了存量与流量的观念以后,可能就觉得这个系统已经相当的完备了。等一下,还有没有什么假设,是我们没有质疑过的?

每当我在问自己这个问题时,我通常都会一再地反问,在执行某个功能时,它的假设会是什么?如果与这个假设有关的资料,我们找不到的时候,要怎么办?如果有那种master detail的关系,没有master的话,detail又要从何而生?

仔细想想quota-transaction。嗯,请假单当然是在user点选请假单时产生,quota table当然是跑年度的batch job才跑出来的。这有什么困难的?

咦,回想一下quota table的key。看起来应该是由(年度,员工编号,假别)三个组合而成的。而目前我们只考虑到年度不同时,应该要怎么处理。那员工编号发生变化时呢?还有假别发生变化时呢?

员工编号什么时候才会发生变化呢?对一个人来说,什么时候才有可能改变他的编号呢?只有离职后,再重新雇用进来时才有可能吧。离职以后再回锅任用,我们会不会再给他另一个不同的编号,这就要看公司的政策了。问过user以后,确定员工编号应该是不会变的。

如果我们不从个人的角度来看,而是从所有的员工的角度来看,员工编号的变化到底代表了什么意思?

嗯,员工编号的变化,就代表了有新进的员工,有离职的员工。按照我们先前的思考逻辑,单据作废都不敢砍掉了,离职员工的资料当然还是应该要保存下来。那如果有新进的员工呢?这些人如果有什么假要请,在quota table里面,是否也应该要有相对应的资料呢?

看来我们年度的batch job没有办法处理这样的问题。因为在跑完年度batch job后再进来的人,铁定不在batch job执行时的员工档里面,这些人就没有quota啦。所以我们应该每天都检查一下,是否有新进的员工,如果有的话,就在quota table里面,新增一笔相对应的资料。

同样的道理,如果有某个假别被作废了,会不会有什么影响?照道理来说,我们应该是不会清掉相关的资料,所以问题应该不大。不过如果新增了某个假别,在这个假别生效了之后,我们就应该要在生效的当天,帮每个符合条件的员工,通通都产生相对应的quota资料。并且在日后执行daily的batch job时,也要帮新进员工,顺便产生相对应的quota资料。

一般而言,使用者会记得要求系统应该要在发现有新员工时,quota table就要增加相对应的资料;然而新增假别时,应该要产生相对应的quota资料,这就比较容易遗忘,因为大多数的user关心的都是现在的作业是什么。将来会发生的事情,可以等到将来再说。现在的假别既然是如此,那就先符合现在的需求。将来有新的假别时,到时候再说吧。

新增一个假别时,需要产生相对应的quota时,这就表示所有的员工都要产生相对应的资料。通常遇到这种状况,使用者多半会要求,这应该是要透过某个手动执行的程式,来进行新增quota的资料。而不是放在batch job里面。以免一时不小心建错资料,造成一大堆后遗症。

Hint 7:要challenge目前对于某个table design是否完备时,先想想看你要怎么样,才能从这个table中,identify出来一笔资料与其他资料的不同?也就是说你会设下什么样的primary key或是unique constraint?接着依照这个unique constraint所对应的data item来思考,如果这些data item发生了变化,你要怎么handle?例如对于quota table来说,一个人的员工编号如果被改掉了,这该怎么办?假别被改掉了,或是产生了一个新的假别,这该怎么办?

※※※※※※※※※※※※※※※※※※※※※※※※※※

从data model里面,进行反反覆覆的推敲,challenge你自己的各种假设,可以帮你找出很多潜在的问题,以及应该要提供的功能。这通常是运用其他开发方式进行开发的人,不会去思考到的问题。

这也就难怪很多从OO角度来看待商用系统的人,都把解释business rule以及与domain相关的分析设计工作推到user或是domain expert的身上,因为从object oriented analysis与design的过程里面,并不会用这样子的方法来思考问题。这可能也就是为什么,采用OOAD开发的人,会把那么多系统本来就应该要有的功能,列为requirement change的原因吧。

※※※※※※※※※※※※※※※※※※※※※※※※※※

如果可以了解quota-transaction table的奥义,也可以想出新增员工与假别时,应该要提供产生quota table的程式,哪么这套系统不管在function上,以及功能上,基本的运作以及交易的追踪,就已经有了大致的雏形。能够做到这样,就算是具有国中程度了。


高中组
======
当我们开始了存量与流量的记录之后,接着想加强的,就是系统的trace ability。基本的出发点还是一样,只是我们会开始一个动作一个动作地来仔细分解,每个动作都会去跟背后设计的逻辑互相印证。

对于高中生来说,新增一张假单,应该把签核状态拆散来看。当你新增一张假单的时候,其实文件刚送出时,你就已经把quota的余额减掉了,以避免其他假单会用掉这个目前未知的余额。

如果假单真的核准了,那么对于quota-transaction这个table来说,里面纪录的值就是对的。可是如果假单被驳回了,这个时候quota table的值,就应该被还回去。问题是,这个还回去的动作,到底纪录在哪里呢?

我们先前曾经讨论过:『要确定每次进行余额的异动时,都要在这个table里面可以找到一笔相对应的资料。』因此,这里就会发现,我们在把quota table的值还回去的时候,其实并没有纪录在quota-transaction这个table里面。那也就是为什么,我们方才需要把quota-transaction,配合假单的审核状态与文件状态合并起来考虑。

此外,像是我们针对单据进行作废或是修改时,我们也会把原始单据的quota还回去,可是进行这个还的动作时,其实也没有详细纪录到底还了多少,有没有什么做错的地方。更不要提在执行年度的结算quota的batch job,或是新增次年度的quota时,我们也没有特别把资料记录在quota-transaction中。

仔细区分一下,对于quota这个余额档的operation,可以分成五种不同的类型。

1. Create:这是在新增/修改一张新的请假单时产生。

2. Reject:这是因为老板把单子退回来,所以要把quota还回去。

3. Return:因为要修改或作废一张单子,所以要把原有的quota还回去。

4. Batch Job Create:这指的是产生新年度资料,或是新进员工产生quota资料。

5. Batch Job Update:这指的是年度结算时,把旧年度的quota带到新的年度时的update。

如果我们在quota-transaction里面再增加一个栏位,说明这笔资料的建立,是属于上面所提到的哪一种类型,并且每次增减的值,都详细纪录增减的值,这样是否就已经尽善尽美了?

经验告诉我们,记录下来quota-transaction的CreateTime会很有帮助。你可能会说,咦,这个资讯不是已经就记录在请假单里面了吗?这里就会牵涉到一个小小的技巧─De-normalization。

Hint 8:适度地进行de-normalization

※※※※※※※※※※※※※※※※※※※※※※※※※※

小时候上过资料库程式设计课程的人,资料库的正规化(normalization),这绝对是耳熟能详的概念。在学校时,经过老师的熏陶,总觉得如果要设计table schema,正规化是一定要的啦。

后来开始工作以后,常常看到别人的table里面,居然没有normalized。内心不禁奇怪,咦,这些人是没上过database的课吗?怎么连这么基础的观念都没有?让我来帮忙好了。小时候闲闲没事看,常常就会想要做做功德,于是乎就会顺手帮他们做一下normalization。

一直到后来,才发现很多时候,我们会刻意地不去做normalization,甚至还会把原来已经normalized的table de-normalized。为什么呢?我仔细地推敲,基本上,考虑的重点有两个:

1. Performance

2. 留底,也就是保留历史记录

举一个最简单的例子好了。我们有一个客户资料档,里面就是(客户编号,客户名称,客户住址)。另外有一个订单资料档,里面则是记载了(订单编号,客户编号,客户名称,客户住址)。

学过normalization的人就会说,咦,你应该要做normalization啊。不然,如果客户的地址改了,客户公司的名称变了,你要怎么办?所以订单资料档,应该就只保留(订单编号,客户编号)。

对于现在的我来说,我就会从不一样的角度来看待这个问题。

1. 我在显示订单资料时,常常会需要显示客户的名称跟住址。如果performance不好时,我就会留一份资讯在订单资料档里面,这样减少一次table join。

2. 如果客户真的改了地址,如果某天,你临时需要把原始订单列印出来,你没有把资料留在订单资料档里面,所以只好把最新的客户资料档的资料印出来。印出来的文件,不就会跟以前印出来的文件不符合了吗?如果这时候原始文件又被不小心找到了(这种事情常常发生啦),两相比较一下,怎么两张单据变得不一样了呢?当这种事情如果造成争端时,你该怎么办?你的系统有任何地方记得客户的旧地址吗?如果他一直吵没收到这批货,你又没有原始单据可以来查证,这时候该怎么办?

所以我们会在某些特殊考量的情况下,刻意地进行de-normalization。不是为了performance就是为了留底。下回看到别人的database里面,有些table没有normalized,可先不要急着笑人家。搞不好闹笑话的人会是你喔。

※※※※※※※※※※※※※※※※※※※※※※※※※※

同样的道理,高中生还会再增加一个栏位:起始请假单编号。为什么呢?当我们需要把同一个系列的单子,对于quota的变化量通通summarize起来时,你要下什么样的SQL command呢?

Select 年度,员工编号,假别,sum(时数) from quota_transaction where 起始请假单编号 = ‘A001’ group by 年度,员工编号,假别;


这样一来,就不需要再多join 请假单table了。否则,可能会很麻烦。你变成要


Select 年度,员工编号,假别,sum(时数) from quota_transaction where 请假单编号 in

(select 请假单编号 from 请假单 where 起始请假单编号= ‘A001’)

group by 年度,员工编号,假别;


考虑了这些因素以后,你的quota-transaction table就应该设计成:

(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号)

而OperationType就应该包含下面五种不同的值:(Create, Reject, Return, Batch Job Create, Batch Job Update)


还是举个例子好了。

员工编号007的user请了一张假单,编号A001,用到2003年的特休假。所以你会产生一笔资料:(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号) = (A001, 2003, 007, 特休, 8, Create, 2003/9/15 13:00:00, A001)

(Case 1) 这张单子被reject时,这时候你多了另外一笔资料。(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号) = (A001, 2003, 007, 特休, -8, Reject, 2003/9/15 14:00:00, A001)

(Case 2) 如果这张单子被核准了,可是你发现,糟了,我的单据送错了,我应该请病假才对。于是你修改了以后,送出了一张新的单子,单号A002。

这时会产生两笔资料:(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号) = (A002, 2003, 007, 病假, 8, Create, 2003/9/15 15:00:00, A001)

(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号) = (A002, 2003, 007, 特休, -8, Return, 2003/9/15 15:00:00, A001)


如果这个时候,老板看了觉得很讨厌,就跟你说你不可以请病假,用特休请了就该用特休假,就把你的单子退回来,这时候要做的事情,就是把原先因为这张单子所造成的效应抵销。所以你会再产生两笔资料:

(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号) = (A002, 2003, 007, 病假, -8, Reject, 2003/9/15 16:00:00, A001)

(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号) = (A002, 2003, 007, 特休, 8, Reject, 2003/9/15 16:00:00, A001)


这时候你想一想,算了,还是来上班吧,所以你就based on A001,送出一张作废的单子A003。这个时候,这时会产生一笔资料:(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号) = (A003, 2003, 007, 特休, -8, Return, 2003/9/15 17:00:00, A001)


因为老板喜欢当tester,不然我也不知道老板为什么心血来潮想要再退一次你的单子。这时候(请假单编号,年度,员工编号,假别,时数,OperationType,CreateTime,起始请假单编号) = (A003, 2003, 007, 特休, 8, Reject, 2003/9/15 18:00:00, A001)


所以每次你要reject一张单子,你就依据你要退回去的单据编号,把相关quota-transaction的资料通通找出来,接着把所有的quota-transaction给还原。原本正的,就变成负的。并且把每一个动作都清楚地写下来。

当你要modify一张单子,除了要把新的单子的quota-transaction给insert进去以外,还要记得把原始单据所create的quota-transaction给还原。

至于要作废单子的话,就记得把原始单据所create的quota-transaction给还原就行了。

嗯,看起来有关quota的记录应该是万无一失了。且慢,我还是不懂为何要把原始单据编号放进来。刚刚提到的这段SQL又有什么用呢?

Select 年度,员工编号,假别,sum(时数) from quota_transaction where 起始请假单编号 = A001 group by 年度,员工编号,假别;


其实这也是为了检查的程式所设计的。有些时候我们会想要检查程式的正确性。我们可能会把目前active的单据,拿这张单子,跟新增修改再修改…这整个系列所扣的quota来做比较,看看资料是否相符,如此而已。如果没有这种需求,其实就可以考虑不用在quota-transaction再多这一个栏位了。

仔细想想,嗯,看来有关quota的这个部分应该无懈可击了。没错,这个部分我也想不出什么好继续谈下去的,这个时候就该是进入大学的时候了。


大学组
※※※※※※※※※※※※※※※※※※※※※※※※※※

我觉得在最后的这个phase,我们考虑的应该是,怎么打造出一个够robust的系统,可以完整的记录在时间不断流动的过程中,整个系统的变化。

怎么说呢?我们曾经推敲过,员工编号变动所代表的涵义。不过,wait a minute,我们只处理了有新进员工时要怎么办的状况啊,如果他有其他资料改变的话,我们又该怎么办呢?我们是否需要记录每一个员工资料的变化呢?

对于请假系统来说,他关心的可能只有员工资料的一小块。例如员工的姓名,到职日,属于哪一个部门,这几个栏位。这个人是什么时候来的,会影响到特休的计算,姓名是拿来印在各式各样的报表或显示他的基本资料的时候会用到,部门资料则是在进行签核时会用到。

如果这些资料改变了,对系统会有什么影响?而你觉得系统需不需要记得这些资料曾经是什么样子?

再举另外一个例子好了。员工编号007的这个人,因为表现良好,从9/1起开始升官,原本他只是个『情报员』,9/1以后变成了『高级情报员』。而现在是8/15,你收到了他要升官的这个指令。你想要把这个资讯记载在电脑里。

对于天真无邪的小朋友所设计的系统来说,系统永远不记得曾经发生过什么事情,也没办法知道将来要发生什么事情。所有的事情都只有当下,也就是这个栏位现在是什么值。要是你觉得不对,那就直接改吧。

你问他三个月前是什么?如果我有设计个历史档来记录,那算你运气好。有这种需求,你要早点讲啊,这是requirement change。

举另外一个例子好了。User跟你说:『喂,我现在有笔资料,是半个月以后才生效,哪该怎么办?』『那你就写张小纸条,贴在你的隔间墙上,半个月以后记得新增这笔资料不就得了?』

想也知道这样会被K。你可能会说,这个简单,我每个table都记载着生效日、失效日好了。这样子,他高兴怎么改,就怎么改啊。我们原先在设计参数档时,不就是这样子做吗?


考虑下面这几个问题吧。

1. 如果有两笔资料,生效的期间重迭,你该怎么处理?一个员工可以同时有两笔员工基本资料是有效的吗?同一个参数有两笔资料同时有效,该怎么办?

2. 如果有资料上的空窗期,那该怎么办?一个参数1/1 ~ 8/31有效,另一个参数11/1以后才生效,那我9/1~10/31就中断了,没有参数可以用了,后来才发现啊,原来是资料打错了,这该怎么办?

3. 如果我根据参数产生相关的资料以后,这时候返回来改参数。所以你有旧的结果,新的参数。不明究里的人看到这种不一致的状况,就challenge系统:『为什么跑出来的结果,跟现在看到的参数完全兜不起来?』这时候,你又该怎么办?

每一个robust的系统,都应该考虑到,任何时间点,都有可能会有人为的错误发生。所以任何时候,都应该要让系统可以回到历史上的某个时间点,依据正确的值,重新再跑下去。这也就让我们的系统,需要能够记得历史上的『曾经』。

为了解决这个典型的问题,我通常会用三个table,跟一个独立的batch job,来解决这个问题。这三个table我通常给的名字就是现值档,异动档,历史档。

拿我们先前提过的例子来说明好了。假设我们的员工基本资料档就只有两个栏位:(员工编号,职称)。这个table就是我们的现值档。里面没有任何历史的回忆跟未来的梦想,就是单纯现在是什么值。

当我们要修改现值档的资料时,我们并不直接去update这里面的data。我们做的事情是把资料写到一个异动档里面。帮他取个名字吧,就叫员工资料异动档。这个table通常会有的栏位是:(sequence number做成的primary key,员工编号,新职称,生效日期,状态,CreateDate)。状态很单纯,就是记载着(已处理,未处理)。

而历史档顾名思义就是存放着已经变成古董的资料。不过通常我会把现在的资料也留一份在这里,算是埋一个伏笔吧,接下来本山人自有妙用。基本上的栏位设计会是:(sequence number做成的primary key,员工编号,职称,生效日期,失效日期,新增日期,修改日期,状态)。状态很单纯,就是记载着(active,inactive)。

当table schema都设计好了,咱们就来看看,这中间的交互作用是什么吧。一开始的时候,员工基本资料档(员工编号,职称)应该是(007, 情报员),员工基本资料历史档(sequence number,员工编号,职称,生效日期,失效日期,新增日期,修改日期,状态)则是(1, 007, 情报员, 2000/1/1, null, 2000/1/1, null, active)。

到2003/8/15时,你发现你需要新增一笔资料,这个时候,你就建了一笔异动档的资料(sequence number,员工编号,新职称,生效日期,状态,CreateDate) = (1, 007, 高级情报员, 2003/9/1, 未处理, 2003/8/15)

而这支batch job在做什么呢?每天,它都会去看看系统有没有那种生效日到了,状态还是未处理的资料。如果发现了,它就会把目前历史档active的这笔资料,也就是对应到现值档的资料的失效日,定为新资料生效的前一天,目前历史档active的这笔资料,则是把它的状态改成inactive。接着拿异动档的资料来更新现值档的资料,并且在历史档里面增加一笔新的资料,都做完了以后,再把异动档的状态改为已处理。


拿我们这个例子来说,

员工基本资料档(员工编号,职称)会从(007, 情报员)变成了(007, 高级情报员)

员工基本资料历史档(sequence number,员工编号,职称,生效日期,失效日期,新增日期,修改日期,状态)则从(1, 007, 情报员, 2000/1/1, null, 2000/1/1, null, active)一笔资料,变成(1, 007, 情报员, 2000/1/1, 2003/8/31, 2000/1/1, 2003/9/1, inactive)以及(2, 007, 高级情报员, 2003/9/1, null, 2000/9/1, null, active)两笔资料。

而异动档的资料(sequence number,员工编号,新职称,生效日期,状态,CreateDate) 就会变成(1, 007, 高级情报员, 2003/9/1, 已处理, 2003/8/15)

如果我们另外有一个程式,在运算的过程里面,会用到员工基本资料档的话,它可能就会把参考到的员工基本档的primary key,也就是员工编号,写到它记录运算过程的table中。这样一来,我们就可以从生效日与失效日的期间,找出相对应的资料。

select 职等 from 员工基本资料历史档 where 生效日 <= 我们想要查询的日期 and ((失效日is null) or (失效日 >= 我们想要查询的日期)) and 员工编号 = 我们想要查询的员工的员工编号;

咦,这还是不对啊。在这个table里面,我们除了存放员工基本档的primary key以外,也应该要放员工基本资料历史档的primary key啊。这样才能在第一时间内找出真正运算时所参考到的员工基本档,当时的值是什么。所以我们要设计这种记录运算过程的table时,应该存放两个table的primary key,包含员工基本资料档,以及目前active的员工资料历史档的primary key。

这也就是为什么,我会坚持在员工资料历史档里面,应该记录历史以及目前的完整资料。如果每笔资料一生效时,就已经记录在员工资料历史档里面的话,任何时候需要知道到底当初是参考到哪一笔历史资料时,就可以直接从员工资料历史档里面找出来。

为了performance上的考量,这里我还会用到一些小技巧。

如果我们每次要找这个员工编号,目前active的资料,它的primary key是什么的话,我们要下这样的query:

select 这个table的primary key from 员工基本资料历史档 where 状态 = active and 员工编号 = 我们想要查询的员工的员工编号;

如果这个查询很频繁的话,这就会是个很大的burden。我通常会在员工基本资料档里面再增加一个栏位,来记录目前active的员工资料历史档的primary key。所以这个table就有三个栏位:(员工编号,职称,员工资料历史档编号)

而在batch job更新资料时,就应该要把员工资料历史档的编号也抄写一份在员工基本资料档里面。No big deal。

另外一个值得注意的点在于foreign key的使用。历史档的特点就是,里面的资料有可能年代一久远,就会被备份到某个不知名的地方。如果在所有用到历史档编号的table都设了foreign key,这种备份就很难做啦。所以我会建议最好不要设任何这种类型的foreign key,以免自找麻烦。

除此之外,你可能会问我,为什么异动档要有一个CreateDate?而员工资料历史档要有一个修改日期?

这主要是为了预防使用者在时间点过去以后,再补上过去的资料,造成演算结果与逻辑不合,我们却完全无法追踪的事情发生。例如已经到了10月份,才补一张9/1生效的异动资料。这样一来,凡是9月份的交易,其实都是参考9月以前的原始资料所进行的,可是如果依照我们的逻辑,系统却会在员工资料历史档中,记录旧资料的失效日是8/31;新资料的生效日是9/1。

当然,我们也可以在异动档中再增加一个『处理日』,来记录什么时候我才处理这一个异动档的资料,把现值档的东西更新,并且多写一笔历史档。


总结一下目前的设计:

员工基本资料档:(员工编号,职称,员工资料历史档编号)

员工基本资料历史档:(sequence number也就是员工资料历史档编号,员工编号,职称,生效日期,失效日期,新增日期,修改日期,状态)

员工资料异动档:(sequence number也就是员工资料异动档编号,员工编号,新职称,生效日期,状态,CreateDate,处理日)

有多少table需要做这种处理呢?如果你的系统需要提供维护资料的机制,那么三个table应该跑不掉。所有系统参数应该都需要用这种方式来维护。如果你的系统只是从别的系统抄资料过来,像是在假勤系统里面参考到员工基本档,会计系统里面参考到部门资料档…这种你不需要提供任何资料维护功能,可是需要记得每个不同版本的状况时,你会需要现值档跟历史档。如果你不care历史上的曾经,那么,你只要保留现值档就好了。

当然,你也可以选择比较不robust的方法,就是每笔资料都可以自由maintain生效日与失效日。我以前有一个客户就坚持不要采用正统的三个table的做法,他认为这个太麻烦。他要可以自由自在的maintain生效日与失效日,这样就好了。根据他的说法,这不是design的问题,这是他们的technical requirement。所以到最后做了一个连user都搞不太懂功能是什么的系统出来就是了。做系统可以做到这种田地,还能验收,这也算是神乎其技了。

所以一般来说,大多数系统所用到的参数,或是像是人事系统的员工基本档,部门资料档,都应该要用这种严谨的方式来维护。


提到部门资料档,就想到另一个小技巧。

大多数人,包含我自己在内,一开始设计部门资料档时,都是想的美美的。要有弹性,table要normalized,所以通常设计出来的table都像是这个样子:(部门编号,部门名称,隶属的部门编号)。设计完了以后,自己都拍手叫好啊,这样子部门要几层就有几层,还非常有弹性。

后来就发现,这样子很不好。我不知道遇上多少次有那种要在萤幕上画一个完整的组织图,还有要按照某一个层级的部门来summarize资料的情形。什么按照部门别来列印统计报表,按照厂处来列印统计报表,按照利润中心来列印统计报表…搞不完啊。

每次遇到这样的需求,发现你根本就没有办法轻轻松松地下SQL办到这种需求。因为树状结构,可能要一个node一个node去扫,才可以把整个tree给弄出来。这就表示了很多次的SQL statement。如果单纯要画组织图还简单。我们可以把所有table的资料,读到memory里面以后,再想办法把tree construct出来。可是遇到我们想下SQL statement产生统计报表时,就很讨厌了。

解决的方式倒也简单。你先确定一下,这个组织图到底会有几层。接着把这整个org tree,摊开来以后通通记录在table里面。如果org tree最高就是N层,基本的设计方法是(部门编号,部门名称,所属第1层的部门编号,第1层的部门名称,所属第2层的部门编号,第2层的部门名称…所属第N层的部门编号,第N层的部门名称)。接下来每次有人异动部门资料时,就重新更新一次这个table里面的所有资料。

大学可以毕业了吗?其实还没。我还想讨论另外一个观念,就是结算的概念。

商用系统其实多半都有一期一期的观念,所以可能会做月结,或是做年结,也有人做日结,端赖这个系统的特性而定。其实结算的观念很简单。要进行结算时,要先确定这一期的交易都已经完成了,确定完没有办法再插入新的这一期的资料以后,接着就是把上一期的余额,加上这一期的变化,就会得到这一期的余额。这就是结算。

当然,还有人可能会在结算的过程里面,产生一些summary table,以便制作报表,或是特别产生一些自动化的交易记录。不过基本上,就是要把这一期的余额算出来。

结算有多重要呢?其实结算就代表了一个时期的终止。一旦结了以后,就没有办法再补那段时间的交易资料。接着把所有的余额档重新计算一次,以避免程式写错了(像是race condition,单纯的bug…),造成余额档的错误。

要是没有结帐的动作,你的系统就不会有一个经过确认过的余额档。任何时候要知道余额,最准的方式就会是从盘古开天辟地以来,所有的数字加起来才会最准。如果有了结帐的动作,就不用这么麻烦,只要计算从上次结算时,到现在的变化量到底有多少,加起来就可以了。

此外,宣告一个阶段的结束,这也是很重要的。系统需要把哪些想要改旧资料的需求拦下来。如果你的财务报表都已经产生出来,公布给投资大众了,这时候还想要改旧资料,可能就太晚了吧。


练习题
※※※※※※※※※※※※※※※※※※※※※※※※※※

做系统应该要有的sense是,如果每个人都可以查到他自己的资料,那么他的老板一定会想知道整个部门的统计资料。所以你可以思考一下,如果要设计部门别请假时数统计资料档的话,该怎么做?这里可能会牵涉到的问题是,员工会在部门间轮调或离职,部门可能会改组。除此之外,有什么requirement上的潜在问题,会需要跟user确认呢?


结语
※※※※※※※※※※※※※※※※※※※※※※※※※※

因为工作上的需要,我interview过不少想当SA的人。很多小公司的SA,其实就是负责design database 的table schema,并且说明每支程式应该要负责对data进行什么处理。如此而已。

很多懂得OOAD的人都会嘲笑这些人的技术落后,觉得这些人的功力不过尔尔。不过在我看来,不过就是大家用不同的角度,来看应该要怎么分析设计一套系统。没有孰优孰劣,只有谁真正比较清楚并且完整地capture user requirement如此而已。

大多数用到database的商用系统,其实在进行modeling时,最重要的重点,其实就是data modeling。对于系统的behavior,其实并不是那么在乎,那也就是为什么我会不断思考,UML, use case driven OOAD是否真的适用于这种类型的专案开发的原因。或许没听过structure analysis的人,在读完这一章以后,可以拥有一个不同的观点吧。


后记
※※※※※※※※※※※※※※※※※※※※※※※※※※

很多人常常问我,我为什么不把自己对于软体开发该怎么做的想法分享出来?我其实也想。可是写这种东西,又枯燥乏味,又缺乏乐趣,实在是有违我自己的天性。时时刻刻还要担心如果写错字了,或是引喻失当,会不会有损我一世英名。

嗯,这种又辛苦又不好玩的事情,下回不打算做了。这个学术探讨的系列搞不好就会到此为止。我还是写写游戏文章好了。

如果你发现了我的文章有什么错误,(因为我通常是睡眼惺忪地写,所以这是很有可能发生的),还是有什么经验想要分享的。请写信到singlelog@yahoo.com.tw给我。不过我的信箱很小,不要把我灌爆啊。


阅读(174) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册