未利用封裝
客戶代碼使用顯式類型檢查(使用一系列if-else或switch語句檢查對象的類型),而不利用出層次結構內已封裝的類型變化時,將導致這種壞味。
為什么要利用封裝?
一種臭名昭著的壞味是,在客戶代碼中使用條件語句(if-else或switch語句)來顯式地檢查類型,並根據類型執行相應的操作。我們這里討論的是:要檢查的類型都封裝在了層次結構中,但沒有利用這一點,即使用顯式類型檢查,而不依賴於動態多態性。這將導致如下問題:
- 顯式類型檢查讓客戶程序和具體類型緊密耦合,降低了設計的可維護性。例如,引入新類型后,必須修改客戶程序,在其中檢查新類型以及執行相應操作的代碼。
- 客戶程序必須顯式地檢查層次結構中所有相關的類型。如果未檢查一個或多個這樣的類型,客戶程序在運行階段可能出現意外的行為。相反,如果利用了運行時多態,完全可以避免這種問題。
未利用封裝潛在的原因
以過程型思維使用面向對象語言
開發時的思維是以代碼執行過程為導向,自然而然就會使用if-else語句和switch語句。
未應用面向對象原則
無力將面向對象的概念付諸實踐。
示例分析一
根為抽象類DataBuffer的層次結構封裝了各種基本數據結構型數組,DataBuffer的子類DataBufferByte、DataBufferUShort、DataBufferInt支持相應的基本數據類型數組。DataBuffer定義了常量TYPE_BYTE、TYPE_USHORT、TYPE_INT。客戶程序使用TYPE_BYTE、TYPE_USHORT、TYPE_INT的DataBuffer來存儲數據。
下面是客戶程序的示例,演示如何使用switch語句執行針對具體類型的顯式類型檢查。
switch (transferType)
{
case DataBuffer.TYPE_BYTE:
byte[] bdata = (byte[])inData;
pixel = bdata[0] & 0xff;
length = bdata.Length;
break;
case DataBuffer.TYPE_USHORT:
short[] sdata = (short[])inData;
pixel = sdata[0] & 0xffff;
length = sdata.Length;
break;
case DataBuffer.TYPE_INT:
int[] idata = (int[])inData;
pixel = idata[0];
length = idata.Length;
break;
default:
throw new Exception("不支持的transferType");
}
上面代碼使用的數據成員transferType定義如下:
protected int transferType;
重構建議:將決定行為的條件語句刪除,並在層次結構中引入多態方法。
在客戶程序中,提供合適的DataBuffer子類對象。在DataBuffer層次結構類型中,定義方法GetPixel()和GetLengthl()。
public abstract class DataBuffer
{
public const int TYPE_BYTE = 1;
public const int TYPE_DOUBLE = 2;
public const int TYPE_FLOAT = 3;
public const int TYPE_INT = 4;
public const int TYPE_USHORT = 5;
public abstract int GetPixel(object inData);
public abstract int GetLength(object inData);
}
public class DataBufferInt: DataBuffer
{
public override int GetPixel(object inData)
{
int[] idata = (int[])inData;
return idata[0];
}
public override int GetLength(object inData)
{
int[] idata = (int[])inData;
return idata.Length;
}
}
public class DataBufferByte : DataBuffer
{
public override int GetPixel(object inData)
{
byte[] bdata = (byte[])inData;
return bdata[0] & 0xff;
}
public override int GetLength(object inData)
{
byte[] bdata = (byte[])inData;
return bdata.Length;
}
}
public class DataBufferUShort : DataBuffer
{
public override int GetPixel(object inData)
{
short[] sdata = (short[])inData;
return sdata[0] & 0xffff;
}
public override int GetLength(object inData)
{
short[] sdata = (short[])inData;
return sdata.Length;
}
}
並將客戶程序switch語句及其case語句簡化為:
int pixel = GetPixel(inData);
int length = GetLength(inData);
由於引用dataBuffer指向的是傳入的DataBuffer子類對象,因此上述語句將調用相應子類的GetPixel()和GetLength()方法。這里需要注意的是客戶程序代碼提供特定DataBuffer子類對象,檢查輸入數據類型和創建DataBuffer子類對象的工作由客戶程序負責。可能需要在客戶代碼或一個工廠類中使用switch-case語句,而只需要使用一次這個switch-case語句。由於客戶程序不知道具體是哪個DataBuffer子類,所以它與DataBuffer層次結構耦合更低。這樣在DataBuffer層次結構修改既有類型和添加新類型時,不會對客戶程序造成影響。即使有影響也是只需要使用一次的這個switch-case語句,修改代碼代價極小。
這讓我想起,我在看完《重構》后天真幼稚的想消除項目中的switch-case語句,只要項目中存在switch-case語句我就覺得存在壞味道,此后的一段時間我很痛苦,因為項目中總是存在消滅不了的switch-case語句。其實如果項目中需要與外部世界的實體交互,要避免使用條件邏輯很難。例如用戶在頁面的操作在代碼中肯定對應不同的對象來處理,這中間必須使用條件邏輯判斷使用哪個對象處理。但是這樣的判斷應該只有一處,負責日后的代碼維護是個災難。
示例分析二
還是那句話switch-case語句和if-else語句不可怕,可怕的是多個witch-case語句和if-else語句。
對於這樣的代碼我們要給予充分的關注:
代碼1:
if(obj is XXX)
{
//做事情A
}
if(obj is YYY)
{
//做事情B
}
if(obj is ZZZ)
{
//做事情C
}
代碼2:
if(obj is XXX)
{
//做事情A
}
if(obj is YYY)
{
//做事情B
}
if(obj is ZZZ)
{
//做事情C
}
代碼3:
if(obj is XXX)
{
//做事情A
}
if(obj is YYY)
{
//做事情B
}
if(obj is ZZZ)
{
//做事情C
}
這樣的代碼是難以擴展的,新增一個類NNN,就需要找到代碼1、2、3甚至n進行修改,很容易遺漏。而且遺漏造成的錯誤只用在代碼運行階段才能發現。
這種情況反映出來的問題就是沒有利用封裝,已經有了層次結構,卻沒有予以利用。沒有面向接口編程,每個地方面向的都是具體的實現類,每個地方都需要判斷實例的類型才可以進行下一步的動作。
進行重構:
代碼1:
obj.DoSomething1();
代碼2:
obj.DoSomething2();
代碼3:
obj.DoSomething3();
obj可以是XXX、YYY、ZZZ。對於現在的代碼,新增一個類NNN,代碼1、2、3甚至n處根本不需要任何改動。因為它們實現了統一的接口,並且符合開閉原則。
參考:《軟件設計重構》
