冒號和他的學(xué)生們
25.軟件應(yīng)變
潛其心能觀天下之理,定其心能應(yīng)天下之變 ——《呂坤·呻吟語》
第七課剛一開堂,冒號就提了一個問題:“如果把一個Java程序中所有的private關(guān)鍵字換成public,請問該程序還能工作嗎?”
“應(yīng)該還能工作,除非——此前不能工作。”問號小心翼翼地回答。
冒號接著問:“既然如此,何必費(fèi)事區(qū)分它們呢?”
嘆號嘴一撇:“當(dāng)然是為了信息隱藏啰。”
冒號步步緊逼:“隱藏什么信息呢?又為什么要隱藏?”
嘆號應(yīng)對:“對象的狀態(tài)需要隱藏。如果一個對象的狀態(tài)直接暴露在外,讓客戶隨意修改,可能會破壞對象的內(nèi)在邏輯。”
冒號依舊窮追不舍:“那為什么對象的方法有些也需要隱藏?”
“以前我也有此疑問,看別人代碼時(shí)最感興趣的就是那些私有方法。”引號不打自招。
逗號逗他:“看來你患有偷窺癖哦。”
引號暗暗踢了逗號一腳:“現(xiàn)在我明白了,這是為了實(shí)現(xiàn)數(shù)據(jù)抽象,將接口與實(shí)現(xiàn)分離開來。”
冒號仍不罷休:“這種抽象究竟有何實(shí)際好處?”
句號搶答:“一方面,抽象接口描述了一個類最本質(zhì)的行為特征;另一方面,具體實(shí)現(xiàn)隨時(shí)可能變動,隱藏它們可以保證這種變動不會波及客戶代碼。”
“說到點(diǎn)子上了!”冒號終于停止了追問,“軟件與硬件之別,不僅是無形與有形之別,更是變化與固化之別。所謂變化,指源代碼隨時(shí)可能因需而變。一個軟件修改維護(hù)的時(shí)間通常會超過編寫時(shí)間,越復(fù)雜越成熟的程序越是如此。軟件的難點(diǎn)有二:其一是邏輯的復(fù)雜,其二是需求的變化。許多程序員看重前者而看輕后者,大部分時(shí)間花在尋求解決方案上,而不是在選擇解決方案上。他們目眩于奇技淫巧卻不解大巧若拙之妙,殊不知充滿技巧的代碼不僅難于理解而易于出錯,且因其普適性低而受變化的沖擊更大。眾所周知,比武時(shí)最忌招式用老,老即難以變化,一旦為對手看破則后果不堪設(shè)想。同樣,動不動凌空躍起只是影視作品中招徠眼球的花哨場面,實(shí)戰(zhàn)中很少出現(xiàn),蓋因空中不易變招。當(dāng)然凡事皆有度,無一招用老,便無一招用實(shí),難以完成致命一擊。反映在軟件上,那就是過度設(shè)計(jì)會帶來不必要的復(fù)雜和效率損失。”
眾人均想,又上起久違的武術(shù)課了。
冒號滔滔不絕:“一言以蔽之,軟件之軟,體現(xiàn)在適應(yīng)變化的能力。許多編程設(shè)計(jì)思想包括OOP的思想都是以此為主題的,抽象與封裝便是典型代表。抽象一個對象模型即是將一類對象最本質(zhì)因而最不易變化的部分提煉出來,而封裝——準(zhǔn)確地說是信息隱藏——則是將非本質(zhì)、容易變化的部分隱藏起來,從而將一個類劃分為陰陽兩面。由于變化多發(fā)生在陰面,對外是屏蔽的,因此修改該面毫無累及客戶之憂,由此提高了軟件的抗變能力。有些人誤認(rèn)為信息隱藏是出于軟件安全(security)的考慮,實(shí)乃是似是而非的皮相之見。”
問號提問:“軟件的變化主要有哪些?”
“軟件的變化大致分兩種:一種是出于內(nèi)在需求而作的結(jié)構(gòu)性變化,通常以改善軟件質(zhì)量為目的,即所謂的重構(gòu)(refactoring);一種是出于外在需求而作的功能性變化,通常以滿足客戶需要為目的。理想的抽象與封裝,應(yīng)能完全避免第一類變化對于客戶代碼的影響,也能最大限度地降低第二類變化的副作用。只是知易行難,為細(xì)微的變化而付出巨大代價(jià)的例子比比皆是。‘千年蟲’就是一個最典型的例子,而當(dāng)32 位的IPv4 全部換成128位的IPv6 ,其代價(jià)也不遑多讓。從中可以看出,信息隱藏,尤其是結(jié)構(gòu)性信息隱藏是多么的重要!下面看一個簡單的例子。”冒號打開幻燈片——
// 用直角坐標(biāo)實(shí)現(xiàn)的復(fù)數(shù)類
public class Complex



