2009年2月25日 星期三

T23 養成所有設計都符合準則的習慣

定義元件的佈置規則藉以增加檢測程度的同時,意味著開發人員必須花時間依規定放置程式碼。這意味著開發的效率“可能”與設計準則的數量成反比。許多有能力的設計者對於設計準則的規範都有相當的排斥感,明明可以很快地完成功能卻為了額外的規則而變得綁手綁腳,甚至感覺設計的靈感因之而減少許多。

單就開發與測試而言的確是這樣子,依循設計準則的代價是多花數倍的時間撰寫程式碼,但是結果都是一樣;這也就是之前主管為什麼不想多投入人力作好設計的原因。但是從其他角度來看,元件的對應、封裝與切割可以使設計更有彈性,層次的繼承、佈置與統一能夠讓產出一致化,使用關聯、追溯與收集,都是在需求之外可以得到的好處。

以快速著稱的設計者大多無法利用文件清楚描述他的想法,要求畫出程式流程圖時感覺似乎是想要他的命一般,對於使用的關聯亦無法百分之百地確認。如果能夠發現“快速設計”的代價是犧牲掉這麼多有意義的結果,設計人員應當願意好好佈置好自己的程式碼,讓所有的人都可以快速地明白自己的想法。

開發程式時沒有設計準則的話,大家可能都任意地依照自己的想法做事;規定好設計細節之後,每段程式碼的放置都需要檢查是否符合規則。利用工具程式檢查設計產出是否符合規定,就像在不能闖紅燈的地方安置警察,讓每個人都知道不能做違法的事;儘可能鋪設全面的檢查點,就像檢測食品時用最詳細的檢驗方式,讓黑心產品無法過關而被刷下。

設計人員對自己產出的所有設計都戰戰兢兢地都依照準則放置的話,達成元件化的軟體工廠的目標是指日可待的。

2009年2月24日 星期二

T22 工廠的成功要看產品的可檢測程度

細心的人即將購買物品之前,一定都會去訪查所有類似產品的各種規格再從中選擇最適合的一個,採買時願意花功夫訪查細節,開發系統選用元件時應沒可能隨便就拿來用用。從另一個角度來看,製作物品者都可以提供各式各樣的規格資料與使用說明書來讓人明白其內涵,元件沒理由只具備Interface而像個黑盒子。

以我所知,Naming Standard、Coding Rule是目前最常用來檢驗程式碼是否符合規定的項目。前者會根據專案規定在review或unit test時查看,後者則會選擇一種適用的工具定義程式碼檢查項目後全部加以檢驗。但是架構的佈置與動作的流程(註)都只能看到理論的闡述,沒有明確的作法與可驗證的工具,問過同事也查過資料,一直都沒有聽說過有這樣的工具可用。

每一類型的工程,其任何產出物都應具有可檢驗的諸元可作測量或測試,而且可檢測的物件與規格越細微相對代表其品質越可靠。在該檢驗的部分只有模糊的理論時,自然就會被不同的人用不同的想法來解讀為不同的作法。人員之間有差異、專案之間也有差異,漸漸地整間公司內的產出根本都不相同,此種情形之下要如何採用標準化的制度或工具來套用雜亂的項目?

簡單地說,一個產品提供的檢測項目越涵蓋其特性就越能保證會有一定的品質,以目前開發重心都傾向快速完成功能的思維來看,對於系統內部的使用、測試、交接、修改、維護等等的需求總是難以做到不令人詬病的狀況。明列出所有程式區塊的特性與檢查點,會有助於建立完整測試個案與快速讓他人瞭解內容,這也就是建立標準元件結構的目的。

軍隊裡對於所有的姿勢(站、坐、蹲、跪、伏……等)與動作(左右轉、行進……等)都對肢體的擺放有明確的定義,在不同的情境下(姿勢與動作再加上背槍、提物、抬物……等)也會另外定義對肢體的要求。詳細的定義與嚴格的要求在人數眾多時,在每個人身上呈現的就是整齊的紀律,將同樣的意義推展到軟體元件的產出,所需要的就是明確一致的詳細設計準則。

註:雖然有很多開發工具都能在程式執行時追溯出該次執行的完整sequence diagram,但那是很詳細的程式呼叫順序,對於不需要看到太多實作的SA與部分SD人員來說根本沒法得到他們需要的設計概念。

2009年2月22日 星期日

T21 簡單型的Component

簡單型的Component類似於Deploy過的Component,與Component最大的差異是Inplementation、Flow、Action與Properties都放在同一組Class裡。這裡的設計思維與一般的Class可說完全相同,不過我仍會堅持使用Interface-Abstract Class-Class的設計以避免reuse後重覆程式碼的問題。

