軟體設計是一種求取均衡的取捨,而取捨的關鍵視軟體欲解決的問題而定。正如同人在《軟體開發是工藝還是工程》中所提到的,軟體開發的本體並不在工藝,也不在工程,而是一種問題解決的過程。依此觀點來開發軟體,我們一開始會用最簡單且直覺的編程方式把軟體的功能做出來,然後再隨著需求的改變,逐步演進程式碼及設計架構。用複雜性及複雜適應性系統的概念,找到混沌邊緣,組織系統讓複雜行為自然形成卻不致混亂而導致失控,如此演化所產生的系統將具備穩定與彈性。
這種問題解決的過程,如同「庖丁解牛」一般,循著問題脈絡而予以支解,而不是用蠻力大刀亂砍。然而,軟體開發要解決的問題畢竟不如解牛那般顯而易見。一開始,我們根本不知道邊緣在那裡,等到我們跨越了邊緣,到達了極度混亂的混沌時,問題又難以掌控了。所以,在軟體開發的過程當中,我們必須能夠察覺邊緣所在,運用測試先行的實務,我們可以讓程式聚焦於欲解決的問題上,不致發生程式碼發生過度混亂而導致無法掌控的情況。
一旦需求不斷增加,在現有設計無法負荷軟體需求變動時,可以透過程式碼重構來增加空間以容納新功能。重構並不是增加新功能,而是在不改變功能的情形下改變其程式結構,測試程式在此扮演不致使修改結構造成對程式功能的影響之角色,而能保持程式碼的結構不致因紊亂而變壞。
如此,透過重構,我們將程式碼組織起來到混沌邊緣,同時促成程式的簡明以實現優美,讓系統更容易適應變化。把程式碼之中,可以簡化且不損功能的部分予以簡化。例如,某一段程式碼,如果在 class 中,一再地重覆出現,應提取出來而成為 method;若在多個 class 重覆出現,則可能需要考慮提取出來而成為其它 class 的 method,這樣可以大幅降低重覆程式碼的現象,以增進程式碼的結構。而在提取重覆程式碼的過程中,也很容易發現原來未發現的關鍵抽象概念,使得程式碼的可讀性、彈性或穩定性可以大幅增加。
然而,對於程式碼中的迴圈處理要抽取出重覆的程式碼,問題似乎會複雜些,例如下面這一段程式碼:
for (i = 0; i < myArray.length; i++) {
processor.process(myArray[i]);
}
...
Iterator iterator = myList.iterator();
while (iterator.hasNext()) {
processor.process(iterator.next());
}
從這段的程式碼我們發現,因為存放資料項目的聚合體(資料型態為陣列或實作衍生自 Collection 的 class,如 List)的差異,對於如何取得下一筆資料項目的方式也就不同,使我們必須撰寫不同的程式碼來處理迴圈,但它們本質上卻是做相同一件事呀。所以,在程式碼應儘量避免重覆的角度來看,上一段程式碼似乎顯露著一些美中不足的地方。
這問題不難解決,只要為陣列寫一個 Iterator。這樣,即使資料項目聚合體並不一樣,我們還是可以利用 Iterator,將資料項目聚合體讀取下一筆資料項目的實作細節封裝起來。這樣,就可以用相同的方式來處理迴圈控制。如下段程式所示:
public class ArrayIterator implements Iterator {
private Object array;
private int index = 0;
private static final String
ARRAY_NOT_SUPPORT_REMOVE_OPERATION = …
protected ArrayIterator(Object array) {
super();
this.array = array;
}
public static ArrayIterator
getIterator(Object array) {
ArrayIterator itr =
new ArrayIterator(array);
itr.array = array;
return itr;
}
public boolean hasNext() {
boolean result = false;
if (index < Array.getLength(array)) {
result = true;
}
return result;
}
public Object next() {
Object obj = Array.get(array, index);
index++;
return obj;
}
public void remove() {
throw new
UnsupportedOperationException(
ARRAY_NOT_SUPPORT_REMOVE_OPERATION);
}
}
...
public void process(Iterator iterator) {
while (iterator.hasNext()) {
processor.process(iterator.next());
}
}
process(ArrayIterator.getIterator(myArray));
不過,程式碼中的迴圈處理包括了迴圈控制與迴圈內的資料處理,即使迴圈控制的問題解決了,但如果迴圈內的處理不一樣,單靠 Iterator 還是不夠的。如同下面這一段程式:
…
Result1 rslt1 = null;
Iterator iterator = myList.iterator();
while (iterator.hasNext()) {
obj = iterator.next();
processOp1(obj, rslt1);
}
Result2 rslt2 = null;
Iterator iterator = myList.iterator();
while (iterator.hasNext()) {
obj = iterator.next();
processOp2(obj, rslt2);
}
…
}
從這一段程式碼我們會發現,迴圈內的處理並不相同,所以不能用上面的方式來簡化程式碼,怎麼辦呢?把不一樣的部分封裝起來吧,也就是把迴圈內的處理提取出來,並放在 LoopProcessor 的抽象概念中;然後將共通的部分,即迴圈控制提取至 ProcessDirector 中,最後結果會變成這樣:
public interface LoopProcessor {
public void process(Object obj);
}
public class Op1Processor implements LoopProcessor {
private Result1 result = null;
public void process(Object obj) {
…
result = …
}
public Result1 getResult() {
return result;
}
}
public class Op2Processor implements LoopProcessor {
private Result2 result = null;
public void process(Object obj) {
…
result = …
}
public Result2 getResult() {
return result;
}
}
public class ProcessDirector {
public void makeProcess(
Iterator iterator,
LoopProcessor proccessor) {
Object obj;
while (iterator.hasNext()) {
obj = iterator.next();
processor.process(obj);
}
}
}
…
LoopProcessor processor1 = new Op1Processor();
LoopProcessor processor2 = new Op2Processor();
ProcessorDirector director =
new ProcessDirector();
Iterator iterator = myList.iterator();
director.makeProcess(
iterator, processor1);
result1 = processor1.getResult();
Iterator iterator = myList.iterator();
director.makeProcessor(
iterator, processor2);
result2 = processor2.getResult();
如此一來,ProcessDirector 負責迴圈控制,而實現 LoopProcessor 的 class 負責迴圈內的資料處理,呼叫者只要呼叫 ProcessDirector 的 makeProcess,並傳入資料項目之聚合體及迴圈處理的 LoopProcessor,最後再從傳入的 LoopProcessor 實作取得資料處理結果即可,程式碼將變得更為簡潔而易讀,同時要增加不同方式的資料處理並不會影響到既有之資料處理或迴圈控制的部分。
這便是應用 GOF 的 builder 及 iterator design pattern,將迴圈控制與資料處理的概念區隔開來,讓程式碼的結構更具有彈性與穩定性。把程式碼組織起來,它們將自然形成一個充滿驚奇及富有彈性的世界,並為軟體注入鮮活的生命力,而在演化過程中,我們用參與調適取代控制預測,也讓自已不斷地成長與接受改變,我想這是軟體開發最饒富趣味的過程吧。