2007年8月31日 星期五

D20 令人感到麻煩的設計(5)──Abstract Class的設計

同樣的程式碼抽取到方法讓大家共用,同樣的方法抽取到共同的父類別來使用,這是設計中對於程式碼的reuse。
這是是一個簡單的繼承架構,ClassInterface下有一個DefaultClass作為基本實作類別,另外衍生了三個子類別。理論上DefaultClass應該會做出介面裡定義的全部動作,子類別在方法裡有不同的動作時再override(覆寫)父類別的的方法。

有的時候介面的方法很明顯地是該由各個子類別來決定如何實作,這個時候父類別就不可能有可以共用的方法實作。例如一個交通工具介面裡有move()方法,因為每種交通工具移動的方法不同,所以實作介面的基本類別,move()方法就應該保持為abstract等待子類別來實作。

會有人說:先隨便實作一個方法再由子類別override不是也可以嗎?設計時不該讓物件進行沒有必要的動作,不僅會令物件進行沒有定義的行為,同時不必要的override也讓程式更難追蹤。Abstract Class是無法產生實體的對應,只是對於物件的定義而不會變成物件;在意義上就像生物學裡的分類,界門綱目科屬種各定義了不同所屬物種的特徵,到最後的分類時(種)才有所有符合所有特徵定義的生物。

交通工具類別已經宣告move()為abstract,但是直接把汽車、輪船、飛機等直接繼承交通工具也會有問題,因為子類別裡還是會有多個類別有同樣動作的情形,一有改變就可能牽動到所有子類別。理想的設計是再衍成對應的分類類別,子類別再去繼承分類類別。一組類別要設計成這樣時比起快速地直接使用一個類別,當然會多花很多的時間,不過應做的工作還是該去做好的。

2007年8月30日 星期四

D19 令人感到麻煩的設計(4)──物件關係的影響

在B21裡提到了物件的三個關聯:is、has、use,其中關係最緊密的是is,其次是has、最鬆散的是use。

左圖的專案物件目前繼承CompClass2,在宣告的同時專案物件就永遠是一個CompClass2而不可能會變成其他種類,動作的時候就只能依CompClass2的規範。右圖的專案物件使用一個變數來記住內含的CompClassInterface,可以依需要操作設定的CompClassInterface實作(注意:三種CompClass都可以設定),這就增加了專案物件使用的彈性(Strategy Design Pattern)。

右圖的專案物件本身可以使用前一篇的方法繼承另一個Class,加上混用Wrapper Design Pattern讓專案物件同時實作CompClassInterface,並將實作方法全部直接呼叫strategy設定的CompClass的話,我們就可以擁有身具兩種CompClass的專案元件應用在更多的地方。(必要時還可以增加其他的strategy來擴充功能)

use是在呼叫時傳入物件,物件只在方法中使用而不另外記憶下來;在服務性質的API可以大量使用這種方式。呼叫Web Service時使用的就是這種物件關係。

2007年8月29日 星期三

D18 令人感到麻煩的設計(3)──吸收改變的避震器


上面這張圖的狀況是專案物件會廣泛地使用一個底層API。一開始我們很直覺地使用左圖裡的方式直接呼叫BasicAPI的方法,而且沒有遇到什麼問題;後來我們發現有些連續呼叫數個BasicAPI的方法在專案裡很常發生,需要抽出另外成為固定可用的API,於是我們建立了一個BasicAPIExt1,在遇到需要呼叫特定連續方法時使用新的BasicAPIExt1,其他情況則直接呼叫BasicAPI。

中間的圖我們可以看出,專案物件有些呼叫BasicAPI,有些則呼叫BasicAPIExt1,不僅層次的使用比較雜亂,每個專案物件有使用關聯的物件也變多了。設計該是具有層次與抽出共用部分簡化關係的,所以會用右圖的設計,讓BasicAPIExt1繼承BasicAPI來達到這些目的;而且未來BasicAPI的某些改變只會影響到BasicAPIExt1而不會影響到專案物件。

這個範例應用的範圍不只是API,應該包括專案所有使用(static method)或繼承關係的設計範圍。如果能夠費心把專案與元件之間的使用關係都加上一個類似避震器用途的類別(即使只是沿用也要加)的話,將會更輕易應付突然發生的改變。

2007年8月28日 星期二

D17 理論串接實作的瓶頸(9)──專案思維與元件思維

從系統的角度來看,需要呈現給使用者看到的是View、Model、顯示狀態的Message與處理中記錄的Log,會從系統外部來的有Properties與Model。元件如果像縮小的系統,是不是應該也要考慮這些與輸出入有關的物件呢?

我的答案是否定的。在前面我所切分的元件層次裡,主要有功能與功能的Controller、傳入的Properties、再加上一個Implementation用Factory處理功能的切換與用Wrapper把傳入的基本Model轉變為Component Model。View、Message與Log呢?它們只允許存在於系統?

元件應該是一個獨立的單位,它最重要的工作是完成我們需要的功能,並在無法處理時回報狀況與資訊。View的作用在於執行前輸入資料並於執行後顯示結果,這些資料其實都屬於Model的內容,元件只要能處理Model就可以工作;如果把View寫進元件裡頭,要是下一個系統使用的View不相容時應該又會是一樁慘劇。基於這個想法,元件也不應拋出提示的Dialog,我們可以規定功能的完整執行要進行三次呼叫,由上層的系統來控制每一次呼叫之中要做哪些提示動作。

