2014年1月15日 星期三

[Java]用介面Interface使用「用介面Interface訂出來的參數架構」

 (接續上一篇「用介面Interface設計參數架構」...)

有點饒口的標題。

我相信(因為這也曾經困擾我)一個程式語言的語法好學習,迴圈、判斷式、運算子、函式定義、物件宣告......可是怎麼把這些「規矩」組合成「功能」,就很難學......

因為鮮少有人在教。

如果在網路上、在論壇上,明確的講出自己想要的功能,其實得到的往往是連「模糊」都稱不上的答案。當然不能怪那些程式老鳥們這種態度,畢竟「又沒錢拿」,如果菜鳥們、出學者們都乖乖掏個幾千、甚至幾萬塊去走進補習班和短期課程,學程式設計肯定會好混許多。



可是學習方式應該有很多種。從「規矩」到「具體功能」之間,明顯有個難以跨越的門檻,沒理由要讓「任大家自生自滅,可以自己領悟的人才有資格進入程式設計殿堂」變成放諸四海、天下人接要遵守的規矩。

所以我用我的風格寫這些文章,或許......有人看過這些文章後可以有比讀其他文章、鑽研被公開分享的範例更有效率的學會程式設計。



接續上一篇「用介面Interface設計參數架構」...

(別告訴我你不懂何謂參數......我還沒準備好寫那麼基礎的東西。)

用「介面設計參數」這話直覺上違反Java特性,因為寫在介面中的參數會變成final形態,是不能被修改的。

可是 (請注意接下來這段話特指物件導向,所以看完立刻以為「你挑出了明顯的錯誤和例外」的人請去重讀物件導向) ...「物件導向的特性讓參數和函式在記憶體中都只是個位址,差別在於參數的位置可能導向另一個存放數值的記憶體區塊,函數的位置則是導向存放一連串的運算式、指令、判斷式、迴圈的記憶體區塊, 共通點在於它們都是個「另一段記憶體區段的索引」罷了,(主要的差異在於:函數存的索引值不能改,可是參數可以。另外就是程式碼的寫法不同。)

所以.......「為何不直接把函數拿來當參數用?反正在多型性的程式世界中,編譯器才不會好奇為何設計者設計的父類別沒有參數只有函數。」

但這都是 「用介面Interface設計參數架構」中的內容。



這篇講的其實是「多形性」到底怎麼應用來寫程式。或是說「用多型性寫程式」到底是會寫出什麼樣的程式。

這個介面只有定義一個函式。而這個函式要求傳入一個屬性為ValueInterface的參數。

為何只定義一個函數?

這是因為「大家習慣用IDE編輯器來編寫Java」,而「IDE編輯器(這裡是指Eclipse)讀取註解時有些特性」。

下圖是個「實作」這個介面的程式/類別/物件...(我又迷失在專業術語中了。)

