新聞中心
有時(shí)候一味地追求"降低代碼依賴"反而會(huì)使系統(tǒng)更加復(fù)雜,我們必須在"降低代碼依賴"和"增加系統(tǒng)設(shè)計(jì)復(fù)雜性"之間找到一個(gè)平衡點(diǎn),而不應(yīng)該去盲目追求"六人定理"那種設(shè)計(jì)境界。

我們提供的服務(wù)有:成都做網(wǎng)站、網(wǎng)站制作、微信公眾號(hào)開(kāi)發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、蕉嶺ssl等。為1000多家企事業(yè)單位解決了網(wǎng)站和推廣的問(wèn)題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的蕉嶺網(wǎng)站制作公司
| 注:"六人定理"指:任何兩個(gè)人之間的關(guān)系帶,基本確定在六個(gè)人左右。兩個(gè)陌生人之間,可以通過(guò)六個(gè)人來(lái)建立聯(lián)系,此為六人定律,也稱作六人法則。 |
12.1 從面向?qū)ο箝_(kāi)始
在計(jì)算機(jī)科技發(fā)展歷史中,編程的方式一直都是趨向于簡(jiǎn)單化、人性化,"面向?qū)ο缶幊?正是歷史發(fā)展某一階段的產(chǎn)物,它的出現(xiàn)不僅是為了提高軟件開(kāi)發(fā) 的效率,還符合人們對(duì)代碼世界和真實(shí)世界的統(tǒng)一認(rèn)識(shí)觀。當(dāng)說(shuō)到"面向?qū)ο?,出現(xiàn)在我們腦海中的詞無(wú)非是:類,抽閑,封裝,繼承以及多態(tài),本節(jié)將從對(duì)象基 礎(chǔ)、對(duì)象擴(kuò)展以及對(duì)象行為三個(gè)方面對(duì)"面向?qū)ο?做出解釋。
| 注:面向?qū)ο笾械?面向"二字意指:在代碼世界中,我們應(yīng)該將任何東西都看做成一個(gè)封閉的單元,這個(gè)單元就是"對(duì)象"。對(duì)象不僅僅可以代表一個(gè)可以看得見(jiàn)摸得著的物體,它還可以代表一個(gè)抽象過(guò)程,從理論上講,任何具體的、抽象的事物都可以定義成一個(gè)對(duì)象。 |
12.1.1 對(duì)象基礎(chǔ):封裝
和現(xiàn)實(shí)世界一樣,無(wú)論從微觀上還是宏觀上看,這個(gè)世界均是由許許多多的單個(gè)獨(dú)立物體組成,小到人、器官、細(xì)胞,大到國(guó)家、星球、宇宙, 每個(gè)獨(dú)立單元都有自己的屬性和行為。仿照現(xiàn)實(shí)世界,我們將代碼中有關(guān)聯(lián)性的數(shù)據(jù)與操作合并起來(lái)形成一個(gè)整體,之后在代碼中數(shù)據(jù)和操作均是以一個(gè)整體出現(xiàn), 這個(gè)過(guò)程稱為"封裝"。封裝是面向?qū)ο蟮幕A(chǔ),有了封裝,才會(huì)有整體的概念。
圖12-1 封裝前后
如上圖12-1所示,圖中左邊部分為封裝之前,數(shù)據(jù)和操作數(shù)據(jù)的方法沒(méi)有相互對(duì)應(yīng)關(guān)系,方法可以訪問(wèn)到任何一個(gè)數(shù)據(jù),每個(gè)數(shù)據(jù)沒(méi)有訪問(wèn)限制,顯得雜 亂無(wú)章;圖中右邊部分為封裝之后,數(shù)據(jù)與之關(guān)聯(lián)的方法形成了一個(gè)整體單元,我們稱為"對(duì)象",對(duì)象中的方法操作同一對(duì)象的數(shù)據(jù),數(shù)據(jù)之間有了"保護(hù)"邊 界。外界可以通過(guò)對(duì)象暴露在外的接口訪問(wèn)對(duì)象,比如給它發(fā)送消息。
通常情況下,用于保存對(duì)象數(shù)據(jù)的有字段和屬性,字段一般設(shè)為私有訪問(wèn)權(quán)限,只準(zhǔn)對(duì)象內(nèi)部的方法訪問(wèn),而屬性一般設(shè)為公開(kāi)訪問(wèn)權(quán)限,供外界訪問(wèn)。方法就是對(duì)象的表現(xiàn)行為,分為私有訪問(wèn)權(quán)限和公開(kāi)訪問(wèn)權(quán)限兩類,前者只準(zhǔn)對(duì)象內(nèi)部訪問(wèn),而后者允許外界訪問(wèn)。
- //Code 12-1
- class Student //***
- {
- private string _name; //NO.2
- private int _age;
- private string _hobby;
- public string Name //NO.3
- {
- get
- {
- return _name;
- }
- }
- public int Age
- {
- get
- {
- return _age;
- }
- set
- {
- if(value<=0)
- {
- value=1;
- }
- _age = value;
- }
- }
- public string Hobby
- {
- get
- {
- return _hobby;
- }
- set
- {
- _hobby = value;
- }
- }
- public Student(string name,int age,string hobby)
- {
- _name = name;
- _age = age;
- _hobby = hobby;
- }
- public void SayHello() //NO.4
- {
- Console.WriteLine(GetSayHelloWords());
- }
- protected virtual string GetSayHelloWords() //NO.5
- {
- string s = "";
- s += "hello,my name is " + _name + ",\r\n",
- s += "I am "+_age + "years old," + "\r\n";
- s += "I like "+_hobby + ",thanks\r\n";
- return s;
- }
- }
上面代碼Code 12-1將學(xué)生這個(gè)人群定義成了一個(gè)Student類(***處),它包含三個(gè)字段:分別為保存姓名的_name、保存年齡的_age以及保存愛(ài)好的 _hobby字段,這三個(gè)字段都是私有訪問(wèn)權(quán)限,為了方便外界訪問(wèn)內(nèi)部的數(shù)據(jù),又分別定義了三個(gè)屬性:分別為訪問(wèn)姓名的Name,注意該屬性是只讀的,因 為正常情況下姓名不能再被外界改變;訪問(wèn)年齡的Age,注意當(dāng)給年齡賦值小于等于0時(shí),代碼自動(dòng)將其設(shè)置為1;訪問(wèn)愛(ài)好的Hobby,外界可以通過(guò)該屬性 對(duì)_hobby字段進(jìn)行完全訪問(wèn)。同時(shí)Student類包含兩個(gè)方法,一個(gè)公開(kāi)的SyaHello()方法和一個(gè)受保護(hù)的 GetSayHelloWords()方法,前者負(fù)責(zé)輸出對(duì)象自己的"介紹信息",后者負(fù)責(zé)格式化"介紹信息"的字符串。Student類圖見(jiàn)圖 12-2:
圖12-2 Student類圖
| 注:上文中將類的成員訪問(wèn)權(quán)限只分為兩個(gè)部分,一個(gè)對(duì)外界可見(jiàn),包括public;另一種對(duì)外界不可見(jiàn),包括private、protected等。 |
注意類與對(duì)象的區(qū)別,如果說(shuō)對(duì)象是代碼世界對(duì)現(xiàn)實(shí)世界中各種事物的一一映射,那么類就是這些映射的模板,通過(guò)模板創(chuàng)建具體的映射實(shí)例:
圖12-3 對(duì)象實(shí)例化
我們可以看到代碼Code 12-1中的Student類既包含私有成員也包含公開(kāi)成員,私有成員對(duì)外界不可見(jiàn),外界如需訪問(wèn)對(duì)象,只能調(diào)用給出的公開(kāi)方法。這樣做的目的就是將外界 不必要了解的信息隱藏起來(lái),對(duì)外只提供簡(jiǎn)單的、易懂的、穩(wěn)定的公開(kāi)接口即可方便外界對(duì)該類型的使用,同時(shí)也避免了外界對(duì)對(duì)象內(nèi)部數(shù)據(jù)不必要的修改和訪問(wèn)所 造成的異常。
封裝的準(zhǔn)則:
封裝是面向?qū)ο蟮?**步,有了封裝,才會(huì)有類、對(duì)象,再才能談繼承、多態(tài)等。經(jīng)過(guò)前人豐富的實(shí)踐和總結(jié),對(duì)封裝有以下準(zhǔn)則,我們?cè)谄綍r(shí)實(shí)際開(kāi)發(fā)中應(yīng)該盡量遵循這些準(zhǔn)則:
1)一個(gè)類型應(yīng)該盡可能少地暴露自己的內(nèi)部信息,將細(xì)節(jié)的部分隱藏起來(lái),只對(duì)外公開(kāi)必要的穩(wěn)定的接口;同理,一個(gè)類型應(yīng)該盡可能少地了解其它類型, 這就是常說(shuō)的"迪米特法則(Law of Demeter)",迪米特法則又被稱作"最小知識(shí)原則",它強(qiáng)調(diào)一個(gè)類型應(yīng)該盡可能少地知道其它類型的內(nèi)部實(shí)現(xiàn),它是降低代碼依賴的一個(gè)重要指導(dǎo)思想, 詳見(jiàn)本章后續(xù)介紹;
2)理論上,一個(gè)類型的內(nèi)部代碼可以任意改變,而不應(yīng)該影響對(duì)外公開(kāi)的接口。這就要求我們將"善變"的部分隱藏到類型內(nèi)部,對(duì)外公開(kāi)的一定是相對(duì)穩(wěn)定的;
3)封裝并不單指代碼層面上,如類型中的字段、屬性以及方法等,更多的時(shí)候,我們可以將其應(yīng)用到系統(tǒng)結(jié)構(gòu)層面上,一個(gè)模塊乃至系統(tǒng),也應(yīng)該只對(duì)外提供穩(wěn)定的、易用的接口,而將具體實(shí)現(xiàn)細(xì)節(jié)隱藏在系統(tǒng)內(nèi)部。
封裝的意義:
封裝不僅能夠方便對(duì)代碼對(duì)數(shù)據(jù)的統(tǒng)一管理,它還有以下意義:
1)封裝隱藏了類型的具體實(shí)現(xiàn)細(xì)節(jié),保證了代碼安全性和穩(wěn)定性;
2)封裝對(duì)外界只提供穩(wěn)定的、易用的接口,外部使用者不需要過(guò)多地了解代碼實(shí)現(xiàn)原理也不需要掌握復(fù)雜難懂的調(diào)用邏輯,就能夠很好地使用類型;
3)封裝保證了代碼模塊化,提高了代碼復(fù)用率并確保了系統(tǒng)功能的分離。
12.1.2 對(duì)象擴(kuò)展:繼承
封裝強(qiáng)調(diào)代碼合并,封裝的結(jié)果就是創(chuàng)建一個(gè)個(gè)獨(dú)立的包裝件:類。那么我們有沒(méi)有其它的方法去創(chuàng)建新的包裝件呢?
在現(xiàn)實(shí)生活中,一種物體往往衍生自另外一種物體,所謂衍生,是指衍生體在具備被衍生體的屬性基礎(chǔ)上,還具備其它額外的特性,被衍生體往往更抽象,而 衍生體則更具體,如大學(xué)衍生自學(xué)校,因?yàn)榇髮W(xué)具備學(xué)校的特點(diǎn),但大學(xué)又比學(xué)校具體,人衍生自生物,因?yàn)槿司邆渖锏奶攸c(diǎn),但人又比生物具體。
圖12-4 學(xué)校衍生圖
如上圖12-4,學(xué)校相對(duì)來(lái)講最抽象,大學(xué)、高中以及小學(xué)均可以衍生自學(xué)校,進(jìn)一步來(lái)看,大學(xué)其實(shí)也比較抽象,因?yàn)榇髮W(xué)還可以有具體的本科、???, 因此本科和??瓶梢匝苌源髮W(xué),當(dāng)然,抽象和具體的概念是相對(duì)的,如果你覺(jué)得本科還不夠具體,那么它可以再衍生出來(lái)一本、二本以及三本。
在代碼世界中,也存在"衍生"這一說(shuō),從一個(gè)較抽象的類型衍生出一個(gè)較具體的類型,我們稱"后者派生自前者",如果A類型派生自B類型,那么稱這個(gè)過(guò)程為"繼承",A稱之為"派生類",B則稱之為"基類"。
注:派生類又被形象地稱為"子類",基類又被形象地稱為"父類"。
在代碼12-1中的Student類基礎(chǔ)上,如果我們需要?jiǎng)?chuàng)建一個(gè)大學(xué)生(College_Student)的類型,那么我們完全可以從Student類派生出一個(gè)新的大學(xué)生類,因?yàn)榇髮W(xué)生具備學(xué)生的特點(diǎn),但又比學(xué)生更具體:
- //Code 12-2
- class College_Student:Student //***
- {
- private string _major;
- public string Major
- {
- get
- {
- return _major;
- }
- set
- {
- _major = value;
- }
- }
- public College_Student(string name,int age,string hobby,string major) :base(name,age,hobby) //NO.2
- {
- _major = major;
- }
- protected override string GetSayHelloWords() //NO.3
- {
- string s = "";
- s += "hello,my name is " + Name + ",\r\n",
- s += "I am "+ Age + "years old, and my major is " + _major + ",\r\n";
- s += "I like "+ Hobby + ", thanks\r\n";
- return s;
- }
- }
如上代碼Code 12-2所示,College_Student類繼承Student類(***處),College_Student類具備Student類的屬性,比 如Name、Age以及Hobby,同時(shí)College_Student類還增加了額外的專業(yè)(Major)屬性,通過(guò)在派生類中重寫(xiě) GetSyaHelloWords()方法,我們重新格式化"個(gè)人信息"字符串,讓其包含"專業(yè)"的信息(NO.3處),***,調(diào)用 College_Student中從基類繼承下來(lái)的SayHello()方法,便可以輕松輸出自己的個(gè)人信息。
我們看到,派生類通過(guò)繼承獲得了基類的全部信息,之外,派生類還可以增加新的內(nèi)容(如College_Student類中新增的Major屬性), 基類到派生類是一個(gè)抽象到具體的過(guò)程,因此,我們?cè)谠O(shè)計(jì)類型的時(shí)候,經(jīng)常將通用部分提取出來(lái),形成一個(gè)基類,以后所有與基類有種族關(guān)系的類型均可以繼承該 基類,以基類為基礎(chǔ),增加自己特有的屬性。
圖12-5 College_Student類繼承圖
有的時(shí)候,一種類型只用于其它類型派生,從來(lái)不需要?jiǎng)?chuàng)建它的某個(gè)具體對(duì)象實(shí)例,這樣的類高度抽象化,我們稱這種類為"抽象類",抽象類不負(fù)責(zé)創(chuàng)建具 體的對(duì)象實(shí)例,它包含了派生類型的共同成分。除了通過(guò)繼承某個(gè)類型來(lái)創(chuàng)建新的類型,.NET中還提供另外一種類似的創(chuàng)建新類型的方式:接口實(shí)現(xiàn)。接口定義 了一組方法,所有實(shí)現(xiàn)了該接口的類型必須實(shí)現(xiàn)接口中所有的方法:
- //Code 12-3
- interface IWalkable
- {
- void Walk();
- }
- class People:IWalkable
- {
- //…
- public void Walk()
- {
- Console.WriteLine("walk quickly");
- }
- }
- class Dog:IWalkable
- {
- //…
- public void Walk()
- {
- Console.WriteLine("walk slowly");
- }
- }
如上代碼Code 12-3所示,People和Dog類型均實(shí)現(xiàn)了IWalkable接口,那么它們必須都實(shí)現(xiàn)IWalkable接口中的Walk()方法,見(jiàn)下圖12-6:
圖12-6 接口繼承
繼承包括兩種方式,一種為"類繼承",一種為"接口繼承",它們的作用類似,都是在現(xiàn)有類型基礎(chǔ)上創(chuàng)建出新的類型,但是它們也有區(qū)別:
1)類繼承強(qiáng)調(diào)了族群關(guān)系,而接口繼承強(qiáng)調(diào)通用功能。類繼承中的基類和派生類屬于祖宗和子孫的關(guān)系,而接口繼承中的接口和實(shí)現(xiàn)了接口的類型并沒(méi)有這種關(guān)系。
2)類繼承強(qiáng)調(diào)"我是(Is-A)"的關(guān)系,派生類"是"基類(注意這里的"是"代表派生類具備基類的特性),而接口繼承強(qiáng)調(diào)"我能做(Can-Do)"的關(guān)系,實(shí)現(xiàn)了接口的類型具有接口中規(guī)定的行為能力(因此接口在命名時(shí)均以"able"作為后綴)。
3)類繼承中,基類雖然較抽象,但是它可以有具體的實(shí)現(xiàn),比如方法、屬性的實(shí)現(xiàn),而接口繼承中,接口不允許有任何的具體實(shí)現(xiàn)。
繼承的準(zhǔn)則:
繼承是面向?qū)ο缶幊讨袆?chuàng)建類型的一種方式,在封裝的基礎(chǔ)上,它能夠減少工作量、提高代碼復(fù)用率的同時(shí),快速地創(chuàng)建出具有相似性的類型。在使用繼承時(shí),請(qǐng)遵循以下準(zhǔn)則:
1)嚴(yán)格遵守"里氏替換原則",即基類出現(xiàn)的地方,派生類一定可以出現(xiàn),因此,不要盲目地去使用繼承,如果兩個(gè)類沒(méi)有衍生的關(guān)系,那么就不應(yīng)該有繼 承關(guān)系。如果讓貓(Cat)類派生自狗(Dog)類,那么很容易就可以看到,狗類出現(xiàn)的地方,貓類不一定可以代替它出現(xiàn),因?yàn)樗鼉筛揪蜎](méi)有抽象和具體的 層次關(guān)系。
2)由于派生類會(huì)繼承基類的全部?jī)?nèi)容,所以要嚴(yán)格控制好類型的繼承層次,不然派生類的體積會(huì)越來(lái)越大。另外,基類的修改必然會(huì)影響到派生類,繼承層次太多不易管理,繼承是增加耦合的最重要因素。
3)繼承強(qiáng)調(diào)類型之間的通性,而非特性。因此我們一般將類型都具有的部分提取出來(lái),形成一個(gè)基類(抽象類)或者接口。
12.1.3 對(duì)象行為:多態(tài)
"多態(tài)"一詞來(lái)源于生物學(xué),本意是指地球上的所有生物體現(xiàn)出形態(tài)和狀態(tài)的多樣性。在面向?qū)ο缶幊讨卸鄳B(tài)是指:同一操作作用于不同類的實(shí)例,將產(chǎn)生不同的執(zhí)行結(jié)果,即不同類的對(duì)象收到相同的消息時(shí),得到不同的結(jié)果。
多態(tài)強(qiáng)調(diào)面向?qū)ο缶幊讨?,?duì)象的多種表現(xiàn)行為,見(jiàn)下代碼Code 12-4:
- //Code 12-4
- class Student //***
- {
- public void IntroduceMyself()
- {
- SayHello();
- }
- protected virtual void SayHello()
- {
- Console.WriteLine("Hello,everyone!");
- }
- }
- class College_Student:Student //NO.2
- {
- protected override void SayHello()
- {
- base.SayHello();
- Console.WriteLine("I am a college student…");
- }
- }
- class Senior_HighSchool_Student:Student //NO.3
- {
- protected override void SayHello()
- {
- base.SayHello();
- Console.WriteLine("I am a senior high school student…");
- }
- }
- class Program
- {
- static void Main()
- {
- Console.Title = "SayHello";
- Student student = new Student();
- student.IntroduceMyself(); //NO.4
- student = new College_Student();
- student.IntroduceMyself(); //NO.5
- student = new Senior_HighSchool_Student();
- student.IntroduceMyself(); //NO.6
- Console.Read();
- }
- }
如上代碼Code 12-4所示,分別定義了三個(gè)類:Student(***處)、College_Student(NO.2處)、 Senior_HighSchool_Student(NO.3處),后面兩個(gè)類繼承自Student類,并重寫(xiě)了SayHello()方法。在客戶端代 碼中,對(duì)于同一行代碼"student.IntroduceMyself();"而言,三次調(diào)用(NO.4、NO.5以及NO.6處),屏幕輸出的結(jié)果卻 不相同:
圖12-7 多態(tài)效果
如上圖12-7所示,三次調(diào)用同一個(gè)方法,不同對(duì)象有不同的表現(xiàn)行為,我們稱之為"對(duì)象的多態(tài)性"。從代碼Code 12-4中可以看出,之所以出現(xiàn)同樣的調(diào)用會(huì)產(chǎn)生不同的表現(xiàn)行為,是因?yàn)榻o基類引用student賦值了不同的派生類對(duì)象,并且派生類中重寫(xiě)了 SayHello()虛方法。
對(duì)象的多態(tài)性是以"繼承"為前提的,而繼承又分為"類繼承"和"接口繼承"兩類,那么多態(tài)性也有兩種形式:
1)類繼承式多態(tài);
類繼承式多態(tài)需要虛方法的參與,正如代碼Code 12-4中那樣,派生類在必要時(shí),必須重寫(xiě)基類的虛方法,***使用基類引用調(diào)用各種派生類對(duì)象的方法,達(dá)到多種表現(xiàn)行為的效果:
2)接口繼承式多態(tài)。
接口繼承式多態(tài)不需要虛方法的參與,在代碼Code 12-3的基礎(chǔ)上編寫(xiě)如下代碼:
- //Code 12-5
- class Program
- {
- static void Main()
- {
- Console.Title = "Walk";
- IWalkable iw = new People();
- iw.Walk(); //***
- iw = new Dog();
- iw.Walk(); //NO.2
- Console.Read();
- }
- }
如上代碼Code 12-5所示,對(duì)于同一行代碼"iw.Walk();"的兩次調(diào)用(***和NO.2處),有不同的表現(xiàn)行為:
圖12-8 接口繼承式多態(tài)
在面向?qū)ο缶幊讨?,多態(tài)的前提是繼承,而繼承的前提是封裝,三者缺一不可。多態(tài)也是是降低代碼依賴的有力保障,詳見(jiàn)本章后續(xù)有關(guān)內(nèi)容。
#p#
12.2 不可避免的代碼依賴
本書(shū)前面章節(jié)曾介紹過(guò),程序的執(zhí)行過(guò)程就是方法的調(diào)用過(guò)程,有方法調(diào)用,必然會(huì)促使對(duì)象跟對(duì)象之間產(chǎn)生依賴,除非一個(gè)對(duì)象不參與程序的運(yùn)行,這樣的 對(duì)象就像一座孤島,與其它對(duì)象沒(méi)有任何交互,但是這樣的對(duì)象也就沒(méi)有任何存在價(jià)值。因此,在我們的程序代碼中,任何一個(gè)對(duì)象必然會(huì)與其它一個(gè)甚至更多個(gè)對(duì) 象產(chǎn)生依賴關(guān)系。
12.2.1 依賴存在的原因
"方法調(diào)用"是最常見(jiàn)產(chǎn)生依賴的原因,一個(gè)對(duì)象與其它對(duì)象必然會(huì)通信(除非我們把所有的代碼邏輯全部寫(xiě)在了這個(gè)對(duì)象內(nèi)部),通信通常情況下就意味著 有方法的調(diào)用,有方法的調(diào)用就意味著這兩個(gè)對(duì)象之間存在依賴關(guān)系(至少要有其它對(duì)象的引用才能調(diào)用方法),另外常見(jiàn)的一種產(chǎn)生依賴的原因是:繼承,沒(méi)錯(cuò), 繼承雖然給我們帶來(lái)了非常大的好處,卻也給我們帶來(lái)了代碼依賴。依賴產(chǎn)生的原因大概可以分以下四類:
1)繼承;
派生類繼承自基類,獲得了基類的全部?jī)?nèi)容,但同時(shí),派生類也受控于基類,只要基類發(fā)生改變,派生類一定發(fā)生變化:
圖12-9 繼承依賴
上圖12-9中,B和C繼承自A,A類改變必然會(huì)影響B(tài)和C的變化。
2)成員對(duì)象;
一個(gè)類型包含另外一個(gè)類型的成員時(shí),前者必然受控于后者,雖然后者的改變不一定會(huì)影響到前者:
圖12-10 成員對(duì)象依賴
如上圖12-10,A包含B類型的成員,那么A就受控于B,B在A內(nèi)部完全可見(jiàn)。
注:成員對(duì)象依賴跟組合(聚合)類似。
3)傳遞參數(shù);
一個(gè)類型作為參數(shù)傳遞給另外一個(gè)類型的成員方法,那么后者必然會(huì)受控于前者,雖然前者的改變不一定會(huì)影響到后者:
圖12-11 傳參依賴
如上圖12-11,A類型的方法Method()包含一個(gè)B類型的參數(shù),那么A就受控于B,B在A的Method()方法可見(jiàn)。
4)臨時(shí)變量。
任何時(shí)候,一個(gè)類型將另外一個(gè)類型用作了臨時(shí)變量時(shí),那么前者就受控于后者,雖然后者的改變不一定會(huì)影響到前者:
- //Code 12-6
- class A
- {
- public void DoSomething()
- {
- //…
- }
- }
- class B
- {
- public void DoSomething()
- {
- //…
- A a = new A();
- a.DoSomething();
- //…
- }
- }
如上代碼Code 12-6,B的DoSomething()方法中使用了A類型的臨時(shí)對(duì)象,A在B的DoSomething()方法中局部范圍可見(jiàn)。
通常情況下,通過(guò)被依賴者在依賴者內(nèi)部可見(jiàn)范圍大小來(lái)衡量依賴程度的高低,原因很簡(jiǎn)單,可見(jiàn)范圍越大,說(shuō)明訪問(wèn)它的概率就越大,依賴者受影響的概率也就越大,因此,上述四種依賴產(chǎn)生的原因中,依賴程度按順序依次降低。
12.2.2 耦合與內(nèi)聚
為了衡量對(duì)象之間依賴程度的高低,我們引進(jìn)了"耦合"這一概念,耦合度越高,說(shuō)明對(duì)象之間的依賴程度越高;為了衡量對(duì)象獨(dú)立性的高低,我們引進(jìn)了"內(nèi)聚"這一概念,內(nèi)聚性越高,說(shuō)明對(duì)象與外界交互越少、獨(dú)立性越強(qiáng)。很明顯,耦合與內(nèi)聚是兩個(gè)相互對(duì)立又密切相關(guān)的概念。
注:從廣義上講,"耦合"與"內(nèi)聚"不僅適合對(duì)象與對(duì)象之間的關(guān)系,也適合模塊與模塊、系統(tǒng)與系統(tǒng)之間的關(guān)系,這跟前面講"封裝"時(shí)強(qiáng)調(diào)"封裝"不僅僅指代碼層面上的道理一樣。
"模塊功能集中,模塊之間界限明確"一直是軟件設(shè)計(jì)追求的目標(biāo),軟件系統(tǒng)不會(huì)因?yàn)樾枨蟮母淖?、功能的升?jí)而不得不大范圍修改原來(lái)已有的源代碼,換句話說(shuō),我們?cè)谲浖O(shè)計(jì)中,應(yīng)該嚴(yán)格遵循"高內(nèi)聚、低耦合"的原則。下圖12-12顯示一個(gè)系統(tǒng)遵循該原則前后:
圖12-12 高內(nèi)聚、低耦合
如上圖12-12所示,"高內(nèi)聚、低耦合"強(qiáng)調(diào)對(duì)象與對(duì)象之間(模塊與模塊之間)盡可能多地降低依賴程度,每個(gè)對(duì)象(或模塊,下同)盡可能提高自己的獨(dú)立性,這就要求它們各自負(fù)責(zé)的功能相對(duì)集中,代碼結(jié)構(gòu)由"開(kāi)放"轉(zhuǎn)向"收斂"。
"職責(zé)單一原則(SRP)"是提高對(duì)象內(nèi)聚性的理論指導(dǎo)思想之一,它建議每個(gè)對(duì)象只負(fù)責(zé)某一個(gè)(一類)功能。
12.2.3 依賴造成的"尷尬"
如果在軟件系統(tǒng)設(shè)計(jì)初期,沒(méi)有合理地降低(甚至避免)代碼間的耦合,系統(tǒng)開(kāi)發(fā)后期往往會(huì)遇到前期不可預(yù)料的困難。下面舉例說(shuō)明依賴給我們?cè)斐傻?尷尬"。
假設(shè)一個(gè)將要開(kāi)發(fā)的系統(tǒng)中使用到了數(shù)據(jù)庫(kù),系統(tǒng)設(shè)計(jì)階段確定使用SQL Server數(shù)據(jù)庫(kù),按照"代碼模塊化可以提高代碼復(fù)用性"的原則,我們將訪問(wèn)SQL Server數(shù)據(jù)庫(kù)的代碼封裝成了一個(gè)單獨(dú)的類,該類只負(fù)責(zé)訪問(wèn)SQLServer數(shù)據(jù)庫(kù)這一功能:
- //Code 12-7
- class SQLServerHelper //***
- {
- //…
- public void ExcuteSQL(string sql)
- {
- //…
- }
- }
- class DBManager //NO.2
- {
- //…
- SQLServerHelper _sqlServerHelper; //NO.3
- public DBManager(SQLServerHelper sqlServerHelper)
- {
- _sqlServerHelper = sqlServerHelper;
- }
- public void Add() //NO.4
- {
- string sql = "";
- //…
- _sqlServerHelper.ExcuteSQL(sql);
- }
- public void Delete() //NO.5
- {
- string sql = "";
- //…
- _sqlServerHelper.ExcuteSQL(sql);
- }
- public void Update() //NO.6
- {
- string sql = "";
- //…
- _sqlServerHelper.ExcuteSQL(sql);
- }
- public void Search() //NO.7
- {
- string sql = "";
- //…
- _sqlServerHelper.ExcuteSQL(sql);
- }
- }
如上代碼Code 12-7所示,定義了一個(gè)SQL Server數(shù)據(jù)庫(kù)訪問(wèn)類SQLServerHelper(***處),該類專門(mén)負(fù)責(zé)訪問(wèn)SQL Server數(shù)據(jù)庫(kù),如執(zhí)行sql語(yǔ)句(其它功能略),然后定義了一個(gè)數(shù)據(jù)庫(kù)管理類DBManager(NO.2處),該類負(fù)責(zé)一些數(shù)據(jù)的增刪改查 (NO.4、NO.5、NO.6以及NO.7處),同時(shí)該類還包含一個(gè)SQLServerHelper類型成員(NO.3處),負(fù)責(zé)具體SQL Server數(shù)據(jù)庫(kù)的訪問(wèn)。SQLServerHelper類和DBManager類的關(guān)系見(jiàn)下圖12-13:
圖12-13 依賴于具體
如上圖12-13所示,DBManager類依賴于SQLServerHelper類,后者在前者內(nèi)部完全可見(jiàn),當(dāng)DBManager需要訪問(wèn) SQL Server數(shù)據(jù)庫(kù)時(shí),可以交給SQLServerHelper類型成員負(fù)責(zé),到此為止,這兩個(gè)類型合作得非常好,但是,現(xiàn)在如果我們對(duì)數(shù)據(jù)庫(kù)的需求發(fā)生 變化,不再使用SQL Server數(shù)據(jù)庫(kù),而要求更改使用MySQL數(shù)據(jù)庫(kù),那么我們需要做些什么工作呢?和之前一樣,我們需要定義一個(gè)MySQLHelper類來(lái)負(fù)責(zé) MySQL數(shù)據(jù)庫(kù)的訪問(wèn),代碼如下:
- //Code 12-8
- class MySQLHelper
- {
- //…
- public void ExcuteSQL(string sql)
- {
- //…
- }
- }
如上代碼Code 12-8,定義了一個(gè)專門(mén)訪問(wèn)MySQL數(shù)據(jù)庫(kù)的類型MySQLHelper,它的結(jié)構(gòu)跟SQLServerHelper相同,接下來(lái),為了使原來(lái)已經(jīng)工 作正常的系統(tǒng)重新適應(yīng)于MySQL數(shù)據(jù)庫(kù),我們還必須依次修改DBManager類中所有對(duì)SQLServerHelper類型的引用,將其全部更新為 MySQLHelper的引用。如果只是一個(gè)DBManager類使用到了SQLServerHelper的話,整個(gè)更新工作量還不算非常多,但如果程序 代碼中還有其它地方使用到了SQLServerHelper類型的話,這個(gè)工作量就不可估量,除此之外,我們這樣做出的所有操作完全違背了軟件設(shè)計(jì)中的" 開(kāi)閉原則(OCP)",即"對(duì)擴(kuò)展開(kāi)放,而對(duì)修改關(guān)閉"。很明顯,我們?cè)谠黾有碌念愋蚆ySQLHelper時(shí),還修改了系統(tǒng)原有代碼。
出現(xiàn)以上所說(shuō)問(wèn)題的主要原因是,在系統(tǒng)設(shè)計(jì)初期,DBManager這個(gè)類型依賴了一個(gè)具體類型SQLServerHelper,"具體"就意味著 不可改變,同時(shí)也就說(shuō)明兩個(gè)類型之間的依賴關(guān)系已經(jīng)到達(dá)了"非你不可"的程度。要解決以上問(wèn)題,需要我們?cè)谲浖O(shè)計(jì)初期就做出一定的措施,詳見(jiàn)下一小節(jié)。
#p#
12.3 降低代碼依賴
上一節(jié)末尾說(shuō)到了代碼依賴給我們工作帶來(lái)的麻煩,還提到了主要原因是對(duì)象與對(duì)象之間(模塊與模塊,下同)依賴關(guān)系太過(guò)緊密,本節(jié)主要說(shuō)明怎樣去降低代碼間的依賴程度。
12.3.1 認(rèn)識(shí)"抽象"與"具體"
其實(shí)本書(shū)之前好些地方已經(jīng)出現(xiàn)過(guò)"具體"和"抽象"的詞眼,如"具體的類型"、"依賴于抽象而非具體"等等,到目前為止,本書(shū)還并沒(méi)有系統(tǒng)地介紹這兩者的具體含義。
所謂"抽象",即"不明確、未知、可改變"的意思,而"具體"則是相反的含義,它表示"確定、不可改變"。我們?cè)谇懊嬷v"繼承"時(shí)就說(shuō)過(guò),派生類繼 承自基類,就是一個(gè)"抽象到具體"的過(guò)程,比如基類"動(dòng)物(Animal)"就是一個(gè)抽象的事物,而從基類"動(dòng)物(Animal)"派生出來(lái)的"狗 (Dog)"就是一個(gè)具體的事物。抽象與具體的關(guān)系如下圖12-14:
圖12-14 抽象與具體的相對(duì)性
| 注:抽象與具體也是一個(gè)相對(duì)的概念,并不能說(shuō)"動(dòng)物"就一定是一個(gè)抽象的事物,它與"生物"進(jìn)行比較,就是一個(gè)相對(duì)具體的事物,同理"狗"也不一定就是具體的事物,它跟"哈士奇"進(jìn)行比較,就是一個(gè)相對(duì)抽象的概念。 |
在代碼中,"抽象"指接口、以及相對(duì)抽象化的類,注意這里相對(duì)抽象化的類并不特指"抽象類"(使用abstract關(guān)鍵字聲明的類),只要一個(gè)類 型在族群層次中比較靠上,那么它就可以算是抽象的,如上面舉的"動(dòng)物(Animal)"的例子;"具體"則指從接口、相對(duì)抽象化的類繼承出來(lái)的類型,如 從"動(dòng)物(Animal)"繼承得到的"狗(Dog)"類型。代碼中抽象與具體的舉例見(jiàn)下表12-1:
表12-1 抽象與具體舉例
|
序號(hào) |
抽象 |
具體 |
說(shuō)明 |
|
1 |
Interface IWalkable { void Walk(); } |
class Dog:IWalkable { public void Walk() { //… } } |
IWalkable接口是"抽象",實(shí)現(xiàn)IWalkable接口的Dog類是"具體"。 |
|
2 |
class Dog:IWalkable { public void Walk() { //… } } |
class HaShiQi:Dog { //… } |
Dog類是"抽象",繼承自Dog類的HaShiQi類則是"具體"。 |
如果一個(gè)類型包含一個(gè)抽象的成員,比如"動(dòng)物(Animal)",那么這個(gè)成員可以是很多種類型,不僅可以是"狗(Dog)",還可以是"貓 (Cat)"或者其它從"動(dòng)物(Animal)"派生的類型,但是如果一個(gè)類型包含一個(gè)相對(duì)具體的成員,比如"狗(Dog)",那么這個(gè)成員就相對(duì)固定, 不可再改變。很明顯,抽象的東西更易改變,"抽象"在降低代碼依賴方面起到了重要作用。
12.3.2 再看"依賴倒置原則"
本書(shū)前面章節(jié)在講到"依賴倒置原則"時(shí)曾建議我們?cè)谲浖O(shè)計(jì)時(shí):
1)高層模塊不應(yīng)該直接依賴于低層模塊,高層模塊和低層模塊都應(yīng)該依賴于抽象;
2)抽象不應(yīng)該依賴于具體,具體應(yīng)該依賴于抽象。
抽象的事物不確定,一個(gè)類型如果包含一個(gè)接口類型成員,那么實(shí)現(xiàn)了該接口的所有類型均可以成為該類型的成員,同理,方法傳參也一樣,如果一個(gè)方法包 含一個(gè)接口類型參數(shù),那么實(shí)現(xiàn)了該接口的所有類型均可以作為方法的參數(shù)。根據(jù)"里氏替換原則(LSP)"介紹的,基類出現(xiàn)的地方,派生類均可以代替其出 現(xiàn)。我們?cè)倏幢菊?2.2.3小節(jié)中講到的"依賴造成的尷尬",DBManager類型依賴一個(gè)具體的SQLServerHelper類型,它內(nèi)部包含了 一個(gè)SQLServerHelper類型成員,DBManager和SQLServerHelper之間產(chǎn)生了一個(gè)不可變的綁定關(guān)系,如果我們想將數(shù)據(jù)庫(kù) 換成MySQL數(shù)據(jù)庫(kù),要做的工作不僅僅是增加一個(gè)MySQLHelper類型。假設(shè)在軟件系統(tǒng)設(shè)計(jì)初期,我們將訪問(wèn)各種數(shù)據(jù)庫(kù)的相似操作提取出來(lái),放到 一個(gè)接口中,之后訪問(wèn)各種具體數(shù)據(jù)庫(kù)的類型均實(shí)現(xiàn)該接口,并使DBManager類型依賴于該接口:
- //Code 12-9
- interface IDB //***
- {
- void ExcuteSQL(string sql);
- }
- class SQLServerHelper:IDB //NO.2
- {
- //…
- public void ExcuteSQL(string sql)
- {
- //…
- }
- }
- class MySQLHelper:IDB //NO.3
- {
- //…
- public void ExcuteSQL(string sql)
- {
- //…
- }
- }
- class DBManager //NO.4
- {
- //…
- IDB _dbHelper; //NO.5
- public DBManager(IDB dbHelper)
- {
- _dbHelper = dbHelper;
- }
- public void Add() //NO.6
- {
- string sql = "";
- //…
- _dbHelper.ExcuteSQL(sql);
- }
- public void Delete() //NO.7
- {
- string sql = "";
- //…
- _dbHelper.ExcuteSQL(sql);
- }
- public void Update() //NO.8
- {
- string sql = "";
- //…
- _dbHelper.ExcuteSQL(sql);
- }
- public void Search() //NO.9
- {
- string sql = "";
- //…
- _dbHelper.ExcuteSQL(sql);
- }
- }
如上代碼Code 12-9所示,我們將訪問(wèn)數(shù)據(jù)庫(kù)的方法放到了IDB接口中(***處),之后所有訪問(wèn)其它具體數(shù)據(jù)庫(kù)的類型均需實(shí)現(xiàn)該接口(NO.2和NO.3處),同 時(shí)DBManager類中不再包含具體SQLServerHelper類型引用,而是依賴于IDB接口(NO.5處),這樣一來(lái),我們可以隨便地將 SQLServerHelper或者M(jìn)ySQLHelper類型對(duì)象作為DBManager的構(gòu)造參數(shù)傳入,甚至我們還可以新定義其它數(shù)據(jù)庫(kù)訪問(wèn)類,只要 該類實(shí)現(xiàn)了IDB接口,
- //Code 12-10
- class OracleHelper:IDB //***
- {
- //…
- public void ExcuteSQL(
本文名稱:熱點(diǎn)推薦——難免的尷尬:代碼依賴
URL地址:http://www.dlmjj.cn/article/dpcjisd.html


咨詢
建站咨詢
