2009年3月30日 星期一

U12 Review與Refactoring

基本Data Model讀寫器是結構複雜的元件,雖然Class的架構佈置與Method的設計準則都已經具有規範,但是實際的產出是否符合就需要review進而refactor以符合規範。進行的時機點在元件設計與實作完成,進行單元測試的前後都可以。

此時的調適原則有以下幾個方向:

調整常數與方法的定義層級。檢查元件中定義的常數與方法是否放在正確的對應層級,需要逐一檢查常數與方法並思索其意義應在哪裡表示。有時會遇到難以界定的情況,可以討論或是詢問其他有經驗的人來判別,而且所有的常數與方法都要在此時定義妥當。

調整Flow與Action內應該抽出為獨立方法的程式。在Flow與Action細部設計所定義的分解動作,是否應要求將各個動作獨立為方法。有相當設計經驗的人時常直接把一個動作所需的程式寫在應該抽取方法的地方,造成一個範圍大小混合的程式區塊,在這裡要避免產生這樣的結果。

使用最好的寫法實作方法。如果以可以達成功能作為結果,方法內的實作可能不是最佳的作法。在設計時可以指定寫法,如果原本的不適用在review的同時也可以重新選擇寫法。例如排序,雖然都可以排序集合內的物件,但是速度的快慢與寫法的難易有所不同。

各部分確認正確後補完註解。當元件內所有的內容都回歸應存在的地方並通過測試,就可以著手補上註解;Interface、Class、Data、Method與逐段的程式都要補齊。註解並不是有寫就好,其格式也會有一致的規定。(這裡先不寫上註解,後續會有定義註解與應用的說明)

調整後的元件於通過單元測試後,暫時可以視為完成並進行設計另一個元件。

2009年3月28日 星期六

U11 第二個元件(2)──讀寫XML、Text、與Properties檔案

XMLParserActionAbstract是負責讀寫XML檔案內容到XML Model的程式。大致作法是從輸入的InputStream裡拿到DOM物件,再依序將name、value、comment與attrubutes、children放入資料物件的對應屬性;另一方面再由反方面匯出為字串並將之寫回指定的檔案。XMLDataModelInterface定義了ATTR_ID的常數並在addChild()時取值傳入,另外提供兩個可用xpath字串取得指定節點的方法可以快速拿到想要的節點。

對TextParserActionAbstract的設計是逐行讀入文字檔的內容,放到一個新建child的value。在TextDataModel裡每一個子節點代表了一行資料,另外定義getLineCount()與getTexts()兩個方法提供操作。目前沒有修改內容的需求,即使有也打算把資料對應到JtextArea物件在上面修改後再將內容更新回來。

PropertiesDataModelInterface是讀取properties檔案後的產出,其功能與JDK裡的Properties功能相同,整合到基本Data Model是為了資料讀寫的一致性以便適用在通用編輯器上。PropertiesParserActionAbstract針對這種類型的檔案,讀寫內容與資料物件同步。

這三種類型的檔案內容可謂基本型,因為儲存的格式與Data Model的格式全都有著一對一的對應。將不同格式的資料收集到同一種資料物件裡,意謂著繼承基本Data Model的所有資料物件都能夠用同樣的程式來處理。這種作法讓Data Model在根本上只存在一種唯一的類型,未來繼承出去的所有資料物件全部具有同樣的特性。

2009年3月26日 星期四

U10 第二個元件(1)──基本Data Model讀寫器

既然稱為基本Data Model,就是希望所有的資料都能夠存放在其中;基本Data Model已經提供存取的方法,接著要為幾種不同的資料類型定義不同的Data Model來對應。常用的基本種類有Text、Table(CSV & JXL)、Properties、XML,XML類型之下另再衍伸Bean、Obejct兩種類型,其繼承關係如下:

在XML Data Model系列裡,首先要支援xpath較重要的定義以便進行節點的搜尋;Bean Data Model的資料格式採用Hibernate通用的XML的定義方法,提供attributes與properties的屬性;Object Data Model則是依據Bean Data Model的class屬性,直接生成設定類別名稱的物件後可以直接轉換成該物件作處理(該物件須繼承自Bean Data Model)。