先不看函數中的程式碼,(此為物件導向寫作法的好習慣,)注意一下開頭的註解(第36到39行的程式碼),這個註解會在任何寫了Operate1這個物件名稱的地方跳出來。(只要把滑鼠游標一到上面,就會跳出這段程式碼。

像下圖...可以看到游標一到Operate1上,就自動彈出一個小視窗,顯示註解內容。

有趣的問題是ops這個矩陣中,不管是ops[0]、ops[1]、或ops[2],都有operate()這個函數。

可是如果把註解寫在Operate1的operate()函數上,游標移到接下來的程式碼中出現的operate函數時,什麼都不會顯示。
因為...這個operate()是介面ValueOperatorInterface的operate(),不是Operate1的,而我在介面中沒有寫任何註解,自然不會顯示。

詳細一點說明為何只設計一個函數:當這個物件只有一個函數時,你寫在建構子上的註解自然會是它唯一的功能說明,閱讀性理論上會好一點。可是我工作的地方,大部分的工程師都還是習慣結構導向寫法,物件導向...頂多就是「記得寫個建構子」,多型性?好像沒啥人真得在活用。



基於多型性的理由,我不會考慮直接用Operate1當成參數的型別/屬性,(但是這樣設計程式沒什麼錯,更沒什麼問題,就是有點可惜罷了。而且如果要這樣作,就沒有必要寫介面了。)

但說到所謂的多型性...我可以隨意組合ops矩陣的內容,而且都會成立、都可以精準運作。(除果出錯,絕對是程式出錯,而不是「多型性」這個原則有例外。)

有了多型性,我設計好了各種不同的Operator類別,然後在各種不同的程式中組合它們......我就可以快速擴建出各種不同功能的程式。




一般會反駁我這種寫法、認為這樣很多餘的人,都會認為...「與其我設計三種Operator類別,不如我在ValueInterface中額外增加三個函數,(甚至直接就用物件寫,不需要經過設計ValueInterface的步驟,)然後把本來要寫在三種Operator中的程式碼般到三個函數中。」(參考一下我平常測試的方法...這次不附上測試方法了。不過,如果覺得這樣測試根本不對......根本不重要啦!原因...繼續看下去。)

所以我一開始要解釋參數與函數的差異......

我原始的想法是認為......一個只有兩個函數(一個讀取參數、一個寫入參數)的介面會在記憶體中佔去兩個單位的記憶體,一個有五個函數的介面自然就是在記憶體中佔去五個單位的記憶體。如果用矩陣大量操作這個ValueInterface,例如說「現在要1000個ValueInterface」,想想看「2*1000」和「5*1000」,會有多少差距?

但後來發現......其實不會有差。設計兩個類別,都只有一個參數,然後甲類別有兩個函數,乙類別有五個,分別用一個ArrayList無限量去「產生、並收納」,結果會發現...我在Android模擬器下分別去執行,會發現大概都在89300000次的執行上下時,會出現「OutOfMemory」的錯誤。

反過來,如果將乙類別的函數全砍掉,改成八個參數,(參數的結構不詳述,)結果執行次數一口氣就掉到14300000左右而已。

簡單來說,我對於在物件導向機制下Java應用記憶體的方式假想試錯誤的,「函數並不會隨著類別宣告為實體物件而增加佔去的記憶體空間,只有參數會。」不管類別A被產生了幾個物件實體,函數都導向/共用同一個記憶體區塊。

(所以我上面說「測試方法對不對」並不重要......仔細研究了一下物件導向的機制,確實如此。)

我並不能夠說服自己:這樣寫,程式的效能和記憶體的使用效率比較高。──並不會!但我還是繼續這樣寫程式。



為何要這樣寫程式?因為......

專案方便擴充!提高程式碼利用度!

如果不盡可能徹底使用物件導向,一個程式未來如果要擴充功能,難度明顯會高許多。

上面提到的「與其我設計三種Operator類別,不如我在ValueInterface中額外增加三個函數,(甚至直接就用物件寫,不需要經過設計ValueInterface的步驟,)然後把本來要寫在三種Operator中的程式碼般到三個函數中。」假設一支程式經過無數次的改寫擴充後,已經從三個函數增加到十三個函數,每個函數的平均工能從三十行程式碼增加到六十行程式碼......然後今天又要再增加兩個新函數,然後有至少五個舊函數的功能要修改........請便吧!看到這種程式碼,我每次都有罷工的衝動。

把要新增加的功能額外獨立成一支程式/類別,然後再要擴充的地方增加一個參數。像下圖,我只要新設計一個實作 ValueOperatorInterface的類別,然後在switch中找個地方插入它即可......

雖然這樣沒有徹底作到「舊程式永遠不會膨脹」,但是修改的難度低、幅度永遠不會過大,而且程式的邏輯/模式永遠不會變化太大,不會因為逐次擴充/修改/維護,而失去原樣。

這個範例使用的是switch指令。假想一下今天使用的是「if...else...」然後每一段「...」裡頭都有數百行程式碼,第一次拿到程式碼、又要再上頭擴充功能的人,可能就會迷失在追蹤各種if條件值中,──可能很難搞懂自己新增加的if條件為何沒有順利發揮作用,到底是哪個if、或哪個if中的if把程式的執行判斷權拿走了。(「執行判斷權」?我又發明了一個莫名其妙的名詞。)

當那「數百行程式碼」被縮減成一個物件和它的函數時,if就很好追蹤了。(就像這個switch......天哪!比這個switch還好用。)



另外,如果不盡可能徹底使用物件導向卻又牽涉到「版本控管」......先假定物件導向跟專案管理無關,一般的專案管理就只能期望專案管理軟體夠強、或專案團隊製定的專案管理流程夠嚴謹.......好笑的是:JAVA物件導向的精神之一就包含了解決上述問題。

使用Java撰寫程式不善用物件導向特性,卻期望「專案管理」軟體代勞......我不知道這叫什麼,只是發現這想法/態度/策略挺普遍的。

有機會在介紹一個更明確的「多型性」寫作法。

沒有留言:

張貼留言