Log的問題也很類似,把這個系統使用的Log物件傳入元件裡記錄,要是下次用的是另一個種類呢?Message則會牽涉內容的客製化與多國語言的問題,同樣不應該進入到元件裡面處理。每個元件的輸入Properties名稱與執行後的Message內容都應該設定成唯一存在的id,參數與資訊都使用代號後就可以在系統層級儘情地客製化為喜歡的樣子;當然,元件本身應該提供一個預設範本供系統直接使用或參考修改。

2007年8月27日 星期一

D16 Copy-Paste絕對不是Reuse

許多人在面對需要新增一項功能時,如果發現已經存在一個類似的時,心裡想到的必然是重覆使用已存在的,但是需求上有些微的差異必須實作,可是又擔心修改原有的會造成side effect。這時通常祭出的是copy-paste大法,快速“生出”另外一組程式碼,再隨心所欲的修改內容。

以功能需求來說,這樣做絕對是滿足功能的最快方式,同時也可以保證功能的測試很快完成。但是,即使用的是copy-paste,對於系統來說“全部”都是新增的程式碼,根據新增程式碼的作業流程所應該製作的文件、測試與追溯一個都不能少;雖然連這些都可以複製原來的,但是這樣的行為一多,整個系統類似的物件就會以等比級數來成長。

寫程式時若有方法需要擴充功能,大家都知道在同樣的方法裡增加參數讓它可以處理更多樣的情況;功能上的想法也應該是如此,在元件裡加上符合新增需求的方法或參數,努力想辦法讓元件支援更多種的功能。在分析與設計之後,動到的程式碼與copy-paste相差無多,但是只要修改原有文件而不用再“生出”一堆其他的文件。

copy-paste出來的產物,只能應付特定範圍內的“某些指定功能”;經由設計層次定位的元件,卻能夠應付特定範圍內的“所有功能”。在未來開發的時候遇到同樣特定範圍的功能時,前者還需要列出所有元件以供挑選哪一個適合,後者則直接掛上一個元件就滿足。同樣的道理,前者在library裡必須記錄全部的Component,後者就只有一個Component。

copy-paste到底是好是壞?想讓自己產出的元件是什麼樣子?你的決定將會影響到後人對自己的評價。

2007年8月26日 星期日

D15 令人感到麻煩的設計(2)──所有水平層次的設計

在設計每一個層次時,我們同樣也可能會遇到其他人送來的設計圖長得類似下圖。他想表達的是三個不同的元件各有其定義的介面與實作的入口類別。



不管我們怎麼決定設計,其實該有的東西都有的話系統應該都能正常做出來。但設計的目的除了讓每個物件各司其職之外,另一個重要的目的是要去蕪存菁,把重覆的程式碼集中到上一層的類別裡。上面的設計因為實作直接對應介面而沒有任何父類別,因而造成每個實作類別都得存放所有的程式碼在裡頭;那麼上一篇提到的介面基本方法就會到處都是。

理想的設計,還是必須多花一點功夫設計一個BasicCompInterface與BasicCompImpl來收集通用的屬性與方法,實際使用的元件繼承他們之後再撰寫自己的介面與實作。如果元件衍生時有因專案類型而適用在不同行業時,最好針對每一種專有的行業再設計一組對應的介面與實作,如此才能把不同的屬性與方法放置在他們應該存在的層級,而且可以快速篩選出適用特定行業的元件。

2007年8月25日 星期六

D14 理想Component介面的基本

思考到現在,心裡理想的Component介面的基本逐漸地成型。除了本身功能提供的方法之外,我認為要有以下幾種基本的通用方法:

public void setProperties(Properties properties)
public Properties getProperties()
設定參數集合與取得現有參數集合的方法,要不要有存取單一參數的方法則視需求而定。不過既然可以拿到集合,再從中取得指定的都不是問題。提供這組參數存取方法之外,也需要有文件列示所有的參數名稱與其影響。

public ComponentModelInterface getComponentModelInterface(BasicModelInterface basicModel)
Component傳入的Model是這個元件專用的。基於軟體工廠的想法,所以提供一個方法把基本Model傳入後會在裡面自動Wrapper成Component Model,接下來就可以任意地傳入Component裡來執行。

public List getAllMessageIDs()
public String getMessage(String messageID)
Component的訊息的內容應該由文件交待全部有哪些,或者有範例檔提供全部的訊息代號與預設的訊息內容供系統複製後修改。如果不想花時間另做文件的話,可以提供像這樣的API取得所有的訊息代號與其內容再存放到系統也不錯。

ComponentException
例外要使用Component自己專用的,每個具有無法處理或回復狀況的方法都應該定義成拋出Component Exception。

2007年8月24日 星期五

D13 令人感到麻煩的設計(1)──Data Model的設計

直覺地把系統的動作寫成程式是最快的開發方式,在執行上也很可能比較快。設計雖然也同樣地把同樣的系統動作寫為程式,但是多了很多與處理層次有關的類別,不僅在開發上相對緩慢,執行上也會因為呼叫的路徑變長而變慢些。


想像一下,我們是印表機延伸工具的開發廠商。依前面所談的,公司已經準備好Base Model作為基本資料類別,現在第一版系統要支援三款印表機。在一開始的Data Model設計有人提出了像上圖的方案,身為Leader的你應該通過這個Data Model設計嗎?

這個設計基本上當然可以讓系統正常執行(就算只用Basic Model也可以,但那會一團亂),然而設計的目標是讓系統的程式分門別類地形成層次。這裡的問題在於印表機延伸工具的系統,會定義出一些自己專用的資料與存取方法,以這個設計來看,那些專用的方法都必須實作在三組Data Model裡,不管是個別使用的或者是三種用法都相同的。

如果我們曉得要把重覆使用的程式區段抽取為方法來使用是正確的方式,那麼把重覆使用的方法再抽取到一個類別來reuse也是應該做的事情。雖然設計因此多加了一個層次,但是每個物件各司其職不作重覆的事,是OOAD所要表達的一個重要精神。