為了將檔案或字串的內容對應存取Data Model,需要設計對應不同類型的資料處理程式來達成,下圖是我預計支援的各種檔案類型、讀寫類別與Data Model之間的對應。在跨越swimming lane的關聯上都發生一對多或多對一的關係,因此在讀寫時傳入的參數必須各自傳入對應的參數作為識別。

在ModelInterface的功能需求上,目前要能夠從檔案存取、從字串存取與從URL存取,因此定義了對應的方法;另外根據不同的parser要能提供預設的Data Model也定義了對應的方法。(Exception的定義可能有所缺漏,這要到細部設計時才會收集到完整的集合)
  public ModelInterface load(String filename, int type, ModelInterface model);
  public ModelInterface load(URL url, int type, ModelInterface model);
  public boolean save(String filename, int type, ModelInterface model) throws ModelParserExceptionDefault;
  public ModelInterface loadString(String data, int type, ModelInterface model) throws ModelParserExceptionDefault;
  public String saveString(ModelInterface model, int type) throws ModelParserExceptionDefault;
 public ModelInterface getDefaultModel(int type);

在設計時需要根據傳入的parser type轉呼叫對應的程式方法,切換點放置在Implemention、Flow或Action都可以達成功能,然而依據設計原則決定將放在Flow並將Action繼承出各種不同的實作類型。

2009年3月24日 星期二

U09 基本Data Model的單元測試

針對ModelInterface裡列出的所有方法,都必須要作unit test。由於在ModelAbstract這層還不需要實作類別,所以另外準備了一個ModelAbstractTest類別作為測試用途。

同樣地在產生測試類別的同時,創建所有需要測試的方法並一一加以實作,測試的內容越多越詳細未來出問題的機率就越小。在測試setAttributes()時發現ListMap需要一個clone的方法,用來避免傳入的集合被ModelAbstract直接引用的問題。

以下是通過全部測試的結果圖。

2009年3月23日 星期一

U08 根本的建立(6)──基本Data Model

依據經驗,各種資料物件的最大集合是XML類型。它擁有name、value、comment、attributes、children等屬性,因此先以protected宣告這些屬性。其中attributes具有key-value的特性,在輸出時又得依序輸出,需要另外設計ListMap物件;children是放置其他data node的集合,為了保持順序而定義為List。

ListMap的實作其實就是同時將List與Map兩種集合包裝在一個物件裡,新增與刪除時都同時維護兩個集合。參考List與Map提供的方法定義好ListMap介面並加以實作完成;ListMap這個集合物件可以在很多時機使用的。(建構時允許指定物件的類別會更有彈性)

完成ListMap與ListMapImpl後,就可以進一步設計ModelInterface;由於這是最基本的Data Model,因此在這裡定義了所有可能的操作方法以應付所有可能的操作。不過沒法子根據需求來定義需要的方法而必須自己思考出全部的應用,會是設計底層物件較困難的地方。所有會變動內部資料的方法都要加上boolean的回傳值,提供呼叫者驗證結果是否成功,這要特別留意一下。

name、value、comment三者都僅是單一的物件,因此只需要提供最基本的getter、setter操作。attributes、chileren都是ListMap集合,對於集合就必須多考慮單一與整體的操作,至少需要以下的操作方法才能算是完整:
  getWhole()
  setWhole()
  clearWhole()
  getPartCount()
  getPart()
  addPart()
  removePart()

參考以上方法定義好attributes與children的操作方法後,在ModelAbstract裡實作這些方法並在其中串連到ListMap的方法。

2009年3月20日 星期五

U07 元件結構調整與基本Properties名稱

目前元件結構有以下所列的三個調整:

元件用的常數拉出為獨立介面。雖然大家時常把常數與方法定義在同一個Interface裡,但是ApplLog只想使用LogUtil裡定義的常數時連方法也必須實作,因此將常數與方法切割為兩個Interface。實作時介面直接繼承後即可使用。不過這樣的繼承法雖然方便,卻也表示ApplLog與LogUtil已經緊密綁在一起,從意義上來看應該在ApplLog的常數Interface定義自己的記錄等級常數才對。(為了記錄等級的一致化,甚至要定義一個管理LogLevel的Class專門提供一致的記錄等級常數;這被定義為LogConstantInterface)

