- 你必須知道的.NET(第2版)
- 王濤
- 5662字
- 2018-12-27 13:41:48
1.3 封裝的秘密
本節(jié)將介紹以下內(nèi)容:
·面向?qū)ο蟮姆庋b特性
·字段賞析
·屬性賞析
1.3.1 引言
在面向?qū)ο笕刂校庋b特性為程序設(shè)計(jì)提供了系統(tǒng)與系統(tǒng)、模塊與模塊、類與類之間交互的實(shí)現(xiàn)手段。封裝為軟件設(shè)計(jì)與開發(fā)帶來前所未有的革命,成為構(gòu)成面向?qū)ο蠹夹g(shù)最為重要的基礎(chǔ)之一。在.NET中,一切看起來都已經(jīng)被包裝在.NET Framework這一復(fù)雜的網(wǎng)絡(luò)中,提供給最終開發(fā)人員的是成千上萬的類型、方法和接口,而Framework內(nèi)部一切已經(jīng)做好了封裝。例如,如果你想對(duì)文件進(jìn)行必要的操作,那么使用System.IO.File基本就能夠滿足多變的需求,因?yàn)?NET Framework已經(jīng)把對(duì)文件的重要操作都封裝在System.IO.File等一些基本類中,用戶不需要關(guān)心具體的實(shí)現(xiàn)。
1.3.2 讓ATM告訴你,什么是封裝
那么,封裝究竟是什么?
首先,我們考察一個(gè)常見的生活實(shí)例來進(jìn)行說明,例如每當(dāng)發(fā)工資的日子小王都來到ATM機(jī)前,用工資卡取走一筆錢為女朋友買禮物,從這個(gè)很帥的動(dòng)作,可以得出以下的結(jié)論:
·小王和ATM機(jī)之間,以銀行卡進(jìn)行交互。要取錢,請(qǐng)交卡。
·小王并不知道ATM機(jī)將錢放在什么地方,取款機(jī)如何計(jì)算錢款,又如何通過銀行卡返回小王所要數(shù)目的錢。對(duì)小王來說,ATM就是一個(gè)黑匣子,只能等著取錢;而對(duì)銀行來說,ATM機(jī)就像銀行自己的一份子,是安全、可靠、健壯的員工。
·小王要想取到自己的錢,必須遵守ATM機(jī)的對(duì)外約定。他的任何違反約定的行為都被視為不軌,例如欲以磚頭砸開取錢,用公交卡冒名取錢,盜卡取錢都將面臨法律風(fēng)險(xiǎn),所以小王只能安分守己地過著月光族的日子。
那么小王和ATM機(jī)的故事,能給我們什么樣的啟示?對(duì)應(yīng)上面的3條結(jié)論,我們的分析如下:
·小王以工資卡和ATM機(jī)交互信息,ATM機(jī)的入卡口就是ATM機(jī)提供的對(duì)外接口,磚頭是塞不進(jìn)去的,公交卡放進(jìn)去也沒有用。
·ATM機(jī)在內(nèi)部完成身份驗(yàn)證、余額查詢、計(jì)算取款等各項(xiàng)服務(wù),具體的操作對(duì)用戶小王是不可見的,對(duì)銀行來說這種封閉的操作帶來了安全性和可靠性保障。
·小王和ATM機(jī)之間遵守了銀行規(guī)定、國(guó)家法律這樣的協(xié)約。這些協(xié)約和法律,就掛在ATM機(jī)旁邊的墻上。
結(jié)合前面的示例,再來分析封裝吧。具體來說,封裝隱藏了類內(nèi)部的具體實(shí)現(xiàn)細(xì)節(jié),對(duì)外則提供統(tǒng)一訪問接口,來操作內(nèi)部數(shù)據(jù)成員。這樣實(shí)現(xiàn)的好處是實(shí)現(xiàn)了UI分離,程序員不需要知道類內(nèi)部的具體實(shí)現(xiàn),只需按照接口協(xié)議進(jìn)行控制即可。同時(shí)對(duì)類內(nèi)部來說,封裝保證了類內(nèi)部成員的安全性和可靠性。在上例中,ATM機(jī)可以看做封裝了各種取款操作的類,取款、驗(yàn)證的操作對(duì)類ATM來說,都在內(nèi)部完成。而ATM類還提供了與小王交互的統(tǒng)一接口,并以文檔形式——法律法規(guī),規(guī)定了接口的規(guī)范與協(xié)定來保證服務(wù)的正常運(yùn)行。以面向?qū)ο蟮恼Z(yǔ)言來表達(dá),類似于下面的樣子:
namespace InsideDotNet.OOThink.Encapsulation { /// <summary> /// ATM類 /// </summary> public class ATM { #region 定義私有方法,隱藏具體實(shí)現(xiàn) private Client GetUser(string userID) {} private bool IsValidUser(Client user) {} private int GetCash(int money) {} #endregion #region 定義公有方法,提供對(duì)外接口 public void CashProcess(string userID, int money) { Client tmpUser = GetUser(userID); if (IsValidUser(tmpUser)) { GetCash(money); } else { Console.Write("你不是合法用戶,是不是想被發(fā)配南極?"); } } #endregion } /// <summary> /// 用戶類 /// </summary> public class Client { } }
在.NET應(yīng)用中,F(xiàn)ramework封裝了你能想到的各種常見的操作,就像微軟提供給我們一個(gè)又一個(gè)功能不同的ATM機(jī)一樣,而程序員手中籌碼就是根據(jù).NET規(guī)范進(jìn)行開發(fā),是否能取出自己的錢,要看你的卡是否合法。
那么,如果你是銀行的主管,又該如何設(shè)計(jì)自己的ATM呢?該以什么樣的技術(shù)來保證自己的ATM在內(nèi)部隱藏實(shí)現(xiàn),對(duì)外提供接口呢?
1.3.3 秘密何處:字段、屬性和方法
字段、屬性和方法,是面向?qū)ο蟮幕靖拍钪唬浠镜母拍罱榻B不是本書的范疇,任何一本關(guān)于語(yǔ)言和面向?qū)ο蟮闹髦卸加邢嚓P(guān)的詳細(xì)解釋。本書關(guān)注的是在類設(shè)計(jì)之初應(yīng)該基于什么樣的思路,來實(shí)現(xiàn)類的功能要求與交互要求?每個(gè)設(shè)計(jì)者,是以什么角度來完成對(duì)類架構(gòu)的設(shè)計(jì)與規(guī)劃呢?在我看來,下面的問題是應(yīng)該首先被列入討論的選項(xiàng):
·類的功能是什么?
·哪些是字段,哪些是屬性,哪些是方法?
·對(duì)外提供的公有方法有哪些,對(duì)內(nèi)隱藏的私有變量有哪些?
·類與類之間的關(guān)系是繼承還是聚合?
這些看似簡(jiǎn)單的問題,卻往往是困擾我們進(jìn)行有效設(shè)計(jì)的關(guān)鍵因素,通常系統(tǒng)需求描述的核心名詞,可以抽象為類,而對(duì)這些名詞驅(qū)動(dòng)的動(dòng)作,可以對(duì)應(yīng)地抽象為方法。當(dāng)然,具體的設(shè)計(jì)思路要根據(jù)具體的需求情況,在整體架構(gòu)目標(biāo)的基礎(chǔ)上進(jìn)行有效的篩選、剝離和抽象。取舍之間,彰顯OO智慧與設(shè)計(jì)模式的魅力。
那么,了解這些選項(xiàng)與原則,我們就不難理解關(guān)于字段、屬性和方法的實(shí)現(xiàn)思路了,這些規(guī)則可以從對(duì)字段、屬性和方法的探索中找到痕跡,然后從反方向來完善我們對(duì)于如何設(shè)計(jì)的思考與理解。
1.字段
字段(field)通常定義為private,表示類的狀態(tài)信息。CLR支持只讀和讀寫字段。值得注意的是,大部分情況下字段都是可讀可寫的,只讀字段只能在構(gòu)造函數(shù)中被賦值,其他方法不能改變只讀字段。常見的字段定義為:
public class Client { private string name; //用戶姓名 private int age; //用戶年齡 private string password; //用戶密碼 }
如果以public表示類的狀態(tài)信息,則我們就可以以類實(shí)例訪問和改變這些字段內(nèi)容,例如:
public static void Main() { Client xiaoWang = new Client(); xiaoWang.name = "Xiao Wang"; xiaoWang.age = 27; xiaoWang.password = "123456" }
這樣看起來并沒有帶來什么問題,Client實(shí)例通過操作公有字段很容易達(dá)到存取狀態(tài)信息的目的,然而封裝原則告訴我們:類的字段信息最好以私有方式提供給類的外部,而不是以公有方式來實(shí)現(xiàn),否則不適當(dāng)?shù)牟僮鲗⒃斐刹槐匾腻e(cuò)誤方式,破壞對(duì)象的狀態(tài)信息,數(shù)據(jù)安全性和可靠性無法保證。例如:
xiaoWang.age = 1000; xiaoWang.password = "5&@@Ld;afk99";
顯然,小王的年齡不可能是1000歲,他是人不是怪物;小王的密碼也不可能是“@&;”這些特殊符號(hào),因?yàn)锳TM機(jī)上根本沒有這樣的按鍵,而且密碼必須是6位。所以對(duì)字段公有化的操作,會(huì)引起對(duì)數(shù)據(jù)安全性與可靠性的破壞,封裝的第一個(gè)原則就是:將字段定義為private。
那么,如上文所言,將字段設(shè)置為private后,對(duì)對(duì)象狀態(tài)信息的控制又該如何實(shí)現(xiàn)呢?小王的狀態(tài)信息必須以另外的方式提供給類外部訪問或者改變。同時(shí)我們也期望除了實(shí)現(xiàn)對(duì)數(shù)據(jù)的訪問,最好能加入一定的操作,達(dá)到數(shù)據(jù)控制的目的。因此,面向?qū)ο笠肓肆硪粋€(gè)重量級(jí)的概念:屬性。
2.屬性
屬性(property)通常定義為public,表示類的對(duì)外成員。屬性具有可讀、可寫屬性,通過get和set訪問器來實(shí)現(xiàn)其讀寫控制。例如上文中Client類的字段,我們可以相應(yīng)地封裝其為屬性:
public class Client { private string name; //用戶姓名 public string Name { get { return name; } set { name = value == null ? String.Empty : value; } } private int age; //用戶年齡 public int Age { get { return age; } set { if ((value > 0) && (value < 150)) { age = value; } else { throw new ArgumentOutOfRangeException ("年齡信息不正確。"); } } } }
當(dāng)我們?cè)俅我?/p>
xiaoWang.Age = 1000;
這樣的方式來實(shí)現(xiàn)對(duì)小王的年齡進(jìn)行寫控制時(shí),自然會(huì)彈出異常提示,從而達(dá)到了保護(hù)數(shù)據(jù)完整性的目的。
那么,屬性的get和set訪問器怎么實(shí)現(xiàn)對(duì)對(duì)象屬性的讀寫控制呢?我們打開ILDASM工具查看client類反編譯后的情況時(shí),會(huì)發(fā)現(xiàn)如圖1-10所示的情形。