2007年8月23日 星期四

D12 IF介面像插座THAN Data Model是電力規格

在C07曾用機器人的可置換手臂來說明介面的用途,不過目前Interface最常比喻為插頭;不管各式各樣的電器,只要插頭符合插座的介面就可以插上去使用。現在世界上有四種最常見的插座規格,即使是種類不同的插頭,加上所謂的轉換器(Adapter Design Pattern)後還是可以插進去使用。

這是以可見的外觀來說明介面的用途,其實再深一層來說,插頭插上後還有在線路裡流動的電力;唯有提供電壓、電流的種類完全吻合的插座,才能夠正常地驅動電器。以元件的觀點來看,除了介面必須吻合之外,在介面上傳輸的Data Model也必須要能溝通才能動作。

外在的介面定義與內在的處理資料類型,這正是元件封裝之後留存給外界使用的規格。有時設計者為了偷懶,外部看得到的介面與資料定義得很詳細,封裝後看不到的內部就設計為寫死的黑箱,使用的時候因為功能都相同而沒任何感覺,可是一旦有問題或是需要改變,就讓接手的人痛苦萬分。這樣的行為,是不是“圖一己之便利,留後患給旁人”呢?

2007年8月22日 星期三

D11 Component的設計(5)──Exception & Properties

Component裡既然有自己的控制邏輯,那麼期待產生元件設計之初沒有預期到的狀況也是應該的。預期中的狀況可以在Component Controller裡判斷並控制,預期之外或無法處理的狀況就應該拋出Exception告知更上層的控制以便因應。

拋出的Exception應該是這個Component專有的,如此一來可以在層層呼叫的使用時,立即判斷出是在哪個Component裡出現的狀況,進而取得Exception裡的資訊作對應的處理。我們不該傳回基本型態的Exception,也不應與其他Component共用Exception,因為會搞不清楚到底是誰拋出同時也無法取到任何更進一步的資訊,尤其在共用Exception時更會加重使用的關聯。

Component的參數有些人會設計為Component特有的定義檔,在生成時讀取內容並設定。這種方式在設計上很直覺,但衍生的問題是每個Compoent都會擁有至少一個設定檔,當系統擁有上百個有設定檔的Component後,使用者若要調整一些設定就得逐一找到存放的地方打開編輯並儲存。

理想的作法是將所有Component的改變參數都設計傳入類似Properties的集合物件,每個Component的每個參數都擁有唯一的名稱作識別,使用只要建構時傳入或呼叫設定的方法就可以改變Component的設定。所有Component的Properties存放就經由系統統一管理,在需要設定Component時從集中管理的地方找到對應的Properties並傳入即可。

把Component的設定集中到系統管理的最大好處,其實是我們可以針對整個系統的可調整參數設計一個方便使用者編輯參數內容的編輯程式,就類似Windows的控制台的設計概念。

2007年8月21日 星期二

D10 理論串接實作的瓶頸(8)──實現軟體工廠的機會

前面提過所有功能的目的都在於處理資料,以封裝Component的概念來說,唯一的輸入與輸出物件就是Component Model。有很多人說過軟體工廠是不易實現的理想,假使工廠不管生產什麼樣的元件都是處理基本型態相同的Component Model,這樣一來是不是有機會實現呢?

上一篇提到在Model本質不同時有三種資料轉換的方式,每一種都需要另外撰寫程式才能讓資料在不同模組之間轉換:第一種必須寫存取資料的實作,第二種必須撰寫Wrapper程式,第三種則必須寫搬動資料的動作。如果,我是說如果,一間公司裡所有Component Model與Data Model的根本都是相同性質的基本資料類別(比如說是Map),每個Component的方法都附有一個方法將傳入的基本資料類別,包裝為Component Model後傳回再傳入Component處理,處理時對基本資料類別的所有操作都可以自動反應回系統。

2006年開發系統時加上了這個Common Model的想法,向下對應數種不同型態檔案讀回的內容,實際把檔案讀入後變為Common Model,實作時都是操作Common Model,使用者根據檔案類型呼叫對應的Parser後都是Common Model。Common Model再往上可分支實作出不同類型的Data Model給不同的系統使用,但是底層的處理都是通用的。利用這個概念應用在Model與Controller上,我讓兩個有點類似的系統共用了相同的處理核心,使得Use Case的再使用率達到70%以上。(註)

開發第二個系統的快速(花費人力大約佔第一個系統的五分之一)讓我對於軟體工廠的思維有相當大的信心。往後的專案只要先定義好公司使用的基本資料類別,我相信軟體元件化的理想是可以落實的。

註:開發的兩個系統是可以同時使用的,使用者可以自由切換到任一個系統執行任一個功能而不需重新啟動程式。

2007年8月20日 星期一

D09 Component的設計(4)──Model

與系統一樣Component應該有自己的Model。理由很簡單,如果Component操作時使用的Model屬於別的元件,那麼Component間會有使用關聯;如果Component使用的Model是簡單型的集合物件,又會因為加上基本存取動作而使得程式碼變得較難讀懂。

Component的定義與Class一樣需要高聚合與低耦合的封裝,為了減低與其他Component的關聯,定義出屬於自己的Model是勢在必行的。在設計時根據Component Controller的使用決定Model提供的方法並定義在ComponentModelInterface裡,即使是其他地方使用的Model只要實作這個Component指定的ComponentModelInterface就允許傳進來使用。

Component同時應該準備好ComponentModelInterface的基本實作給無法提供Model實作的程式使用,有些設計者會將許多Component集中使用同一種實作的Model而沒有定義ComponentModelInterface,這樣使得Component與Model緊密結合而無法改變使用其他實作的Model。