Flow與Action分工的界限。在ApplLog第一次設計時,Implementation、Flow與Action都定義同樣的一群方法直接從最外層呼叫進去,在Action裡才一次checkLogLevel()並呼叫LogUtil記錄,這種寫法可以達成目標,但完全不符合判斷流程要放在Flow的準則。在Flow與Action之間的流程與分解動作定義是設計時需要注重的部分。

Null Object的建立。由於ApplLog的Flow與Action並不是預設的名稱,最底層的基本Component在生成與使用時會發生NullPointException。加上一大堆判斷物件是否為null的程式看起來很沒效率,因而在無法生成物件時一律生成該物件名稱的Null Object。Null Object需要實作該介面的所有方法,但完全沒有任何處理而直接回傳正確的結果;如此一來會使程式碼簡潔許多。

在Root Component這層共有四個可定義的參數:
  public final static String CLASS_MODEL = "classModel";
  public final static String CLASS_FLOW = "classFlow";
  public final static String CLASS_ACTION = "classAction";
  public final static String LOG_LEVEL = "logLevel";

2009年3月15日 星期日

U06 第一個元件(3)──Log記錄器的單元測試

Log記錄器的單元測試著重在這幾個項目:LogUtil所有層級的記錄功能、ApplLog所有層級的記錄功能、ApplLog設定不同的層級時是否能過濾掉不應有記錄的層級。Log提供的方法沒法驗證是否正確,因此在測試之後就只能在console與記錄檔案裡確認結果。

先寫好兩個使用ApplLog方式記錄trace、debug、info、warn、error、fatal六個層級的方法,一個只傳入message而另一個可傳入message與trhowable;測試時先記錄一行現在的等級,於設定ApplLog之後再呼叫記錄所有層級的方法,最後檢視螢幕的輸出與檔案的輸出入容是否正確。

元件庫現在的結構圖:

2009年3月13日 星期五

U05 第一個元件(2)──Log記錄器

ApplLog是處理所有元件Log記錄的底層元件,執行ComponentUtilUI以下圖的數據建立ApplLog。

ApplLog不需要Model與Exception兩個部分,所以刪除相關檔案。在ApplLogInterface定義所有提供給外部操作的log方法,在ApplLogConstantInterface定義各種等級的常數;常數與方法會分開成兩個Interface的原因在於繼承時會有只使用常數與使用常數加方法的不同需要。

ApplLogImplAbstract裡實作Interface的所有方法都轉呼叫Flow內同樣的方法,並且要在呼叫前後包裝beforeInvoke()、afterInvoke()。
  public void trace(ComponentImplInterface impl, Object message) {
    beforeInvoke();
    getApplLogFlow().trace(impl, message);
    afterInvoke();
  }

ApplLogFlowAbstract要做的是判斷要記錄的等級是否小於等於規定的等級,然後呼叫ApplLogActionAbstract的動作。在這兩層裡的設計結果有許多不同的組合,最後使用最趨近設計準則的結果:Action提供整合的的記錄方法在內部轉呼叫LogUtil的各個方法,Flow呼叫檢查等級方法於需要記錄時傳入現有等級到Action的整合方法。

最後將實作Class名稱中的Default拿掉使之容易記憶使用,需要記錄log時呼叫ApplLog.getInstance()取得元件物件後即可呼叫所有等級的方法。

2009年3月12日 星期四

U04 第一個元件(1)──Log記錄器

第一個需要製作的元件就是提供診斷系統參考使用的Log記錄器。在一般的作法裡Log大多只是一個定義許多static method的Class,在需要留下記錄字串的時候呼叫對應的方法。這樣的作法很方便使用,同時記錄的等級可以在設定檔裡修改。

但是系統開發到某個程度後需要減少Log記錄,這個時候由於只能更換整個系統的等級,就形成要一個一個檢查哪些記錄該刪或該留,當發生問題要多記錄或是穩定後減少記錄需要再調整特定部分記錄等級時又要再全部檢查一遍。如此花費很多時間卻又得不到精確的結果,並不是時程緊迫的專案所能接受的作法。設計Log記錄器元件,一方面提供一般程式現行的直接記錄作法,另一方面提供每一個元件都能夠定義各自的記錄等級。

首先要製作最底層的LogUtil物件,內部包裝真正執行記錄的外部物件,而且定義了LogUtilInterface準備供給Log元件依記錄等級呼叫。Interface裡有直接對應到底層實作log方法(沒任何加工,只是直接轉呼叫),這是提供非元件類別呼叫使用的。依記錄等級定義了trace()、debug()、info()、warn()、debug()與fatal()六種方法。