在這種作法下,Flow與Action的設計準則還是要遵循,副流程與分解動作還是要另外獨立為一個Method。不過此時產生一個問題:遇到一個內部方法時怎麼知道其意義是Flow還是Action?(從名稱不見得看得出來)而這種定義上的不一致,將會連帶造成分析程式碼工具分析錯誤的結果,加上元件結構有所變化而必須改寫元件的分析工具。

會納入元件庫的Component一定要使用標準的結構,這樣才能符合所有的標準而有一致化的運作與產出。雖然簡單型Component可以運用一些規範的要求來達到同樣的效果,但是額外的要求都會更造成開發人員的負擔,需要另外注意與檢查的工作會變得更多。反之若為了方便而減少了那些額外要求,那麼每個人又會再用自己的想法來解讀那些模糊地帶,又會發生產出時所有可能發生的問題。


在系統架構的概念圖裡,我認為ViewListener與所有的Service都可以使用簡單型Component來加速開發的速度。其原因為:這幾個部分常常根據客戶的需求不同而難以重用、ViewListener的意義僅是介接View與後面的處理邏輯、Service使用的資料與參數都由外部傳入使得它僅是處理邏輯。對於這些偏重於部分特性的元件,使用簡易型Component應已足夠。

2009年2月19日 星期四

T20 Component Flow對應的Flow Engine

根據Component設計準則產出的元件,Flow裡就只有流程與呼叫的動作名稱,流程與動作兩個元素正是組成流程圖(或Activity Diagram)的內容。根據這個概念,就有可能製作一個工具,匯入流程圖檔案後依據設定執行,並根據動作的傳回結果分歧到不同的動作步驟。

依流程圖的概念,將流程圖對應匯入到一個Flow Model,每一個動作對應為一個Action Node。定義Start Node指向一個開始的Start Node,還需要Switch Node(或合併在Action Node)根據前一個動作的結果分歧到不同的Action Node,最後走到End Node時再把結果傳出去。

Action Node會需要呼叫時的參數,可能是固定的值也可能是前面執行某動作的結果,需要再定義一個宣告與存放資訊的內部機制,讓同一個流程圖內任何一個地方都可以存取到之前曾經執行到的狀況。這個機制裡存放資訊的物件應繼承自ModelAbstract,如此一來就能讓所有與Model相關的Component都可以操作它。

曾經看過國外某家作主機系統的公司,採用Microsoft Visio製作執行流程圖再用Flow Engine解析運行,雖然繪製與修改流程圖要花很多時間,每個節點只能在執行後立即判斷結果來分歧運行,但是這樣直覺式的設計卻是一種進步。想像有各種不同語言平台的Flow Engine的情境,只要繪製一組流程就有跨平台的效果;或者用特定規則撰寫的文字(通常是XML),也可以轉換為流程執以達成快速開發的效果,這正是時下許多Framework所採用的作法。

2009年2月18日 星期三

T19 Component各部分的設計準則

Properties
每一個參數都要定義一個存放的常數與專屬它自己的存取方法。存取方法內呼叫的是通用的存取,傳入的是屬於自己的那個常數。
public final static String CLASS_MODEL = "classModel";
public final String getModelClassName() {
  getProperties(CLASS_MODEL);
}

Model
每一個資料都要定義一個存放的常數與專屬它自己的存取方法。存取方法內呼叫的是通用的存取,傳入的是屬於自己的那個常數。概念與Properties同。

Flow
Flow裡應只具備完成Interface Method的流程與必要的Method。就像拆解出分解動作的步驟,分解步驟的細節要求則放在Action。
Flow裡意義獨立的動作,要宣告為Action Method。
Flow裡若有數個動作相同的集合,要抽出宣告為一個內部的Flow Method。

Action
Action裡要完成的是分解動作的內容,其意義與Flow相同,僅由流程與必要的Method組成。
Action裡意義獨立的動作,要宣告為Action Method
Action裡若有數個動作相同的集合,要抽出宣告一個內部的Action Method

Exception
Flow與Action裡的Method若有不同的傳回狀況,要額外宣告各種狀況的Exception。使用的所有Exception都要繼承自Component內的根本Abstract Exception。

2009年2月15日 星期日

T18 從語言文字看元件封裝