傳入Component的Model大致上有三類作法:第一種是直接讓系統使用的Data Model實作Component定義的Model Interface;第二種是使用Wrapper Design Pattern在生成ComponentModel的實作時把系統Data Mdeol包裝在裡頭;第三種則是設計一個轉換類別,在使用Component之前把Data Model的內容搬到ComponentModel並在使用後搬回來。

2007年8月19日 星期日

D08 Component的設計(3)──Controller & Action之二

Component的設計與系統一樣,方法進入後會由負責執行流程的Controller來操作實際的動作來完成功能,而Controller流程裡分解動作的實作都放置在Action。控制不變而動作有變時就更換不同的Action,控制改變而動作不變時就更換不同的Controller,兩者都改變時就把Controller與Action都更換掉。

經由這樣的設計,我們可以達成在Component不變的情況下,只要輸入不同的參數就能夠改變Component內部的運作。另一個好處是,當Controller與Action有新增的狀況時,改變都封裝在Component裡而不影響系統;如果在系統上使用Factory來切換元件的話,改變就必須由系統層面來處理。

元件的Controller裡如果使用了其他的元件而且會拋出其他元件的Exception的話,務必記得要在這裡捕捉下來並轉成自己的Exception丟出去。我們應該讓下層的動作只傳到自己這一層,上一層只會收到這層丟出的東西;否則下面所有的物件全都往上丟,就會造成上一層與下一層、下下一層、下下下一層……的所有元件都產生使用的關聯,這是設計時絕對要避免的。

2007年8月18日 星期六

D07 Component的設計(2)──Controller & Action之一

先用我經手過的印表模組的例子說明Controller與Action的差別。

在Interface上定義了print(String data),只要把列印字串傳進Component就會在印表機印出資料。但是印表機的實際列印最多會是由進紙、列印、退紙三個動作組合的,根據印表機的不同又有列印前是否自動進紙與列印後是否自動退紙的些微差異。

根據上面的描述,應該把印表機的動作(進紙、列印、退紙)定義為三個ComponentActionInterface的方法並加以實作,再於上一層ComponentController做一個對應印表機的實作,在print(String data)依印表機特性決定如何呼叫ComponentActionInterface。最後在Implementation裡做好變換的機制,依實際連接的印表機生成對應的Controller與Action就能夠正常使用。

千萬不要把Controller寫成與印表機一對一的對應,印表機可是有數百種機型的。以列印動作來講,列印的動作是絕對要執行到的,其間的差異就只有是否自動進紙與是否自動退紙,總計四種組合。只要設定印表機機型對應的控制方式,不就可以省卻非常多的控制程式碼了嗎?

2007年8月17日 星期五

D06 Component的設計(1)──Implementation


這張圖是我所認定一個元件應具有的完整層次。Component內部的設計同樣應用MVC的想法來決定它的層次。由於Component不需要View,而且處理的Model較為單純,所以切割的層次同樣地比較簡單。

一個完整的元件就等同於一個JDK的Package,應該要有屬於自己的定義Interface、實作Class、處理的Model與拋出的Exception,經由封裝這些種類的物件在Component裡,我們讓Component成為完全與其他Component絕對無關的獨立物件,在使用與抽換上可以不需去擔心它的關連而直接使用。

Interface只是定義而沒有任何程式,因此在進入Component的一開始,我們就需要一個Class的存在來實作Interface裡的定義,Implementation的工作便是如此。我把Implementation定義為Component內部Factory Pattern的實作,經由傳入的參數自動決定接下來應該由哪些程式來實作,如此可以減少系統的設計負擔。如果這個Component的使用必定要經過Factory的方式,我們可以把每次都需要的部分封裝起來。

Implementation的作用只是Component的入口,我們設計時可以簡單讓Component Controller去實作Component Interface,那麼Implementation的責任就可以簡單到只是以Factory的方式叫起適當的實作,然後把每個介面的方法對應到Controller的方法而已。

2007年8月16日 星期四

D05 做事的方法(7)──組織的窗口

各位應該都到戶政機關去申請資料過,現在的政府機關都設置了服務櫃台,民眾只要攜帶必要的資料到櫃台就有服務人員處理好,並提供民眾想拿到的資料。

在這個故事裡,民眾的角色是Controller,政府機關是Component,服務櫃台則是Component Interface。政府機關對民眾來說是完全封閉的黑盒子,民眾想要存取政府機關的任何資料只能透過服務櫃台。櫃台那裡定義好政府機關所有提供的服務事項(Interface methods),同時也定義了申請每一個服務事項時民眾應事先準備好的資料(傳入的物件)以及辦理後可以得到的文件或戶籍狀態的改變(執行的結果)。

政府機關將自己所有的功能封裝起來不讓任何民眾存取,只允許經由窗口來操作,這也是Component最重要的精神。應沒有人看過民眾去政府機關辦事時,服務人員會對民眾說:“你把這個資料放在那張桌子上,再走到盡頭的櫃子打開第三個抽屜就有你的資料”這樣的事吧?Component的內容不應給外部程式直接存取,否則會造成管理上的重大問題。

讓一個組織與外界所有組織完全隔離,只留下規定的窗口進行事先定義好的工作,這是讓工作易於管理的根本。

2007年8月15日 星期三

D04 Component的需求規格與設計

架構設計完成後,可以得到所有新增元件的清單與每個元件應有的屬性與方法,這些便是對Component的功能需求。

