Data Oriented Analysis & Design

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

偶然的機會裡,看到了呆伯特法則,除了常常捧腹大笑以外,總覺得自
己在軟體界待過以後,看到的現象,其實跟書上寫的還真的差不了太多
。唯一的例外是我的老闆們每個都有其獨到之處,很難跟書上寫的主管
一般愚蠢。各位英明的老闆們,看到這句話,我是不是該加薪了呢?
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。

<EX>如果一張單子的編號是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)


還是舉個例子好了。

<Ex>員工編號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給我。不過我的信箱很小,不要把我灌
爆啊。

(全文完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值