有一天,有人也許運(yùn)行這個代碼并且注意到Stack沒有運(yùn)行的如想象的那么快,并且能夠在重負(fù)荷下使用。你能夠重寫Stack,以至于它不用ArrayList并且繼續(xù)提高Stack的效率。這是新的傾向的和有意義的版本:
class Stack
{
private int stack_pointer = -1;
private Object[] stack = new Object[1000];
public void push( Object article )
{
assert stack_pointer < stack.length;
stack[ ++stack_pointer ] = article;
}
public Object pop()
{
assert stack_pointer >= 0;
return stack[ stack_pointer-- ];
}
public void push_many( Object[] articles )
{
assert ( stack_pointer + articles.length )
注意到push_many不再多次調(diào)用push()—它做塊傳輸。新的Stack運(yùn)行正常;事實(shí)上,比前一個版本更好。不幸的是,派生類Monitorable_stack不再運(yùn)行,因?yàn)槿绻鹥ush_many()被調(diào)用,它不正確的跟蹤堆棧的使用(push()的派生類版本不再通過繼承的push_many()方法調(diào)用,所以push_many()不再更新high_water_mark)。Stack是一個脆弱的類。與關(guān)閉它一樣,事實(shí)上不可能通過小心來消滅這些類型的錯誤。
注意如果你用接口繼承,你就沒有這個問題,因?yàn)槟銢]有繼承對你有害的函數(shù)。如果Stack是接口,由Simple_stack和Monitorable_stack實(shí)現(xiàn),那么代碼就是更加健壯的。
我提供了一個基于接口的方法在Listing 0.1。這個解決方法和繼承實(shí)現(xiàn)的方法一樣的靈活:你能夠用Stack抽象術(shù)語來寫代碼而不必?fù)?dān)心你事實(shí)上在操作那種具體的堆棧。因?yàn)閮蓚€實(shí)現(xiàn)必須提供公共接口的所有東西,它很難使事情變糟。我仍然有和寫基類的代碼一樣的只寫一次,因?yàn)槲矣梅庋b而不是繼承。在底層,我不得不通過封裝類中的瑣碎的訪問器方法來訪問缺省的實(shí)現(xiàn)。(例如,Monitorable_Stack.push(…)(在41行)不得不調(diào)用在Simple_stack等價的方法).程序員埋怨寫所有這些行,但是寫這特別行代碼同消除重要的潛在bug是非常小的成本。
Listing 0.1. 用接口消除脆弱基類
1 import java.util.*;
2
3 interface Stack
4 {
5 void push( Object o );
6 Object pop();
7 void push_many( Object[] source );
8 }
9
10 class Simple_stack implements Stack
11 { private int stack_pointer = -1;
12 private Object[] stack = new Object[1000];
13
14 public void push( Object o )
15 { assert stack_pointer < stack.length;
16
17 stack[ ++stack_pointer ] = o;
18 }
19
20 public Object pop()
21 { assert stack_pointer >= 0;
22
23 return stack[ stack_pointer-- ];
24 }
25
26 public void push_many( Object[] source )
27 { assert (stack_pointer + source.length) < stack.length;
28
29 System.arraycopy(source,0,stack,stack_pointer+1,
source.length);
30 stack_pointer += source.length;
31 }
32 }
33
34
35 class Monitorable_Stack implements Stack
36 {
37 private int high_water_mark = 0;
38 private int current_size;
39 Simple_stack stack = new Simple_stack();
40
41 public void push( Object o )
42 { if( ++current_size > high_water_mark )
43 high_water_mark = current_size;
44 stack.push(o);
45 }
46
47 public Object pop()
48 { --current_size;
49 return stack.pop();
50 }
51
52 public void push_many( Object[] source )
53 {
54 if( current_size + source.length > high_water_mark )
55 high_water_mark = current_size + source.length;
56
57 stack.push_many( source );
58 }
59
60 public int maximum_size()
61 { return high_water_mark;
62 }
63 }
64
框架(Frameworks)
沒有提到基于框架編程,那使對于脆弱的基類的討論是不完整的。諸如Microsoft Foundation Classes(MFC)的基類已經(jīng)成為建立類庫的流行途徑。盡管MFC本身正在神圣的隱退,但是MFC的基口已經(jīng)是根深蒂固,而這無關(guān)于Microsoft在那終止,程序員會一直認(rèn)為Microsoft的方法是最好的方法。
一個基于框架的系統(tǒng)典型的使用半成品的類的構(gòu)成庫開始,這些類不做任何需要做的事,而是依賴于派生類來提供需要的功能。在Java中,一個好的例子就是組件的paint()方法,它是一個有效的占位者;一個派生類必須提供真正的版本。
你能夠適度的多國一些東西,但是一個基于定制的派生類的完整的類框架是非常脆弱的。基類是太脆弱了。當(dāng)我們用MFC編程時,每次Microsoft公布新版本時,我不得不重寫我的應(yīng)用。這些代碼將經(jīng)常編譯,但是由于一些基類的改變,它們不能運(yùn)行。
所有提供的Java包工作的非常好。為了使它們運(yùn)行,你不需要擴(kuò)展任何東西。這個已經(jīng)提供的結(jié)構(gòu)比派生類的框架結(jié)構(gòu)更好。它容易維護(hù)和使用,并且如果Sun Microsystems提供的類改變了它的實(shí)現(xiàn),也不會使你的代碼處在危險中。
總結(jié)脆弱的基類
一般,最好避開具體基礎(chǔ)類和extends關(guān)系,而用接口和implements關(guān)系。我的處理規(guī)則是,在我的至少80%的代碼中完全用接口來完成。例如,我從不用對HashMap的引用;我用對Map接口的引用。(我對interface這個字不是嚴(yán)格的。當(dāng)你看怎樣用接口的時候,InputStream是一個好的接口,盡管它在Java中是作為抽象類來實(shí)現(xiàn)的。)
你增加的越抽象,就越靈活。在今天的商業(yè)環(huán)境下,需求隨著程序開發(fā)而改變,靈活就是最主要的。而且靈敏編程中的大多數(shù)只有代碼使用抽象來寫才會很好的運(yùn)行。
如果你近距離的檢查四人幫的模式,你將看到這些模式中的很多是提供方法消除實(shí)現(xiàn)繼承,而最好用接口繼承,并且大多數(shù)模式的共有特征是用接口繼承。這個重要事實(shí)是我們開始時提到的:模式是發(fā)現(xiàn)而不是發(fā)明。模式的出現(xiàn)是當(dāng)你發(fā)現(xiàn)寫得很好,易維護(hù)的運(yùn)行代碼時。它講的是這些寫得好的,易維護(hù)的代碼根本的避開了實(shí)現(xiàn)繼承。
這個文章是從我即將出版的書,暫時命名為《Holub on Patterns:Learning Design Patterns by Looking at Code》,將有Apress在今年秋季出版。