我們把視野縮小到Component,可以感覺它就像一個縮小的系統那樣有限定的範圍,Interface的設定就等同於對於Component的功能需求。有人認為只要定義好Interface,Component要怎麼設計就不需要管,只要符合規格並通過測試就好;這個想法在系統層級時已經討論過,在這裡基於“每一個程式的存在都有它的意義“的想法,Component與系統一樣都是需要設計的。

將Component視為積木並沒有什麼不對,在有不同需要的時候整個置換成另一個。但是我們已經知道在前面提到的印表機案例裡,整個抽換的方式會造成的幾個問題:傳入的Model只符合特定實作、Controller與動作無法分割而有過多重覆的程式碼、支援新的實作時只能copy-paste再修改。

Component的reuse是提升系統開發效率其中的一項指標,但我不認為reuse是整個Component拔掉再換一個,而是使用同一個Component並使用不同的傳入來改變內部的行為。就像一塊具有螺旋漿的積木,使用者可以用手撥動令它旋轉,而不是轉到不同方向時就拿另一塊積木來取代。設計時儘量將變動封裝在Component內部,因應系統需求而需要更換不同Component的設計則在系統上實現。

選用適當的元件用適當的方式快速堆疊出整個系統是我們對軟體工廠的期待;介面的存在提供了如同積木般連接的規格,但是Component並不是只是用積木的方式接起來就可以,在介面上傳遞的資料才是它的精神。

2007年8月14日 星期二

D03 你會是什麼樣的神?

曾有人比喻說,設計系統的感覺有如創造世界的神一般,可以全憑個人的想法決定這個世界的模樣。現在想像你真的是一個萬能的神,第一個任務是任意佈置四尺魚缸,最低需求是要能有20條熱帶魚活在裡面。手邊有許多種類的水草、裝飾與魚類可以任意取用,這時你會怎麼決定這個水中世界呢?

“系統是滿足客戶需求的最小設計。“如果你的心裡響起了這句話,那麼這個魚缸裡就只會有20條顏色亂配的魚與產生足夠氧氣的雜亂水草被丟在裡面。用心的人會去規劃要有哪些種類、哪些顏色的魚,也會選用適當的細砂與裝飾,另外還會佈置水草的層次,說不定還會另外放入一些貝殼小蝦之類原先不在需求裡的項目。一切多出的投入都只為了讓這個魚缸的外觀與運作更加美好。

創造的世界要更好必然會有更多的工作項目必須去完成,每一個工作項目無可避免地需要時間與資源投入。特別做出的項目未必其他人察覺得到,但是那些項目卻經過計算而使得那個世界變得更不易出問題且好用。不管是神還是人,應該都會為自己即將做的一切想好未來所有的可能吧?

同樣的想法也對應到系統的設計上:你會把程式井井有條地規畫好並使之正常運作,甚至提供更多樣化的選項功能,還是把全部程式塞進一個大黑箱裡只負責功能正常就好?這正是你的抉擇!

2007年8月13日 星期一

D02 垂直設計有兩層──需求邏輯與動作邏輯

需求的收集是針對使用者對於系統的要求,這些都是使用者在商業領域裡的要求;作架構設計的目的是根據使用者的功能需求,去分析並設計出符合功能需求的操作流程與結果的對應程式。當然,架構設計時可以決定所有的程式都在這個層次實作,但是這樣一來所有的程式碼都與這次系統的需求綁在一起,未來需要拆離時又會是一件浩大的工程。

抽取共用的部分是設計者都有的習慣,從最底層開始是把共用程式碼抽出成共用API,同時將一些特定意義的動作封裝成元件,元件再經由系統的設計使用在Use Case裡,最後所有的Use Case集合成為系統。在系統每一小格裡的程式,在使用的層面上同樣有很多層次需要設計。

對一個公司而言,元件不是針對特定系統設計而應該是通用型態的,平時應受到管理並且可以很快地查詢到所有元件的定義。在設計系統的時候根據功能需求的內容挑選適用的元件,如果應該屬於元件的動作而找不到的話,就應該依照通用原則定義元件的Interface並在這個階段作細部設計。

無論是元件還是API,如果只是對應到系統的商業領域而沒有辦法做成其他系統通用的時候,他們應該存在於通用元件庫之上的系統元件庫,未來設計要應用在這個商業領域的系統都可以優先在這裡尋找看有沒有更適用的元件。

這個概念的垂直架構大略如圖所示,最上面的專案是之前架構設計階段的產出,使用時上層可使用下方任一層裡的元件或API,下層則不得使用上面層次的物件以免造成迴圈式的關聯。

2007年8月12日 星期日

D01 功能為經,層次是緯──定位系統的所有程式

對於系統而言,功能需求是一定要完成的事項。若把系統視為一個長方形的圖形,我們可以想像所有Use Case列示在左邊,再將長方形依Use Case的數量水平等分,每一個小的長方形就約略等於完成Use Case的程式。如果只把完成Use Case視為工作目標,那麼每一個小長方形都是密閉的黑箱,裡頭設計的品質無從得知,因而亳無取代性可言。
在架構設計時,我們規定了MVC等等的層次,要求每個Use Case都必須經由特定的層次做特定的事來完成功能。把層次依序放到系統長方形的上方,並將長方形垂直等分,我們可以看到系統就像豆腐一樣被切得井井有條。
再來在幾個重要的層次之間,甚至設計時認為同一層次中應該再切分數個合作的層次的地方,加上Façade Pattern,就會成為下面的圖。集合為Façade的好處很類似定義Interface,使得進入下一個層次時必定得經過這道關卡,如此一來無論在硬體上或模組上都使得切割變得容易許多。
最後,我們設計的所有程式都會落在系統長方形的任何一個小格裡,而且也應該在小格裡。到此,所有的程式與它內部使用的其他程式都有自己放置的地方與應盡的責任,加上每個小格裡對外的地方都設計了Interface,被封裝後的小格同樣可以快速地抽換規格相同但功能有差異的其他程式。