圖1-10 Client類的IL結(jié)構(gòu)
由圖1-10可見,IL中不存在get和set方法,而是分別出現(xiàn)了get_Age、set_Age這樣的方法,打開其中的任意方法分析會(huì)發(fā)現(xiàn),編譯器的執(zhí)行邏輯是:如果發(fā)現(xiàn)一個(gè)屬性,并且查看該屬性中實(shí)現(xiàn)了get還是set,就對(duì)應(yīng)地生成get_屬性名、set_屬性名兩個(gè)方法。因此,我們可以說,屬性的實(shí)質(zhì)其實(shí)就是在編譯時(shí)分別將get和set訪問器實(shí)現(xiàn)為對(duì)外方法,從而達(dá)到控制屬性的目的,而對(duì)屬性的讀寫行為伴隨的實(shí)際是一個(gè)相應(yīng)方法的調(diào)用,它以一種簡(jiǎn)單的形式實(shí)現(xiàn)了方法。
所以我們也可以定義自己的get和set訪問器,例如:
public string get_Password() { return password; } public string set_Password(string value) { if (value.Length < 6) password = value; }
事實(shí)上,這種實(shí)現(xiàn)方法正是Java語(yǔ)言所采用的機(jī)制,而這樣的方式顯然沒有實(shí)現(xiàn)get和set訪問器來得輕便,而且對(duì)屬性的操作也帶來多余的麻煩,所以我們推薦的還是下面的方式:
public string Password { get { return password; } set { if (value.Length < 6) password = value; } }
另外,get和set對(duì)屬性的讀寫控制,是通過實(shí)現(xiàn)get和set的組合來實(shí)現(xiàn)的,如果屬性為只讀,則只實(shí)現(xiàn)get訪問器即可;如果屬性為可寫,則實(shí)現(xiàn)set訪問器即可。
通過對(duì)公共屬性的訪問來實(shí)現(xiàn)對(duì)類狀態(tài)信息的讀寫控制,主要有兩點(diǎn)好處:一是避免了對(duì)數(shù)據(jù)安全的訪問限制,包含內(nèi)部數(shù)據(jù)的可靠性;二是避免了類擴(kuò)展或者修改帶來的變量連鎖反應(yīng)。
至于修改變量帶來的連鎖反應(yīng),表現(xiàn)在對(duì)類的狀態(tài)信息的需求信息發(fā)生變化時(shí),如何來減少代碼重構(gòu)基礎(chǔ)上,實(shí)現(xiàn)最小的損失和最大的補(bǔ)救。例如,如果對(duì)client的用戶姓名由原來的簡(jiǎn)單name來標(biāo)識(shí),換成以firstName和secondName來實(shí)現(xiàn),如果不是屬性封裝了字段而帶來的隱藏內(nèi)部細(xì)節(jié)的特點(diǎn),那么我們?cè)诖a中就要拼命地替換原來xiaoWang.name這樣的實(shí)現(xiàn)了。例如:
private string firstName; private string secondName; public string Name { get { return firstName + secondName; } }
這樣帶來的好處是,我們只需要更改屬性定義中的實(shí)現(xiàn)細(xì)節(jié),而原來程序xiaoWang.name這樣的實(shí)現(xiàn)就不需要做任何修改即可適應(yīng)新的需求。你看,這就是封裝的強(qiáng)大力量使然。
還有一種含參屬性,在C#中稱為索引器(indexer),對(duì)CLR來說并沒有含不含參數(shù)的區(qū)別,它只是負(fù)責(zé)將相應(yīng)的訪問器實(shí)現(xiàn)為對(duì)應(yīng)的方法,不同的是含參屬性中加入了對(duì)參數(shù)的處理過程罷了。
3.方法
方法(method)封裝了類的行為,提供了類的對(duì)外表現(xiàn)。用于將封裝的內(nèi)部細(xì)節(jié)以公有方法提供對(duì)外接口,從而實(shí)現(xiàn)與外部的交互與響應(yīng)。例如,從上面屬性的分析我們可知,實(shí)際上對(duì)屬性的讀寫就是通過方法來實(shí)現(xiàn)的。因此,對(duì)外交互的方法,通常實(shí)現(xiàn)為public。
當(dāng)然不是所有的方法都被實(shí)現(xiàn)為public,否則類內(nèi)部的實(shí)現(xiàn)豈不是全部暴露在外。必須對(duì)對(duì)外的行為與內(nèi)部操作行為加以區(qū)分。因此,通常將在內(nèi)部的操作全部以private方式來實(shí)現(xiàn),而將需要與外部交互的方法實(shí)現(xiàn)為public,這樣既保證了對(duì)內(nèi)部數(shù)據(jù)的隱藏與保護(hù),又實(shí)現(xiàn)了類的對(duì)外交互。例如在ATM類中,對(duì)錢的計(jì)算、用戶驗(yàn)證這些方法涉及銀行的關(guān)鍵數(shù)據(jù)與安全數(shù)據(jù)的保護(hù)問題,必須以private方法來實(shí)現(xiàn),以隱藏對(duì)用戶不透明的操作,而只提供返回錢款這一public方法接口即可。在封裝原則中,有效地保護(hù)內(nèi)部數(shù)據(jù)和有效地暴露外部行為一樣關(guān)鍵。
那么這個(gè)過程應(yīng)該如何來實(shí)施呢?還是回到ATM類的實(shí)例中,我們首先關(guān)注兩個(gè)方法:IsValidUser()和CashProcess(),其中IsValidUser()用于驗(yàn)證用戶的合法性,而CashProcess()用于提供用戶操作接口。顯然,驗(yàn)證用戶是銀行本身的事情,外部用戶無權(quán)訪問,它主要用于在內(nèi)部進(jìn)行驗(yàn)證處理操作,例如CashProcess()中就以IsValidUser()作為方法的進(jìn)入條件,因此很容易知道IsValidUser()被實(shí)現(xiàn)為private。而CashProcess()用于和外部客戶進(jìn)行交互操作,這正是我們反復(fù)強(qiáng)調(diào)的外部接口方法,顯然應(yīng)該實(shí)現(xiàn)為public。其他的方法GetUser()、GetCash()也是從這一主線出發(fā)來確定其對(duì)外封裝權(quán)限的,自然就能找到合理的定位。從這個(gè)過程中我們發(fā)現(xiàn),誰為公有、誰為私有,取決于需求和設(shè)計(jì)雙重因素,在職責(zé)單一原則下為類型設(shè)計(jì)方法,應(yīng)該廣泛考慮的是類本身的功能性,從開發(fā)者與設(shè)計(jì)者兩個(gè)角度出發(fā),分清訪問權(quán)限就會(huì)水到渠成。
1.3.4 封裝的意義
通過對(duì)字段、屬性與方法在封裝性這一點(diǎn)上的分析,我們可以更加明確地了解到封裝特性作為面向?qū)ο蟮娜筇匦灾唬憩F(xiàn)出來的無與倫比的重要性與必要性,對(duì)于深入地理解系統(tǒng)設(shè)計(jì)與類設(shè)計(jì)提供了絕好的切入點(diǎn)。
下面,我們針對(duì)上文的分析進(jìn)行小結(jié),以便更好地理解我們對(duì)于封裝所提出的思考,主要包括:
(1)字段通常定義為private,屬性通常實(shí)現(xiàn)為public,而方法在內(nèi)部實(shí)現(xiàn)為private,對(duì)外部實(shí)現(xiàn)為public,從而保證對(duì)內(nèi)部數(shù)據(jù)的可靠性讀寫控制,保護(hù)了數(shù)據(jù)的安全和可靠,同時(shí)又提供了與外部接口的有效交互。這是類得以有效封裝的基礎(chǔ)機(jī)制。
(2)通常情況下的理解正如我們上面提到的規(guī)則,但是具體的操作還要根據(jù)實(shí)際的設(shè)計(jì)需求而定,例如有些時(shí)候?qū)傩詫?shí)現(xiàn)為private,也將方法實(shí)現(xiàn)為private是更好的選擇。例如在ATM類中,可能需要提供計(jì)數(shù)器來記錄更新或者選擇的次數(shù),而該次數(shù)對(duì)用戶而言是不必要的狀態(tài)信息,因此只需在ATM類內(nèi)部實(shí)現(xiàn)為private即可;同理,類型中的某些方法是對(duì)內(nèi)部數(shù)據(jù)的操作,因此也以private方式來提供,從而達(dá)到數(shù)據(jù)安全的目的。
(3)從內(nèi)存和數(shù)據(jù)持久性角度上來看,有一個(gè)很重要但常常被忽視的事實(shí)是,封裝屬性提供了數(shù)據(jù)持久化的有效手段。因?yàn)椋瑢?duì)象的屬性和對(duì)象一樣在內(nèi)存期間是常駐的,只要對(duì)象不被垃圾回收,其屬性值也將一直存在,并且記錄最近一次對(duì)其更改的數(shù)據(jù)。
(4)在面向?qū)ο笾校庋b的意義還遠(yuǎn)不止類設(shè)計(jì)層面對(duì)字段、屬性和方法的控制,更重要的是其廣義層面。我們理解的封裝,應(yīng)該是以實(shí)現(xiàn)UI分離為目的的軟件設(shè)計(jì)方法,一個(gè)系統(tǒng)或者軟件開發(fā)之后,從維護(hù)和升級(jí)的目的考慮,一定要保證對(duì)外接口部分的絕對(duì)穩(wěn)定。不管系統(tǒng)內(nèi)部的功能性實(shí)現(xiàn)如何多變,保證接口穩(wěn)定是保證軟件兼容、穩(wěn)定、健壯的根本。所以O(shè)O智慧中的封裝性旨在保證:
·隱藏系統(tǒng)實(shí)現(xiàn)的細(xì)節(jié),保證系統(tǒng)的安全性和可靠性。
·提供穩(wěn)定不變的對(duì)外接口。因此,系統(tǒng)中相對(duì)穩(wěn)定部分常被抽象為接口。
·封裝保證了代碼模塊化,提高了軟件的復(fù)用和功能分離。
1.3.5 封裝規(guī)則
現(xiàn)在,我們對(duì)封裝特性的規(guī)則做一個(gè)總結(jié),這些規(guī)則就是在平常的實(shí)踐中提煉與完善出的良藥,我們?cè)谶M(jìn)行實(shí)際的開發(fā)和設(shè)計(jì)工作時(shí),應(yīng)盡量遵守規(guī)則,而不是盲目地尋求方法。
·盡可能地調(diào)用類的訪問器,而不是成員,即使在類的內(nèi)部。其目的在我們的示例中已有說明,例如Client類中的Name屬性就可以避免由于需求變化帶來的代碼更改問題。
·內(nèi)部私有部分可以任意更改,但是一定要在保證外部接口穩(wěn)定的前提下。
·將對(duì)字段的讀寫控制實(shí)現(xiàn)為屬性,而不是方法,否則舍近而求遠(yuǎn),非明智之選。
·類封裝是由訪問權(quán)限來保證的,對(duì)內(nèi)實(shí)現(xiàn)為private,對(duì)外實(shí)現(xiàn)為public。再結(jié)合繼承特性,還要對(duì)protected,internal有較深的理解,詳細(xì)的情況參見1.1節(jié)“對(duì)象的旅行”。
·封裝的精華是封裝變化。張逸在《軟件設(shè)計(jì)精要與模式》一書中指出,封裝變化是面向?qū)ο笏枷氲暮诵模岬介_發(fā)者應(yīng)從設(shè)計(jì)角度和使用角度兩方面來分析封裝。因此,我們將系統(tǒng)中變化頻繁的部分封裝為獨(dú)立的部分,這種隔離選擇有利于充分的軟件復(fù)用和系統(tǒng)柔性。
1.3.6 結(jié)論
封裝是什么?橫掃全文,我們的結(jié)論是:封裝就是一個(gè)包裝,將包裝的內(nèi)外分為兩個(gè)空間,對(duì)內(nèi)實(shí)現(xiàn)數(shù)據(jù)私有,對(duì)外實(shí)現(xiàn)方法調(diào)用,保證了數(shù)據(jù)的完整性和安全性。
我們從封裝的意義談起,然后逐層深入到對(duì)字段、屬性和方法在定義和實(shí)現(xiàn)上的規(guī)則,這是一次自上而下的探求方式,也是一次反其道而行的揭秘旅程。關(guān)于封裝,遠(yuǎn)不是本節(jié)所能全面展現(xiàn)的話題,關(guān)于封裝的技巧和更多深入的探求,來自于面向?qū)ο螅瑏碜杂谠O(shè)計(jì)模式,也來自于軟件工程。因此,要想全面而準(zhǔn)確地認(rèn)識(shí)封裝,除了本節(jié)打下的基礎(chǔ)之外,不斷地在實(shí)際學(xué)習(xí)中完善和總結(jié)是不可缺少的,這在.NET學(xué)習(xí)中也是至關(guān)重要的。
- Instant Node Package Manager
- Mastering Visual Studio 2017
- jQuery炫酷應(yīng)用實(shí)例集錦
- Extending Unity with Editor Scripting
- Nagios Core Administration Cookbook(Second Edition)
- Hack與HHVM權(quán)威指南
- Spring Boot從入門到實(shí)戰(zhàn)
- Elasticsearch搜索引擎構(gòu)建入門與實(shí)戰(zhàn)
- C語(yǔ)言程序設(shè)計(jì)教程
- Python高性能編程(第2版)
- 生成藝術(shù):Processing視覺創(chuàng)意入門
- Visual FoxPro數(shù)據(jù)庫(kù)程序設(shè)計(jì)
- Instant AppFog
- 熱處理常見缺陷分析與解決方案
- Scratch編程入門與算法進(jìn)階(第2版)