{

private double x;

private double y;

public Complex(double x, double y)


{

this.x = x;

this.y = y;

}


public double real()
{ return x; }


public double imaginary()
{ return y; }


public double modulus()
{ return StrictMath.hypot(x, y); }


public double argument()
{ return StrictMath.atan2(y, x); }

public Complex add(Complex other)


{

return new Complex(x + other.x, y + other.y);

}

public Complex multiply(Complex other)


{

return new Complex(x * other.x - y * other.y,

x * other.y + y * other.x);

}

}
“這是一個用直角坐標(biāo)實(shí)現(xiàn)的復(fù)數(shù)Java類,為簡明起見,僅僅實(shí)現(xiàn)了實(shí)部、虛部、模、輻角、加法和乘法等運(yùn)算。同樣地,我們也可以用極坐標(biāo)來實(shí)現(xiàn)。”冒號投影出另一段代碼——
// 用極坐標(biāo)實(shí)現(xiàn)的復(fù)數(shù)類
public class Complex



{

private double r;

private double theta;

public Complex(double x, double y)


{

r = StrictMath.hypot(x, y);

theta = StrictMath.atan2(y, x);

}


public double real()
{ return r * StrictMath.cos(theta); }


public double imaginary()
{ return r * StrictMath.sin(theta); }


public double modulus()
{ return r; }


public double argument()
{ return theta; }

public Complex add(Complex other)


{

return new Complex

(r * StrictMath.cos(theta) + other.r * StrictMath.cos(other.theta),

r * StrictMath.sin(theta) + other.r * StrictMath.sin(other.theta));

}

public Complex multiply(Complex other)


{

Complex product = new Complex(0, 0);

product.r = r * other.r;

product.theta = theta + other.theta;

return product;

}

}
句號似已深明其意:“這兩個類的接口相同而實(shí)現(xiàn)方式不同,它們的區(qū)別是結(jié)構(gòu)性的,而不是功能性的。就實(shí)現(xiàn)效率而論,直角坐標(biāo)便于加減運(yùn)算,而極坐標(biāo)便于乘除、乘方開方等運(yùn)算。實(shí)現(xiàn)者可能會為采用何種方案而舉棋不定,好在由于隱藏了結(jié)構(gòu)性信息,即使以后修改了實(shí)現(xiàn)方案,也不會影響客戶。”
冒號補(bǔ)充道:“如果將代碼移植到C++,修改了實(shí)現(xiàn)方案,還是可能在一定程度上影響客戶的。”
嘆號有些驚訝:“為什么?C++不也是OOP語言嗎?”
冒號解釋:“由于C++需要頭文件,即使私有成員也必須在頭文件中聲明。這意味著改動任何私有數(shù)據(jù)結(jié)構(gòu)甚至私有方法的簽名,所有包含該頭文件的源代碼雖不必改寫,卻需要重新編譯鏈接。這對大型程序來說通常是難以忍受的,同時(shí)也說明設(shè)計(jì)與語言息息相關(guān)的。如果一個設(shè)計(jì)者只是高高在上,完全不考慮語言細(xì)節(jié),難免流于紙上談兵。”
逗號問道:“為什么Java不需要頭文件呢?”
“因?yàn)?/span>Java、C#包括D語言中類似頭文件的信息,已經(jīng)在編譯時(shí)自動提取并保存了。”冒號道出緣由,“出于歷史原因和效率上的考慮,C++仍沿用C的頭文件用法,成為除指針和內(nèi)存管理之外最令人頭痛的問題。因此在C++中應(yīng)盡可能地使用前置聲明(forward declaration),減少包含的(included)頭文件。另外,可以將一些私有靜態(tài)(private static)成員從頭文件轉(zhuǎn)移到實(shí)現(xiàn)代碼中,以匿名命名空間(anonymous namespace)的方式來實(shí)現(xiàn)完全隱藏。此外還有一個非常有用的技巧——柄/體(handle/body)模式或稱橋梁模式(bridge pattern),可以將接口與實(shí)現(xiàn)完全分開。這種模式不僅可以解決C++中的頭文件問題,對Java等不需要頭文件的語言也是有用的。下面我們用這種模式重新實(shí)現(xiàn)Complex類。”
幻燈一閃,新的源碼出現(xiàn)在眾人眼前——
// 復(fù)數(shù)計(jì)算接口ComplexImpl