在記錄元件的功能規畫裡,記錄等級不是依賴log自有的設定檔,而是從Properties裡取得預設的log層級;另外元件各自的Properties裡可以定義該元件的Log Level,其設定會優先於LogUtil裡的預設值而影響記錄內容。log level相關的屬性及存取方法都定義在Root Component層級,讓所有的Component的記錄都具有隨時從設定檔切換不同等級的功能。

2009年3月9日 星期一

U03 根本的建立(5)──建立Component使用的Class

在根本Package之下放置了Model、Propeties與Exception三種Class供Component相同意義的部分使用,為什麼不使用JDK的物件而要自己定義一組,這是來自避震器的觀念。將自己額外的需要寫在建構在中間的物件,同時可以集中吸收未來底層物件的改變。

Object
如果要確切地分割,應該要建立自己的Object Class。不過還沒有非得這麼做不可的理由,所以沒有建立。

Model
基本的Data Model會採用類似XML Node的定義,會有name、value、comment、attributes、children等等的屬性,每個屬性都提供getter與setter方法。(後面會說明作法)

Properties
繼承JDK的Properties物件,使用原有的功能。(在Model實作完成後會更換為Model物件,之後會說明)

Exception
繼承JDK的Exception物件,使用原有的功能。

Log與結構無關的獨立功能,目的是要產出記錄提供除錯、查看或分析用途。會是一個獨立的Component。(後面會說明作法)

2009年3月6日 星期五

U02 Project的佈置問題

經手維護的產品是Client-Server架構的,在初期的規劃裡只在垂直層面切割Project,Client與Server的程式則放置在同一層的Project裡。在專案裡的Client程式是由Server派送的,客戶總會抱怨為什麼改Client程式為什麼連Server的東西也要下傳?這就是因為對應的意義不同而取用到不需要的東西。

後來將程式碼分割為Client與Server兩個Project,終於解決了客戶在意的流量問題,但是在這樣的佈置下發現Client與Server間共用的程式碼在兩邊都各有一份,久而久之又爆發了程式碼不一致的狀況。最終再把共用程式碼抽取到另一個Client+Server Project後終於解決了這些問題。

公司另外有一個週邊管理程式,程式碼與建立安裝的環境放置在device這一個Project裡,初期能夠應付所有的需要而沒發現有什麼不對。後來在其他專案需求裡的週邊需要調整程式碼的架構寫法,那是與之前設計有不相容之處的,結果只好複製成兩個Project各自維護。在這個案例中可以發現,環境設定應該另外放置在一個Project裡。

Project的佈置也必須根據其存在的意義(使用意義、使用場所)加以設計,以免因為意義的混淆而多出了許多額外的東西造成負擔。同時也要建立Project的使用關聯作為追溯之用。

2009年3月4日 星期三

U01 根本的建立(4)──建立基本函式庫

在設計的時候會需要很底層的動作(像String與文字檔間的讀寫),那些動作僅有處理的行為而不會保留任何狀態,這些都會被歸類為工具程式(Utility)。每個底層動作都會被定義為一個Method,所有Method再根據其特性放到不同的Class存放。

Utility裡的Method通常被宣告為final static來強調其不可被改變且只存在一份的特性,不過static是一種“固定”,因此會將Utitlity Class用singleton方式建立用以保持僅有一份。提供外部呼叫的Method則會宣告為public final形式,內部抽取出來的Method則依原則宣告成protected final。Utitlity Class不需要定義Interface,每個public final的Method都是提供使用的。

設計Method時應採用Flow、Action的佈置方式,所有程式碼的風格才會一致,同時程式碼分析工具的判斷規則才會相同而不需改寫太多。註解的內容與結構的一致也是必要的,這樣才能保證其他的工具能夠套用。

所有的Utility Class都放在一個固定的Package之下,基於需要而改寫的JDK Class就放在另一個Package下作為集中處。在Utitliy Package之下的全部程式都放置在utility project,以便將來產生utitliy jar file所用。倘使在設計元件時發現某些動作會在元件中共用,但又只侷限在某個範圍內的元件才會使用,則應該放置在那個層級的Project裡,這樣才符合它的意義。