缺失封裝
沒有將實現變化封裝在抽象和層次結構中時,將導致這種壞味。
表現形式通常如下:
- 客戶程序與其需要的服務變種緊密耦合,每當需要支持新變種或修改既有變種時,都將影響客戶程序。
- 每當需要在層次結構中支持新變種時,都添加了大量不必要的類,這增加了設計的復雜度。
為什么不能缺失封裝?
開閉原則(OCP)指出,類型應對擴展開放,對修改關閉。也就是說應該通過擴展(而不是修改)來改變類型的行為。沒有在類型或層次結構中封裝實現變化時,便違反了OCP。
缺失封裝潛在的原因
未意識到關注點會不斷變化
沒有預測到關注點可能發生變化,進而沒有在設計中正確封裝這些關注點。
混合關注點
將彼此獨立的各個關注點聚合在一個層次結構中,而不是分開時,如果關注點發生變化,可能導致類的數量呈爆炸式增長。
幼稚的設計決策
采用過於簡單的方法,如為每種變化組合創建一個類時,可能導致設計無謂的復雜。
示例分析一
假設有一個Entryption類,它需要使用加密算法對數據進行加密。可供選擇的加密算法有很多,包括DES(數據加密標准)、AES(高級加密標准)、TDES(三重數據加密標准)等。Entryption類使用DES算法對數據進行加密。
public class Encryption
{
/// <summary>
/// 使用DES算法進行加密
/// </summary>
public void Encrypt()
{
// 使用DES算法進行加密
}
}
假設出現了新需求,要求使用AES算法對數據進行加密。
最差的方案出現了:
public class Encryption
{
/// <summary>
/// 使用DES算法進行加密
/// </summary>
public void EncryptUsingDES()
{
// 使用DES算法進行加密
}
/// <summary>
/// 使用AES算法進行加密
/// </summary>
public void EncryptUsingAES()
{
// 使用AES算法進行加密
}
}
這種方案有很多不盡如人意的地方:
- Encryption類變得更大、更難以維護,因為它實現了多種加密算法,但是每次只使用一種。
- 難以添加新算法以及修改既有算法,因為加密算法是Encryption類不可分割的部分。
- 加密算法向Encryption類提供服務,但是與Encryption類緊緊耦合在一起,無法在其它地方重用。
不滿意就重構,首先使用繼承進行重構,會有2種方案可以選擇:
選擇1:
讓Encryption類根據需求繼承AESEncryptionAlgorithm或DESEncryptionAlgorithm類,並提供方法Encrypt()。這種方案帶來的問題是Encryption類在編譯階段就將關聯到特定的加密算法,更嚴重的是類之間的關系並不是is-a關系。
/// <summary>
/// AES算法加密類
/// </summary>
public class AESEncryptionAlgorithm
{
/// <summary>
/// 使用AES算法進行加密
/// </summary>
public void EncryptUsingAES()
{
// 使用AES算法進行加密
}
}
/// <summary>
/// DES算法加密類
/// </summary>
public class DESEncryptionAlgorithm
{
/// <summary>
/// 使用DES算法進行加密
/// </summary>
public void EncryptUsingDES()
{
// 使用DES算法進行加密
}
}
public class Encryption: AESEncryptionAlgorithm
{
/// <summary>
/// 使用算法進行加密
/// </summary>
public void Encrypt()
{
EncryptUsingAES();
}
}
選擇2:
創建子類AESEncryption和DESEncryption,它們都擴展了Encryption類,並分別包含加密算法AES和DES的實現。客戶程序可創建Encryption的引用,這些引用指向特定子類的對象。通過添加新的子類,很容易支持新的加密算法。但是這種方案的問題是AESEncryption和DESEncryption將繼承Encryption類的其它方法,降低了加密算法的可重用性。
public abstract class Encryption
{
/// <summary>
/// 使用算法進行加密
/// </summary>
public abstract void Encrypt();
}
/// <summary>
/// AES算法加密類
/// </summary>
public class AESEncryption : Encryption
{
/// <summary>
/// 使用 AES算法進行加密
/// </summary>
public override void Encrypt()
{
// 使用 AES算法進行加密
}
}
/// <summary>
/// DES算法加密類
/// </summary>
public class DESEncryption : Encryption
{
/// <summary>
/// 使用 DES算法進行加密
/// </summary>
public override void Encrypt()
{
// 使用 DES算法進行加密
}
}
最佳的選擇是使用策略模式:
- 可在運行階段給Encryption對象配置特定的加密算法
- 可在其它地方重用層次結構EncryptionAlgorithm中定義的算法
- 很容易根據需要支持新的算法
/// <summary>
/// 算法加密接口
/// </summary>
public interface EncryptionAlgorithm
{
void Encrypt();
}
/// <summary>
/// DES算法加密類
/// </summary>
public class DESEncryptionAlgorithm : EncryptionAlgorithm
{
public void Encrypt()
{
//使用 DES算法進行加密
}
}
/// <summary>
/// AES算法加密類
/// </summary>
public class AESEncryptionAlgorithm : EncryptionAlgorithm
{
public void Encrypt()
{
//使用 AES算法進行加密
}
}
public class Encryption
{
private EncryptionAlgorithm algo;
public Encryption(EncryptionAlgorithm algo)
{
this.algo = algo;
}
/// <summary>
/// 使用算法進行加密
/// </summary>
public void Encrypt()
{
algo.Encrypt();
}
}
示例分析二
支持使用不同算法(DES和AES)對各種內容(Image和Text)進行加密的設計。
最簡單最直觀的的設計:
在這個設計中,有兩個變化點:支持的內容類型和加密算法類型。對於這兩個變化點的每種可能組合,都使用了一個類來表示。這樣會有一個嚴重的問題:假設現在要求支持新加密算法TDES和新內容類型Data,類的數量呈爆炸性增長。因為變化點混在了一起,沒有分別進行封裝。
使用橋接模式進行封裝:
使用橋接模式,分別封裝這兩個關注點的變化。現在要引入新內容類型Data和新加密算法TDES,只需要添加兩個新類。既解決了類數量呈爆炸增長的問題,又增加了根為接口EncryptionAlgorithm層次結構中的加密算法的可重用性。
總結
-
不相關的關注點混在一起,抽象將變得難以重用。
-
對業務中可能的變化點,要給予擴展點,保證開閉原則(OCP),對擴展開放,對修改關閉。
參考:《軟件設計重構》