2007年8月11日 星期六

C30 架構設計的流程(Model)

在需求階段裡,藉由訪談客戶獲取系統需求加以分析並記錄。記錄下來的物件關係模型如下面附圖。圖中物件間的關係如下:與A19的基本概念模型比較,會發現他們的本質上是相同的。

◎一個Use Case Flow由一個或一個以上的Service組合而成
◎每個Service有一個或一個以上的Properties與Input
◎每個Service有可能傳回Return Code(可能沒有)
◎每個Component都應該定義無法處理的Exception狀況(可能沒有)
◎每個例外狀況都要有一個或一個以上以上可能的其他Service
◎一個其他Service由一個或一個以上的Component組合而成註:這裡的Component也有可能是一段程式碼。


每個Use Case Realization在設計時都有Class Diagram與Sequence Diagram,設計的同時每個Class都要定義屬性與方法,同時附上註解。Class會存在於Package,所以每個Package也會有一張Class Diagram來描述有哪些Class。

追溯關係則至少有三種:Use Case與系統Class、系統Class彼此間、系統Class與元件Class的使用關係。此外,訊息與參數的內容應有清單與影響的關係追溯。

2007年8月10日 星期五

C29 理論串接實作的瓶頸(7)──Use Case vs. Class & Class vs. Class的追溯

很多人在一聽到要做這兩層的追溯關係時頭都很痛,因為一個系統動不動就幾百個甚至上千個類別,Use Case也差不多都近百個起跳。光是Use Case對Class就已經是一百對一千的表格了,更何況Class對Class的一千對一千大型表格呢?而且,所有的關連都建立在程式碼裡頭。

如果有這樣的想法,那很肯定這些人是沒有設計層次概念的。雖然實際使用的類別有一千個,但是經由層次的定義,我們可以劃分哪些屬於系統架構設計層次的類別,哪些屬於更下層元件層次的類別。如此一來,Use Case要找的是與架構設計類別的垂直關係(A),架構設計再找出與元件類別的垂直關係(B),另外再加上架構設計類別裡彼此的水平使用關係(C)而已。

A與B的關聯在建立Use Case Realization與繪製裡面的圖表時已經將之設定在rose model裡,經由SoDA可以快速找到。雖然C的關聯也同時被放在圖表裡,但是我們可以經由Package名稱的篩選區隔出系統與元件的區別。系統設計的類別會放在與系統相關的Package裡,元件則基於全公司共用的規則放在不同等級的Package之下,由這個差異我們可以定出類別所屬的不同層級。

應用層次的落差使用較多層次的對應關係來達到像粽子一樣,提起Use Case後可以往下找出所有有關的類別是這裡的關鍵;這樣一來就可以免除開頭所說的那種勾選超大型表格的惡夢。

2007年8月9日 星期四

C28 追溯關係(2)──Use Case vs. Class & Class vs. Class

每個Use Case的功能都是由一群Class合作完成的,這表示著Use Case與Class之間存在著使用的關聯;也就是說在這裡同樣得做好使用關係的追溯。反過來看時,Class參與了哪些Use Case的完成也是必須追溯的關聯。

在架構設計的時候,產生的物件都屬於前面架構各層級的設計,對應的是需求分析的結果,除了使用元件的Interface之外,其他的Class都是與系統直接相關的物件。對於分層設計的概念來說,元件與Service是屬於不同層級的物件,所以不建議追溯時混在一起。

既然設計分了層次,追溯就無可避免地也分對應的層次。在Use Case對Class這層的關係就對應到與系統需求有關的Class;在更為精細的追溯時,追溯的對象會細分到Class裡的變數與方法,這樣可以在修正與變更時能夠更詳細地定位哪些地方需要重新測試。

在SoDA的應用上,我們可以取得所有的Use Case Realization,進而拿到裡頭所有的Class Diagram與Sequence Diagram,再依序條列出圖裡所用到的全部Class,很容易地就可以取得每一個Use Case有關聯的所有Class。

再下一層是系統Class對應使用元件Class的追溯關係。在這裡我們必須要能知道每個系統Class使用了哪些元件Class,同樣地也應該知道元件被應用在哪些系統程式裡。表面上雖然是平等的Class vs. Class,但是在設計層次上其實是上層系統程式與下層使用元件的關聯。

2007年8月8日 星期三

C27 設計元件的放置架構記錄

即使是在同一個Use Case裡,在不同的Tier裡都至少會經由一個以上的Service來控制當時系統應有的動作。在我的設計裡,Component是只能被Service所呼叫的元件,所以在設計Service物件的同時要記錄在哪一個Tier各使用了哪些Component。

記錄的意義同樣在分析關聯,我們可以看出Component被哪些Use Case的哪一段Service所使用。從Tier的觀點來看,每一個Tier都知道總共使用了哪些Component,同時我們知道每個Component各屬於哪一個library,收集起來並加上系統本身在那個Tier應有的程式,得到的就是在那部電腦上需要額外安裝的檔案。

在這個時候,應該產出一張表格註明每一部硬體的規格,使用的作業系統,安裝的應用程式,使用的Component Library與系統預計產出的程式。未來安裝各部分的硬體時,就依照這個表格來製作安裝手冊並安裝之;日後有人問任何硬體所需要的規格與軟體時就可以立即提供。

不過還是要記得,有任何的變更時,這份文件也是必須要同步更新才能隨時得到最正確的內容。




◎系統的目標

2007年8月7日 星期二

C26 Activity Diagram設計產出(2)──Class Diagram