在一次同事間的聊天提到韓國人時,有人忽然提到他有位專精語言學的朋友說過,韓文經過良好的設計是最適合拿來與外星生物溝通的文字。我只看過少數幾國的文字,也沒什麼深入研究,不過還是立即發現韓文所擁有的特性:一個完整的方塊表示一個字,同時包含了該字的發音標示。

對完全不懂某國文字的生物來說,看到文字排列在一起時,西方的文字長短不一必須額外使用空白字來切割單字,日文的假名則是全部連在一起難以分辨,許多韓文排列在一起時,任誰都可以馬上切割出一個一個的字。雖然中文也有類似的效果,但是筆劃的組合太過複雜,遠不如韓文以簡單筆劃的表示而較容易學習。

在一串文字中可以快速切割單位、每個單位裡包含的內容由固定規則組成、單位內容沒有太複雜的變化,這幾個特性的存在使得韓文被推選為最適合與外星生物溝通的文字。相對於不瞭解系統內部的人來說,這不也正是他們所希望可以快速瞭解系統內部的特性嗎?

藉由Component Diagram的描述快速切割出系統內部的單位、Component內部根據特性固定切割為六個部分、每個部分都具有固定的coding rule。公司內(甚至是業界)元件的組成與寫法如果一致,人員任意更換到沒有待過的專案裡仍舊可以依據幾張系統元件圖就可以快速地上手。

大家都知道只要能提出固定的規則就能夠寫出對應的程式來處理,元件內部的規則一致自然就可以撰寫工具來作處理。以沒有例外的一致寫作規則作程式設計與系統開發,這正是我這幾年對於程式設計的領悟。

2009年2月14日 星期六

T17 元件工具(3)──把Component整合回一個Class

在設計元件的時候根據不同的特性將程式碼佈置在不同的部分裡,使得一個元件對應為多個部分。在之前大陸的專案裡測試發現,執行起來的效率並不會特別緩慢,所以系統就直接執行而未對元件內部多作任何處理。不過在講求效率的系統之下,多個Class勢必會影響其效能,因此還是必須有應對的策略。

對策是將性質相近的部分整合回一個Class裡,也就是把Implementation、Flow、Action與Properties四者的Interface、Abstract Class合併在一起(Component Interface、Model與Exception本來就應該獨立存在);整合之後的結果已經與目前常見的寫法沒有差別。整合的邏輯大致如下:

建立名為ComponentAbstrct的Abstract Class實作ComponentInterface。
●Implementation、Flow、Action與Properties四者的變數、建構方法、initialObject()與disposeObject()內容合併在一起。
●Properties內屬於此層級的所有常數與方法搬到ComponentAbstrct並在名稱前加prop_(常數名稱不變)。
●Action內屬於此層級的所有常數與方法搬到ComponentAbstrct並在名稱前加action_(常數用大寫)。
●Flow內屬於此層級的所有常數與方法搬到ComponentAbstrct並在名稱前加flow_(常數用大寫)。
●Implemenet內屬於此層級的所有方法搬到ComponentAbstrct,毌需重新命名用以實作ComponentInterface的宣告。
●檢視所有ComponentAbstract方法裡的程式碼,使用getComponentXXXX().CONSTANT方式呼叫的方法,全部改為xxxx_CONSTANT來呼叫(Properties的除外)。
●檢視所有ComponentAbstract方法裡的程式碼,使用getComponentXXXX().method()方式呼叫的方法,全部改為xxxx_method()來呼叫。

在理論上這樣的整併是可以達成的,因為變化只在元件本身的內部;不過要把這個功能寫好卻是很不容易的事,因為有許多環結的細節都得緊密銜接才不會造成問題。目前先把這個功能的實現放在一旁,等必須要作的時候再回頭完成它,因為即使沒有整合回來元件還是可以正常執行的。

這樣的整合之後,負責元件內部合作的基本Component已經沒有存在的必要,因為元件內的互動都在ComponentAbstract裡。但是放置所有元件基本功能方法的RootComponent還是要有,因為功能方法並沒有任何取代的地方;這正是需要把基本Component與RootComponent分開定義的原因。

註:方法、常數與變數更名時的對應若特別記錄起來,用特別的運算方法來轉換名稱,就能夠達到混淆的功能;抑或是把某些方法的程式碼嵌到呼叫它的地方造成方法的消失。這兩種作法都能有效降低程式碼被反組譯後的可讀性。

2009年2月12日 星期四

T16 根本的建立(3)──Component共同的RootComponent

基本Component只是負責元件內部各部分的合作運行,完全不會擔負元件實際上的任何功能。雖然基本Component已經是所有元件的Root,但這並不表示我們可以把元件的通用功能拉到基本Component放置──這樣的作法會導致元件內部運作與對外實際功能混合在一個Component裡,造成二對一的關聯。下一篇提到的功能的分隔界限剛好會落在這兩者之間,因此分離內部運作與對外功能是必要的。