public interface ComplexImpl



{

public double real();

public double imaginary();

public double modulus();

public double argument();

public Complex add(Complex other);

public Complex multiply(Complex other);

}

// 用直角坐標(biāo)實(shí)現(xiàn)的ComplexImpl

public class ComplexCartesianImpl implements ComplexImpl



{

private double x;

private double y;

public ComplexCartesianImpl(double x, double y)


{

this.x = x;

this.y = y;

}


public double real()
{ return x; }


public double imaginary()
{ return y; }


public double modulus()
{ return StrictMath.hypot(x, y); }


public double argument()
{ return StrictMath.atan2(y, x); }

public Complex add(Complex other)


{

return new Complex(x + other.real(), y + other.imaginary());

}

public Complex multiply(Complex other)


{

return new Complex(x * other.real() - y * other.imaginary(),

x * other.imaginary() + y * other.real());

}

}

// 用極坐標(biāo)實(shí)現(xiàn)的ComplexImpl

public class ComplexPolarImpl implements ComplexImpl



{

private double r;

private double theta;

// 以下省略。。。

}

// 用橋梁模式實(shí)現(xiàn)的復(fù)數(shù)類

public class Complex



{

private ComplexImpl impl;

public Complex(double x, double y)


{

impl = new ComplexCartesianImpl(x, y);

//或者:impl = new ComplexPolarImpl(x, y);

}


public double real()
{ return impl.real(); }


public double imaginary()
{ return impl.imaginary(); }


public double modulus()
{ return impl.modulus(); }


public double argument()
{ return impl.argument(); }


public Complex add(Complex other)
{ return impl.add(other); }


public Complex multiply(Complex other)
{ return impl.multiply(other); }

}
冒號進(jìn)而指出:“這是橋梁模式的簡化版。稍加改進(jìn),我們不僅可以在編譯期間決定具體實(shí)現(xiàn)方式,甚至可以讓客戶在運(yùn)行期間選擇實(shí)現(xiàn)方式。你們課后不妨試試。”
引號一拍大腿:“妙!如此既免除了實(shí)現(xiàn)者抉擇的煩惱,也給賦予使用者更大的自由,可謂一舉兩得啊。”
句號也道:“信息隱藏雖能將抽象接口與具體實(shí)現(xiàn)分離,但仍然封裝在同一類中。橋梁模式則讓二者徹底解耦(decouple),增強(qiáng)了對變化的適應(yīng)力,具有更大的靈活性和可擴(kuò)展性。”
“當(dāng)然這也增加了一定的復(fù)雜性和效率上的損失,具體運(yùn)用時(shí)應(yīng)酌情考量,避免過度設(shè)計(jì)。”冒號提醒道,“最后,如果Complex類需要功能上的變化,比如增加乘方、開方等運(yùn)算,只要不修改現(xiàn)有運(yùn)算的簽名,是不會傷及客戶代碼的。”