每個Use Case Realization裡也該有Class Diagram,在Sequence Diagram裡為Interface與Class拉上關聯的同時,也該把那個Interface與Class放進這張圖裡。從Class Diagram裡我們可以很快地得知每個Use Case使用的所有Interface、Class與彼此間的關聯。

如果可能的話,努力將每一個Tier(每個Tier對應到一部電腦)裡所用到的類別都作成一張Class Diagram以顯示其架構,同時把Interface、Class之間的繼承與使用關係都放置在圖表裡。

此時的Class Diagram是一部電腦裡必須要有的所有元件介面,在所有Interface都已經定義、實作Class設計好大概並決定好所屬的Package後(方法還可以增減或變更,元件名稱與位置若有增減或改變就得相對地修正),接下來還要考慮哪些意義相似的Package要被包裹成一包library。切分library的基本想法留待C27再作說明。

以靜態的角度來看,最小的單位是Interface與實作的Class,這個物件可在Class裡描述其繼承關係、說明與所屬的所有方法,如果是Service層級者則拉出一張圖來表示它所有的關聯;Interface與Class在建立時就放置在其所屬的Package裡。再往上的單位是Package,由於Rose Model裡可以取得Package裡所有的物件,所以可由SoDA產生Package裡的物件清單而不見要有圖;設計時如果把Package的範圍等同於Component的範圍就可以省卻一張圖。

要有哪些Class Diagram除了決定什麼情況下該有之外,同時也根據什麼地方需要特別說明就多加一張圖註明。

[這裡應該有一張Class Diagram]

2007年8月6日 星期一

C25 Activity Diagram設計產出(1)──Sequence Diagram

在Use Case Realization裡首先要做的是根據Activity Diagram從功能的開始,逐步敲定每個步驟如何在系統裡實作:判斷是否可以進入功能的處理、處理時一層層地決定現在到達的層次、在該層次裡由哪些元件做事、經由哪些元件的方法、應該傳遞些什麼等等。這些Use Case Realization的想法記錄在UML裡,是靜態的Class Diagram與動態的Sequence Diagram。

對於每個Use Case Realization首先要建立屬於它的Sequence Diagram,根據Activity Diagram裡的Activity,逐步在這兩張圖表加上應有的元素。設計一個動作時都有同樣的三件事情要做:決定由哪個元件來做(Component Name)、決定由元件的哪個方法做(Component Method)、決定做的同時要傳入什麼與傳回什麼(Method Parameters & Return Objects)。

如果找得到已經存在的元件與方法可以使用,只要將元件放進Sequence Diagram並拉上呼叫的方法關聯。如果沒有可使用的則必須依照所需的呼叫方法,看是要新增一個功能元件並加上對應方法,或是挑選一個已存在的功能元件只加上對應方法,新增之後再把元件放入並拉上關聯。

以上的設計依據之前分層的方式都定義在元件所屬的Interface上,同時為了方便日後的追溯最好在元件與方法的註解都註明是哪個Use Case所使用的。使用以上的原則,為Use Case裡所有的劇本建立起Sequence Diagram;依此類推直到所有Use Case Realization都處理完成。

[應該有一張 Sequence Diagram]

2007年8月5日 星期日

C24 理論串接實作的瓶頸(6)──Properties, Message, Log

在設計系統的時候,有幾個項目是貫穿全部功能都需要的,像是訊息、記錄以及參數等等。這些項目如果在設計的時候沒有周全的考慮,同樣會有使用時機與層次不一致的影響。

Message:在功能處理的流程裡,處理的經過與結果在必要的時機都需要回饋讓使用者知道。需要設計的有顯示訊息的機制與訊息對照表。系統可以設計一個自己專用的訊息機制,在設計的時最好以提供給其他系統使用為基準,作出通用的元件。

Log:使用時機與設計方式都與訊息雷同,是在處理經過中記錄下系統的必要內容。通常出現的時機與訊息差不多;訊息是該時間點讓使用者知道系統處在什麼狀況下與可以作什麼動作,記錄則是讓使用者能夠查詢在那個狀況下,系統內部的狀態如何,必要時可以追蹤問題的發生原因。

Properties:參數的設定應由系統統一管理,在初始化或是即將進入該段處理的時候使用參數決定處理的方式。常看到的設計是元件裡自行讀取自己用的參數檔案,以元件的立場來看封裝參數是可以理解的;但從系統的角度上來看過多的設定會讓系統的設定失去一致性。所以我會將參數的管理放在系統層級,控制元件的參數再使用Properties物件傳入。

開發系統的極致,是讓未來的開發或維護,在沒有改變設計邏輯情況下只要更改外部檔案的定義就能產生執行參數的效果,不需要再更動任何程式。通用設定如果存在於一致的層級,編輯與維護都會比較容易;更理想的是在提供的參數夠多讓系統更趨於成熟時,就可以提供外部參數檔的編輯器,甚至設計SA Tool之類的產品,在訪談時所作的記錄可以自動產生基本的框架與設計以節省大量重覆開發的時間。

2007年8月4日 星期六

C23 做事的方法(6)──察言觀色、見機行事

小朋友們與爸爸開心地互相逗弄著,家裡充滿了歡樂的笑聲;忽然間電話響了,老板打來跟爸爸說明天到公司辦資遣手續。掛掉電話後,小朋友繼續想跟爸爸玩,結果被爸爸大罵一頓,結束了快樂的時光。

小朋友們做的動作是前後一致的,為什麼會得到不同的反應?很明顯地是因為有一通電話對爸爸傳遞指令造成了改變,因而使他的心情變化進而對同樣的事件有不同的對待。小朋友像使用者,進行同樣的操作動作;電話就像是參數的改變,讓爸爸像系統的處理般與之前有所差異。

在生活裡有兩個方向可以考慮:當別人的反應模式改變時自己該如何面對,與如何利用方法來改變一個人的行為。古人訓示裡的察言觀色與見機行事是教我們如何觀察他人的改變並因應行動;某個功能本來可以印表的,但剛剛有人暫時把印表機拔走,如果此時使用者還是硬逼成系統要列印,那麼這個使用者通常會被歸類為“白目”型。

當希望讓某人做出自己期望中的改變時,我們會設計讓那人照我們的想法去做,古代流傳的說客故事呈現的是這個事實。若期望在某個功能裡可以依照特別的想法去改變處理流程時,我們可以安排參數的存在來造成這樣的影響。

見機行事,機可以是既成事實的反應動作,也可以是我們期望要發生的反應。無論如何,只要有變化存在就必須投入相對的努力與資源總是不爭的事實。

2007年8月3日 星期五

C22 Model的設計(1)──Model的存取


這張圖是我所認定整個系統具有的完整層次。Model的部分是以淡紫色所顯示的部分。

為了支援各種不同的Model實作,首先要定義的是Model的存取介面,這個介面定義的是Model自身內部資料的存取方式與邏輯;再上一層會有Model Service,裡面定義了跨Model存取邏輯的封裝。

如果有個戶政機關想統計轄區裡所有人的平均年齡,那麼會在代表人的Model裡定義取得年齡的方法,交由上一層的Controller計算平均年齡;如果戶政機關想統計的是所有家庭夫妻差距的歲數,這時就得在Service定義一個方法,分開從Model各取得的夫與妻的年齡再傳回差距的歲數。

縱觀View Service、Controller Service與Model Service,其實我們可以發現功能分析時確定要做的每一步驟都會在這幾個層次裡設計,在設計的同時必須確定每一個動作要如何實作,是否要使用元件。在我的想法裡,Service裡應放著系統的處理邏輯,每個動作都該放進元件裡實作(很簡單的動作則另外拉個API),如此可以明確分隔出二者的意義;此時每個使用到的元件介面就必須在架構設計階段內同時設計與定義。

在這個設計步驟完成時,整個系統的功能設計就告一段落;每一個功能、每一個步驟、每一個動作的處理邏輯都應該包含在內。至於如何達成處理動作,就交由元件來完成。

2007年8月2日 星期四

C21 做人的方法(5)──人正是Controller

環顧辦公室的四周,你會發現能夠依著自己意志取用物品的只有人類而已(帶寵物上班的地方除外);辦公室的環境能否維持整齊清潔,其實也就跟人類息息相關。

對應到系統來看,辦公室裡的物品可以視為資料,而人類就是可以隨意控制物品取放與移動的Controller,人能夠隨意地改變物品的所在位置。如果沒有規範,物品會在被使用後被丟到一個可能永遠找不到的地方;再過段時間後,等到不在位置上的物品且不該在位置上的垃圾一多,辦公室就顯得髒亂。

具有“從哪裡拿到的就放回那裡,不該放在那裡的物品不會放”這種美德的人實在不多,很多的人只為了貪圖自己的方便,以最快的方式取得要用的物品,使用後就隨手一放就走了,接下來的人就深受其害。常有人嘆息管理不易,但用另一個方向思考,只要每個人都做好自己應做的事情,放好該放的東西,整個團隊不就已經完全正常運作了嗎?

我們要記得自己是唯一控制經手物品的Controller,必須在對的時間用正確的動作取得該用的資源,並在使用之後立即將之回歸到原來的地方或該去的地方。如果沒把自己經手的物品回歸到它應該放置的地方,不是另一個人要付出代價讓它歸位就是環境要承受它不在位子上帶來的問題。遇到事情時思考施與受雙方面,牢記不要為他人或大環境帶來麻煩,常懷有這樣的想法後就會自然而然地把經手的所有事物作最佳的安置。

請記得對人而言,經手的資源除了物品外,還有自己應記得的一切與該去做的的一切;也就是說要妥善安排與自己相關的所有人、事、時、地、物。

2007年8月1日 星期三

C20 理論串接實作的瓶頸(5)──Controller≠Component

Controller是系統的靈魂,意即是最重要的部分,但是在許多介紹OOAD技術的網頁裡,Controller就直接被定義成一個Component。這樣的作法,讓我頭痛地摸索很長的一陣子都找不到出口。直到參考了幾篇提到Business Logic與Function Logic應分開設計的網頁後,才終於明白應該要有的理想層次。

先以一個例子來說,我們的系統需要在幾種不同的印表機上列印結果,我們的系統有一套產生列印內容的處理,另外要有驅動印表機的機制。在最初的需求裡只支援一款印表機,所以當時的設計定義一個Interface,再把列印內容的處理與驅動印表機機制寫成直接使用類別(相當於視作一個Component)。

後來因其他專案的需求,必須支援其他兩款印表機。最快的作法就是把原來的類別再複製一組,再修改差異的地方,但是這樣雖然快卻會在系統裡出現數組幾乎相同的程式碼,除了造成維護的負擔也顯示出設計上的問題──幾乎沒有設計而只有實作。

後來還是決議把處理內容的邏輯與印表機控制拆開,在中間插入一個控制印表機用的Interface;這樣就成為三組印表機功能實作對應三款印表機,但是內容處理的部分就只要一組。處理內容屬於Business Logic必須在架構設計時處理,印表機功能屬於Function Logic(因為每種印表機的控制都有差異)是Component細部設計的範圍。這樣的作法才完全符合我的架構設計理念。

省卻設計的層次雖然能夠節省初次開發的時間,但也僅只於此一利,之後的任何結構上的需求變更都將會凸顯其害。