將不同意義的事放在不同的對應範圍是讓設計具有彈性的作法。繼承基本Component的都是功能性的Component,為了放置抽取出來的功能需要建立一個RootComponent來放置所有Component都必定要做的動作。讓基本Component負責的是內部合作,RootComponent負責共用且與合作無關的功能。

參考上一篇產生UI元件的圖輸入所有的參數,並在eclipse裡的root專案新增對應的Package然後把產生的所有java file複製進去即可。

從RootComponent開始,每一個元件都有兩件事必須額外作到:內部物件的取得方法要cast成這個Component層級的Interface(已經在元件結構產生工具中處理),所有定義Method拋出的全部Exception都必須是這個Component層級的。每個單位都只使用自己層級所擁有的資源,才能便利於切割出自給自足的獨立單位。

元件庫現在的結構圖:

2009年2月10日 星期二

T15 元件工具(2)──Component結構程式的外殼

擁有Component結構產生工具後,其實已經足夠,但是舉凡Class的名稱與傳入的參數定義都必須詳細定義在相關文件中提供使者的人參考。我們也都知道,有效降低使用門檻的最佳方法是設計一個對應的UI外殼,藉由畫面的提示與直覺的操作讓使用者可以直接上手。

我用簡單的寫法設計出ComponentUtilUI.java來負責這個工作。執行這個程式可以帶出元件產生工具的UI畫面,輸入對應的參數後可以立即執行。執行的結果與實際呼叫API的結果是完全相同的。

在這樣的設計下,View與Controller是分離的;也就是說同樣是產生元件,執行的時候讓使用者用UI輸入或是用程式自動產生(大量批次)是可以自由調整的。

註:Component結構產生工具與其UI基於可以正確使用的要求下,我是用最快的方式(也就是沒有特別佈置的亂寫方式)所作的,目的是為了表達這是可以達成的功能。在軟體工廠的元件化考量下,這兩個小工具都應該使用Component的規範重新撰寫比較理想。(都必須繼承自RootComponent,請參考下一篇)

元件庫現在的結構圖:

2009年2月9日 星期一

T14 元件工具(1)──產生Component結構的程式

Component內擁有的Interface、Class如此之多,舉凡架構佈置、命名規則、實作與繼承類別、必要方法宣告與必須要有的程式碼等等,都是一開始都必須具有的特性。為了保證元件的基本結構全部是百分之百正確無任何誤差,需要一個工具程式提供固定不變的輸出內容。

執行時需要的參數有:輸出資料夾名稱、Component名稱、Component Package名稱、Parent Component名稱、Parent Component Package名稱,設計的方向是把十九個類別的內容放在文字檔裡,把需要用參數置換的文字換成%1、%2之類的關鍵字。建立時只要讀入所有的檔案後置換裡面的關鍵字,再更名儲存到輸出資料夾。這樣做的好處是在範例程式碼需要修改時只要修改外部的文字檔,而不需要變動程式。

以下是傳入的每個參數定義,呼叫時必須全部都有值:
  %0 範例檔來源資料夾
  %1 輸出檔案儲存資料夾
  %2 Component Package
  %3 Component Name
  %4 Parent Component Package
  %5 Parent Component Name
  %6 儲存檔案所用的字元集

我準備了ComponentUtil.java來處理元件的產生,並在其中放置了main()直接執行來驗證其結果。傳入的七個參數使用以下的資料:
  [root專案下data/component資料夾的實際位置]
  “c:/temp/”
  “tw.idv.joying.component”
  “Component”
  “tw.idv.joying.comp”
  “Base”
  “Big5”

把產生的所有檔案複製到tw.idv.joying.component裡,將之Source/Organize Imports與Source/Format後進行編譯,可以發現結果是正確的。

元件庫現在的結構圖:

2009年2月4日 星期三

T13 Component的預設生成Class

“類別的特性應該封裝在Abstract Class裡,Class只負責物件生成的動作”,這個原則應該被套用在所有的Class──包括Component裡的每個部分。

基本Component與功能特性抽取而存在的Component只是具備特性提供Component來繼承,本身僅需建立Abstract Class即可,但是功能用的Component是要被執行的,需要建立執行用的類別。一般在第一次建立類別的時候都會依照當時的用法決定生成的方式,但是同一類別在不同的情境下有可能會有不同的生成與使用方式,可能採用singleton的方式、或是new的作法。因此形成在D05與M02裡提及的型態,而且必須在元件的六個部分同時使用。

物件生成的同時可能使用不同的生成參數,為了簡化呼叫建構方法的過程,Class一律先使用沒有參數的new方法來建立物件,再使用額外的initial()方法傳入需要的參數作初始化的動作(這只在Implementation使用,且應定義在Component Interface)。把建立與初始化的時間點分開另有一個好處:能夠先行生成該物件的空殼再依呼叫的內容初始化而省略一開始先用預設值初始化的浪費。我會建議再定義一個dispose()方法負責清空元件的狀態,在不用重新new的情況下還可以再initial()使用。

最後,每個元件都必須要擁有自己的唯一識別代號,這是在Component的通用處理時識別現在處理的是哪一種Component,進而根據其元件代號作不同的處理(譬如根據元件代號從設定檔裡讀出屬於該元件的參數設定傳入Component)。每個Component都定義一個名為COMPONENTID的常數,其命名規則為package name + component name。

  public final static String COMPONENTID = “”;

2009年2月3日 星期二

T12 Component必須直接或間接繼承基本Component

將眼光放到Component層級,並運用Class的思維來看待Component,可以發現元件的本質其實就等同於我們現在常用的Class。在使用一個Class實作一個Component的情境下,重心多放在如何達成功能的目標,使用機制的互動會因程式碼都在同一個Class內實現而被忽略,繼承的考量也僅會著眼於功能的重用。

為了未來的一些好處而把元件內部切割為六個部分的同時,必須保證所有Component都有同樣的切割方式與運作模式。在建立基本Component之後就得要求每一個Component同樣都必須具備六個部分,而且每一部分都要繼承自基本Component。

倘使Component因功能上的需要必須繼承其他的Component,那麼就會形成上圖裡的情形。這裡的要求是每個Component都要具有自己的Implementation、Flow、Action、Model、Properties與Exception六個部分,而且全部都要繼承自Parent Component的每一個對應部分!

若某一層級沒有建立起對應的Abstract Class,初期可能不會有什麼問題。但是只要遇到屬於那個消失層級應負責的動作時,那些動作找不到放置的地方就必須依附在旁邊的Class裡,在需要切割使用的狀況下,很可能切開的部分多包含或少包含應作的動作而使得系統出現問題。

基本Compoent負責的是六個切割部分的合作,負責的僅是最底層的內部合作;Component的設計就如同一般元件設計,著重在功能的實作。當所有元件內的程式碼都分類放置妥當後,才有可能依同樣的規則寫出可以適用在全部Component的工具。

2009年2月1日 星期日

T11 基本Component內部(4)──基本Component的單元測試

基本Component的內部結構已經成型並鋪設運作的基礎,在一個特定目標達成之時應該測試跨出的這一步是否正確無誤。由於現在只有一些Interface與放置元件合作必要的Abstract Class,測試時並沒有實際的對應物件,因此會準備適量的Class搭配一些檢驗的方法提供給Unit Test檢查結果。

首先在專案裡建立一個名為junit的資料夾放置測試程式碼。根據Component的結構,必須準備六個Class才能作完整的一對一測試;這六個Class我只先準備空的基本建構方法與只執行父類別的initialObject()、disposeObject()。接著建立要對六個Class作測試的TestCase。再來在TestCase裡的setUpBeforeClass()加上建立Properties與Implementation的動作,在tearDownAfterClass()裡對應地消滅它們;至於setUp()與tearDown()則沒有什麼特別要做的。

基本Component的功能是建立起內部六個部分的合作機制,因此測試時只針對Implation、Flow與Action三個部分測試其內部的變數是否建立成功。用以下的程式碼建立起基本Component物件的參數集合與其本身:
  // 建立測試用的Properties.
  properties = new BasePropertiesAbstractTest();
  properties.setProperty(BasePropertiesInterface.CLASS_MODEL,
    "tw.idv.joying.comp.BaseModelAbstractTest");
  properties.setProperty(BasePropertiesInterface.CLASS_FLOW,
    "tw.idv.joying.comp.BaseFlowAbstractTest");
  properties.setProperty(BasePropertiesInterface.CLASS_ACTION,
    "tw.idv.joying.comp.BaseActionAbstractTest");
  // 建立測試用的Implementation Class.
  impl = new BaseImplAbstractTest();
  impl.setProperties(properties);
  impl.initialObject();

再根據每個部分裡所擁有的變數加以測試。經過測試與修改之後,終於讓所有的測試方法都可以得到正確結果: