動力之源:代碼中的泵
高屋建瓴:梳理程式設計約定
程式設計之基礎:資料類型(一)
程式設計之基礎:資料類型(二)
可複用代碼:元件的來龍去脈
重中之重:委托與事件
物以類聚:對象也有生命
12.1 從面向對象開始
12.1.1 對象基礎:封裝
12.1.2 對象擴充:繼承
12.1.3 對象行為:多态
12.2 不可避免的代碼依賴
12.2.1 依賴存在的原因
12.2.2 耦合與内聚
12.2.3 依賴造成的尴尬
12.3 降低代碼依賴
12.3.1 認識抽象與具體
12.3.2 再看“依賴倒置原則”
12.3.3 依賴注入
12.4 架構中的“代碼依賴”
12.4.1 控制轉換
12.4.2 依賴注入對架構的意義
12.5 本章回顧
12.6 本章思考
在浩瀚的代碼世界中,有着無數的對象,跟人和人之間有社交關系一樣,對象跟對象之間也避免不了接觸,所謂接觸,就是指一個對象要使用到另外對象的屬性、方法等成員。現實生活中一個人的社交關系複雜可能并不是什麼不好的事情,然而對于代碼中的對象而言,複雜的"社交關系"往往是不提倡的,因為對象之間的關聯性越大,意味着代碼改動一處,影響的範圍就會越大,而這完全不利于系統重構和後期維護。是以在現代軟體開發過程中,我們應該遵循"盡量降低代碼依賴"的原則,所謂盡量,就已經說明代碼依賴不可避免。
有時候一味地追求"降低代碼依賴"反而會使系統更加複雜,我們必須在"降低代碼依賴"和"增加系統設計複雜性"之間找到一個平衡點,而不應該去盲目追求"六人定理"那種設計境界。
注:"六人定理"指:任何兩個人之間的關系帶,基本确定在六個人左右。兩個陌生人之間,可以通過六個人來建立聯系,此為六人定律,也稱作六人法則。
在計算機科技發展曆史中,程式設計的方式一直都是趨向于簡單化、人性化,"面向對象程式設計"正是曆史發展某一階段的産物,它的出現不僅是為了提高軟體開發的效率,還符合人們對代碼世界和真實世界的統一認識觀。當說到"面向對象",出現在我們腦海中的詞無非是:類,抽閑,封裝,繼承以及多态,本節将從對象基礎、對象擴充以及對象行為三個方面對"面向對象"做出解釋。
注:面向對象中的"面向"二字意指:在代碼世界中,我們應該将任何東西都看做成一個封閉的單元,這個單元就是"對象"。對象不僅僅可以代表一個可以看得見摸得着的物體,它還可以代表一個抽象過程,從理論上講,任何具體的、抽象的事物都可以定義成一個對象。
和現實世界一樣,無論從微觀上還是宏觀上看,這個世界均是由許許多多的單個獨立物體組成,小到人、器官、細胞,大到國家、星球、宇宙, 每個獨立單元都有自己的屬性和行為。仿照現實世界,我們将代碼中有關聯性的資料與操作合并起來形成一個整體,之後在代碼中資料和操作均是以一個整體出現,這個過程稱為"封裝"。封裝是面向對象的基礎,有了封裝,才會有整體的概念。

圖12-1 封裝前後
如上圖12-1所示,圖中左邊部分為封裝之前,資料和操作資料的方法沒有互相對應關系,方法可以通路到任何一個資料,每個資料沒有通路限制,顯得雜亂無章;圖中右邊部分為封裝之後,資料與之關聯的方法形成了一個整體單元,我們稱為"對象",對象中的方法操作同一對象的資料,資料之間有了"保護"邊界。外界可以通過對象暴露在外的接口通路對象,比如給它發送消息。
通常情況下,用于儲存對象資料的有字段和屬性,字段一般設為私有通路權限,隻準對象内部的方法通路,而屬性一般設為公開通路權限,供外界通路。方法就是對象的表現行為,分為私有通路權限和公開通路權限兩類,前者隻準對象内部通路,而後者允許外界通路。
上面代碼Code 12-1将學生這個人群定義成了一個Student類(NO.1處),它包含三個字段:分别為儲存姓名的_name、儲存年齡的_age以及儲存愛好的_hobby字段,這三個字段都是私有通路權限,為了友善外界通路内部的資料,又分别定義了三個屬性:分别為通路姓名的Name,注意該屬性是隻讀的,因為正常情況下姓名不能再被外界改變;通路年齡的Age,注意當給年齡指派小于等于0時,代碼自動将其設定為1;通路愛好的Hobby,外界可以通過該屬性對_hobby字段進行完全通路。同時Student類包含兩個方法,一個公開的SyaHello()方法和一個受保護的GetSayHelloWords()方法,前者負責輸出對象自己的"介紹資訊",後者負責格式化"介紹資訊"的字元串。Student類圖見圖12-2:
圖12-2 Student類圖
注:上文中将類的成員通路權限隻分為兩個部分,一個對外界可見,包括public;另一種對外界不可見,包括private、protected等。
注意類與對象的差別,如果說對象是代碼世界對現實世界中各種事物的一一映射,那麼類就是這些映射的模闆,通過模闆建立具體的映射執行個體:
圖12-3 對象執行個體化
我們可以看到代碼Code 12-1中的Student類既包含私有成員也包含公開成員,私有成員對外界不可見,外界如需通路對象,隻能調用給出的公開方法。這樣做的目的就是将外界不必要了解的資訊隐藏起來,對外隻提供簡單的、易懂的、穩定的公開接口即可友善外界對該類型的使用,同時也避免了外界對對象内部資料不必要的修改和通路所造成的異常。
封裝的準則:
封裝是面向對象的第一步,有了封裝,才會有類、對象,再才能談繼承、多态等。經過前人豐富的實踐和總結,對封裝有以下準則,我們在平時實際開發中應該盡量遵循這些準則:
1)一個類型應該盡可能少地暴露自己的内部資訊,将細節的部分隐藏起來,隻對外公開必要的穩定的接口;同理,一個類型應該盡可能少地了解其它類型,這就是常說的"迪米特法則(Law of Demeter)",迪米特法則又被稱作"最小知識原則",它強調一個類型應該盡可能少地知道其它類型的内部實作,它是降低代碼依賴的一個重要指導思想,詳見本章後續介紹;
2)理論上,一個類型的内部代碼可以任意改變,而不應該影響對外公開的接口。這就要求我們将"善變"的部分隐藏到類型内部,對外公開的一定是相對穩定的;
3)封裝并不單指代碼層面上,如類型中的字段、屬性以及方法等,更多的時候,我們可以将其應用到系統結構層面上,一個子產品乃至系統,也應該隻對外提供穩定的、易用的接口,而将具體實作細節隐藏在系統内部。
封裝的意義:
封裝不僅能夠友善對代碼對資料的統一管理,它還有以下意義:
1)封裝隐藏了類型的具體實作細節,保證了代碼安全性和穩定性;
2)封裝對外界隻提供穩定的、易用的接口,外部使用者不需要過多地了解代碼實作原理也不需要掌握複雜難懂的調用邏輯,就能夠很好地使用類型;
3)封裝保證了代碼子產品化,提高了代碼複用率并確定了系統功能的分離。
封裝強調代碼合并,封裝的結果就是建立一個個獨立的包裝件:類。那麼我們有沒有其它的方法去建立新的包裝件呢?
在現實生活中,一種物體往往衍生自另外一種物體,所謂衍生,是指衍生體在具備被衍生體的屬性基礎上,還具備其它額外的特性,被衍生體往往更抽象,而衍生體則更具體,如大學衍生自學校,因為大學具備學校的特點,但大學又比學校具體,人衍生自生物,因為人具備生物的特點,但人又比生物具體。
圖12-4 學校衍生圖
如上圖12-4,學校相對來講最抽象,大學、高中以及國小均可以衍生自學校,進一步來看,大學其實也比較抽象,因為大學還可以有具體的大學、專科,是以大學和專科可以衍生自大學,當然,抽象和具體的概念是相對的,如果你覺得大學還不夠具體,那麼它可以再衍生出來一本、二本以及三本。
在代碼世界中,也存在"衍生"這一說,從一個較抽象的類型衍生出一個較具體的類型,我們稱"後者派生自前者",如果A類型派生自B類型,那麼稱這個過程為"繼承",A稱之為"派生類",B則稱之為"基類"。
注:派生類又被形象地稱為"子類",基類又被形象地稱為"父類"。
在代碼12-1中的Student類基礎上,如果我們需要建立一個大學生(College_Student)的類型,那麼我們完全可以從Student類派生出一個新的大學生類,因為大學生具備學生的特點,但又比學生更具體:
如上代碼Code 12-2所示,College_Student類繼承Student類(NO.1處),College_Student類具備Student類的屬性,比如Name、Age以及Hobby,同時College_Student類還增加了額外的專業(Major)屬性,通過在派生類中重寫GetSyaHelloWords()方法,我們重新格式化"個人資訊"字元串,讓其包含"專業"的資訊(NO.3處),最後,調用College_Student中從基類繼承下來的SayHello()方法,便可以輕松輸出自己的個人資訊。
我們看到,派生類通過繼承獲得了基類的全部資訊,之外,派生類還可以增加新的内容(如College_Student類中新增的Major屬性),基類到派生類是一個抽象到具體的過程,是以,我們在設計類型的時候,經常将通用部分提取出來,形成一個基類,以後所有與基類有種族關系的類型均可以繼承該基類,以基類為基礎,增加自己特有的屬性。
圖12-5 College_Student類繼承圖
有的時候,一種類型隻用于其它類型派生,從來不需要建立它的某個具體對象執行個體,這樣的類高度抽象化,我們稱這種類為"抽象類",抽象類不負責建立具體的對象執行個體,它包含了派生類型的共同成分。除了通過繼承某個類型來建立新的類型,.NET中還提供另外一種類似的建立新類型的方式:接口實作。接口定義了一組方法,所有實作了該接口的類型必須實作接口中所有的方法:
如上代碼Code 12-3所示,People和Dog類型均實作了IWalkable接口,那麼它們必須都實作IWalkable接口中的Walk()方法,見下圖12-6:
圖12-6 接口繼承
繼承包括兩種方式,一種為"類繼承",一種為"接口繼承",它們的作用類似,都是在現有類型基礎上建立出新的類型,但是它們也有差別:
1)類繼承強調了族群關系,而接口繼承強調通用功能。類繼承中的基類和派生類屬于祖宗和子孫的關系,而接口繼承中的接口和實作了接口的類型并沒有這種關系。
2)類繼承強調"我是(Is-A)"的關系,派生類"是"基類(注意這裡的"是"代表派生類具備基類的特性),而接口繼承強調"我能做(Can-Do)"的關系,實作了接口的類型具有接口中規定的行為能力(是以接口在命名時均以"able"作為字尾)。
3)類繼承中,基類雖然較抽象,但是它可以有具體的實作,比如方法、屬性的實作,而接口繼承中,接口不允許有任何的具體實作。
繼承的準則:
繼承是面向對象程式設計中建立類型的一種方式,在封裝的基礎上,它能夠減少工作量、提高代碼複用率的同時,快速地建立出具有相似性的類型。在使用繼承時,請遵循以下準則:
1)嚴格遵守"裡氏替換原則",即基類出現的地方,派生類一定可以出現,是以,不要盲目地去使用繼承,如果兩個類沒有衍生的關系,那麼就不應該有繼承關系。如果讓貓(Cat)類派生自狗(Dog)類,那麼很容易就可以看到,狗類出現的地方,貓類不一定可以代替它出現,因為它兩根本就沒有抽象和具體的層次關系。
2)由于派生類會繼承基類的全部内容,是以要嚴格控制好類型的繼承層次,不然派生類的體積會越來越大。另外,基類的修改必然會影響到派生類,繼承層次太多不易管理,繼承是增加耦合的最重要因素。
3)繼承強調類型之間的通性,而非特性。是以我們一般将類型都具有的部分提取出來,形成一個基類(抽象類)或者接口。
"多态"一詞來源于生物學,本意是指地球上的所有生物展現出形态和狀态的多樣性。在面向對象程式設計中多态是指:同一操作作用于不同類的執行個體,将産生不同的執行結果,即不同類的對象收到相同的消息時,得到不同的結果。
多态強調面向對象程式設計中,對象的多種表現行為,見下代碼Code 12-4:
如上代碼Code 12-4所示,分别定義了三個類:Student(NO.1處)、College_Student(NO.2處)、Senior_HighSchool_Student(NO.3處),後面兩個類繼承自Student類,并重寫了SayHello()方法。在用戶端代碼中,對于同一行代碼"student.IntroduceMyself();"而言,三次調用(NO.4、NO.5以及NO.6處),螢幕輸出的結果卻不相同:
圖12-7 多态效果
如上圖12-7所示,三次調用同一個方法,不同對象有不同的表現行為,我們稱之為"對象的多态性"。從代碼Code 12-4中可以看出,之是以出現同樣的調用會産生不同的表現行為,是因為給基類引用student指派了不同的派生類對象,并且派生類中重寫了SayHello()虛方法。
對象的多态性是以"繼承"為前提的,而繼承又分為"類繼承"和"接口繼承"兩類,那麼多态性也有兩種形式:
1)類繼承式多态;
類繼承式多态需要虛方法的參與,正如代碼Code 12-4中那樣,派生類在必要時,必須重寫基類的虛方法,最後使用基類引用調用各種派生類對象的方法,達到多種表現行為的效果:
2)接口繼承式多态。
接口繼承式多态不需要虛方法的參與,在代碼Code 12-3的基礎上編寫如下代碼:
如上代碼Code 12-5所示,對于同一行代碼"iw.Walk();"的兩次調用(NO.1和NO.2處),有不同的表現行為:
圖12-8 接口繼承式多态
在面向對象程式設計中,多态的前提是繼承,而繼承的前提是封裝,三者缺一不可。多态也是是降低代碼依賴的有力保障,詳見本章後續有關内容。
本書前面章節曾介紹過,程式的執行過程就是方法的調用過程,有方法調用,必然會促使對象跟對象之間産生依賴,除非一個對象不參與程式的運作,這樣的對象就像一座孤島,與其它對象沒有任何互動,但是這樣的對象也就沒有任何存在價值。是以,在我們的程式代碼中,任何一個對象必然會與其它一個甚至更多個對象産生依賴關系。
"方法調用"是最常見産生依賴的原因,一個對象與其它對象必然會通信(除非我們把所有的代碼邏輯全部寫在了這個對象内部),通信通常情況下就意味着有方法的調用,有方法的調用就意味着這兩個對象之間存在依賴關系(至少要有其它對象的引用才能調用方法),另外常見的一種産生依賴的原因是:繼承,沒錯,繼承雖然給我們帶來了非常大的好處,卻也給我們帶來了代碼依賴。依賴産生的原因大概可以分以下四類:
1)繼承;
派生類繼承自基類,獲得了基類的全部内容,但同時,派生類也受控于基類,隻要基類發生改變,派生類一定發生變化:
圖12-9 繼承依賴
上圖12-9中,B和C繼承自A,A類改變必然會影響B和C的變化。
2)成員對象;
一個類型包含另外一個類型的成員時,前者必然受控于後者,雖然後者的改變不一定會影響到前者:
圖12-10 成員對象依賴
如上圖12-10,A包含B類型的成員,那麼A就受控于B,B在A内部完全可見。
注:成員對象依賴跟組合(聚合)類似。
3)傳遞參數;
一個類型作為參數傳遞給另外一個類型的成員方法,那麼後者必然會受控于前者,雖然前者的改變不一定會影響到後者:
圖12-11 傳參依賴
如上圖12-11,A類型的方法Method()包含一個B類型的參數,那麼A就受控于B,B在A的Method()方法可見。
4)臨時變量。
任何時候,一個類型将另外一個類型用作了臨時變量時,那麼前者就受控于後者,雖然後者的改變不一定會影響到前者:
如上代碼Code 12-6,B的DoSomething()方法中使用了A類型的臨時對象,A在B的DoSomething()方法中局部範圍可見。
通常情況下,通過被依賴者在依賴者内部可見範圍大小來衡量依賴程度的高低,原因很簡單,可見範圍越大,說明通路它的機率就越大,依賴者受影響的機率也就越大,是以,上述四種依賴産生的原因中,依賴程度按順序依次降低。
為了衡量對象之間依賴程度的高低,我們引進了"耦合"這一概念,耦合度越高,說明對象之間的依賴程度越高;為了衡量對象獨立性的高低,我們引進了"内聚"這一概念,内聚性越高,說明對象與外界互動越少、獨立性越強。很明顯,耦合與内聚是兩個互相對立又密切相關的概念。
注:從廣義上講,"耦合"與"内聚"不僅适合對象與對象之間的關系,也适合子產品與子產品、系統與系統之間的關系,這跟前面講"封裝"時強調"封裝"不僅僅指代碼層面上的道理一樣。
"子產品功能集中,子產品之間界限明确"一直是軟體設計追求的目标,軟體系統不會因為需求的改變、功能的更新而不得不大範圍修改原來已有的源代碼,換句話說,我們在軟體設計中,應該嚴格遵循"高内聚、低耦合"的原則。下圖12-12顯示一個系統遵循該原則前後:
圖12-12 高内聚、低耦合
如上圖12-12所示,"高内聚、低耦合"強調對象與對象之間(子產品與子產品之間)盡可能多地降低依賴程度,每個對象(或子產品,下同)盡可能提高自己的獨立性,這就要求它們各自負責的功能相對集中,代碼結構由"開放"轉向"收斂"。
"職責單一原則(SRP)"是提高對象内聚性的理論指導思想之一,它建議每個對象隻負責某一個(一類)功能。
如果在軟體系統設計初期,沒有合理地降低(甚至避免)代碼間的耦合,系統開發後期往往會遇到前期不可預料的困難。下面舉例說明依賴給我們造成的"尴尬"。
假設一個将要開發的系統中使用到了資料庫,系統設計階段确定使用SQL Server資料庫,按照"代碼子產品化可以提高代碼複用性"的原則,我們将通路SQL Server資料庫的代碼封裝成了一個單獨的類,該類隻負責通路SQLServer資料庫這一功能:
如上代碼Code 12-7所示,定義了一個SQL Server資料庫通路類SQLServerHelper(NO.1處),該類專門負責通路SQL Server資料庫,如執行sql語句(其它功能略),然後定義了一個資料庫管理類DBManager(NO.2處),該類負責一些資料的增删改查(NO.4、NO.5、NO.6以及NO.7處),同時該類還包含一個SQLServerHelper類型成員(NO.3處),負責具體SQL Server資料庫的通路。SQLServerHelper類和DBManager類的關系見下圖12-13:
圖12-13 依賴于具體
如上圖12-13所示,DBManager類依賴于SQLServerHelper類,後者在前者内部完全可見,當DBManager需要通路SQL Server資料庫時,可以交給SQLServerHelper類型成員負責,到此為止,這兩個類型合作得非常好,但是,現在如果我們對資料庫的需求發生變化,不再使用SQL Server資料庫,而要求更改使用MySQL資料庫,那麼我們需要做些什麼工作呢?和之前一樣,我們需要定義一個MySQLHelper類來負責MySQL資料庫的通路,代碼如下:
如上代碼Code 12-8,定義了一個專門通路MySQL資料庫的類型MySQLHelper,它的結構跟SQLServerHelper相同,接下來,為了使原來已經工作正常的系統重新适應于MySQL資料庫,我們還必須依次修改DBManager類中所有對SQLServerHelper類型的引用,将其全部更新為MySQLHelper的引用。如果隻是一個DBManager類使用到了SQLServerHelper的話,整個更新工作量還不算非常多,但如果程式代碼中還有其它地方使用到了SQLServerHelper類型的話,這個工作量就不可估量,除此之外,我們這樣做出的所有操作完全違背了軟體設計中的"開閉原則(OCP)",即"對擴充開放,而對修改關閉"。很明顯,我們在增加新的類型MySQLHelper時,還修改了系統原有代碼。
出現以上所說問題的主要原因是,在系統設計初期,DBManager這個類型依賴了一個具體類型SQLServerHelper,"具體"就意味着不可改變,同時也就說明兩個類型之間的依賴關系已經到達了"非你不可"的程度。要解決以上問題,需要我們在軟體設計初期就做出一定的措施,詳見下一小節。
上一節末尾說到了代碼依賴給我們工作帶來的麻煩,還提到了主要原因是對象與對象之間(子產品與子產品,下同)依賴關系太過緊密,本節主要說明怎樣去降低代碼間的依賴程度。
其實本書之前好些地方已經出現過"具體"和"抽象"的詞眼,如"具體的類型"、"依賴于抽象而非具體"等等,到目前為止,本書還并沒有系統地介紹這兩者的具體含義。
所謂"抽象",即"不明确、未知、可改變"的意思,而"具體"則是相反的含義,它表示"确定、不可改變"。我們在前面講"繼承"時就說過,派生類繼承自基類,就是一個"抽象到具體"的過程,比如基類"動物(Animal)"就是一個抽象的事物,而從基類"動物(Animal)"派生出來的"狗(Dog)"就是一個具體的事物。抽象與具體的關系如下圖12-14:
圖12-14 抽象與具體的相對性
注:抽象與具體也是一個相對的概念,并不能說"動物"就一定是一個抽象的事物,它與"生物"進行比較,就是一個相對具體的事物,同理"狗"也不一定就是具體的事物,它跟"哈士奇"進行比較,就是一個相對抽象的概念。
在代碼中,"抽象"指接口、以及相對抽象化的類,注意這裡相對抽象化的類并不特指"抽象類"(使用abstract關鍵字聲明的類),隻要一個類型在族群層次中比較靠上,那麼它就可以算是抽象的,如上面舉的"動物(Animal)"的例子;"具體"則指從接口、相對抽象化的類繼承出來的類型,如從"動物(Animal)"繼承得到的"狗(Dog)"類型。代碼中抽象與具體的舉例見下表12-1:
表12-1 抽象與具體舉例
<col>
序号
抽象
具體
說明
1
Interface IWalkable
{
void Walk();
}
class Dog:IWalkable
public void Walk()
//…
IWalkable接口是"抽象",實作IWalkable接口的Dog類是"具體"。
2
class HaShiQi:Dog
Dog類是"抽象",繼承自Dog類的HaShiQi類則是"具體"。
如果一個類型包含一個抽象的成員,比如"動物(Animal)",那麼這個成員可以是很多種類型,不僅可以是"狗(Dog)",還可以是"貓(Cat)"或者其它從"動物(Animal)"派生的類型,但是如果一個類型包含一個相對具體的成員,比如"狗(Dog)",那麼這個成員就相對固定,不可再改變。很明顯,抽象的東西更易改變,"抽象"在降低代碼依賴方面起到了重要作用。
本書前面章節在講到"依賴倒置原則"時曾建議我們在軟體設計時:
1)高層子產品不應該直接依賴于低層子產品,高層子產品和低層子產品都應該依賴于抽象;
2)抽象不應該依賴于具體,具體應該依賴于抽象。
抽象的事物不确定,一個類型如果包含一個接口類型成員,那麼實作了該接口的所有類型均可以成為該類型的成員,同理,方法傳參也一樣,如果一個方法包含一個接口類型參數,那麼實作了該接口的所有類型均可以作為方法的參數。根據"裡氏替換原則(LSP)"介紹的,基類出現的地方,派生類均可以代替其出現。我們再看本章12.2.3小節中講到的"依賴造成的尴尬",DBManager類型依賴一個具體的SQLServerHelper類型,它内部包含了一個SQLServerHelper類型成員,DBManager和SQLServerHelper之間産生了一個不可變的綁定關系,如果我們想将資料庫換成MySQL資料庫,要做的工作不僅僅是增加一個MySQLHelper類型。假設在軟體系統設計初期,我們将通路各種資料庫的相似操作提取出來,放到一個接口中,之後通路各種具體資料庫的類型均實作該接口,并使DBManager類型依賴于該接口:
如上代碼Code 12-9所示,我們将通路資料庫的方法放到了IDB接口中(NO.1處),之後所有通路其它具體資料庫的類型均需實作該接口(NO.2和NO.3處),同時DBManager類中不再包含具體SQLServerHelper類型引用,而是依賴于IDB接口(NO.5處),這樣一來,我們可以随便地将SQLServerHelper或者MySQLHelper類型對象作為DBManager的構造參數傳入,甚至我們還可以新定義其它資料庫通路類,隻要該類實作了IDB接口,
如上代碼Code 12-10,如果系統需要使用Oracle資料庫,隻需新增OracleHelper類型即可,使該類型實作IDB接口,不用修改系統其它任何代碼,新增加的OracleHelper能夠與已有代碼合作得非常好。
修改後的代碼中,DBManager不再依賴于任何一個具體類型,而是依賴于一個抽象接口IDB,見下圖12-15:
圖12-15 依賴于抽象
如上圖12-15,代碼修改之前,DBManager直接依賴于具體類型SQLServerHelper,而代碼修改後,DBManager依賴于一個"抽象",也就是說,被依賴者不确定是誰,可以是SQLServerHelper,也可以是其它實作了IDB的任何類型,DBManager與SQLServerHelper之間的依賴程度降低了。
理論上講,任何一個類型都不應該包含有具體類型的成員,而隻應該包含抽象類型成員;任何一個方法都不應該包含有具體類型參數,而隻應該包含抽象類型參數。當然這隻是理論情況,軟體系統設計初期就已确定不會再改變的依賴關系,就不需要這麼去做。
注:除了上面說到的将相同部分提取出來放到一個接口中,還有時候需要将相同部分提取出來,生成一個抽象化的基類,如抽象類。接口強調相同的行為,而抽象類一般強調相同的屬性,并且用在有族群層次的類型設計當中。
當兩個對象之間必須存在依賴關系時,"依賴倒置"為我們提供了一種降低代碼依賴程度的思想,而"依賴注入(Dependency Injection)"為我們提供了一種具體産生依賴的方法,它強調"對象間産生依賴"的具體代碼實作,是對象之間能夠合作的前提。"依賴注入"分以下三種(本小節代碼均以12.3.2小節中的代碼為前提):
(1)構造注入(Constructor Injection);
通過構造方法,讓依賴者與被依賴者産生依賴關系,
如上代碼Code 12-11所示,DBManager中包含一個IDB類型的成員,并通過構造方法初始化該成員(NO.1處),之後可以在建立DBManager對象時分别傳遞不同的資料庫通路對象(NO.2、NO.3以及NO.4處)。
通過構造方法産生的依賴關系,一般在依賴者(manager、manager2以及manager3)的整個生命期中都有效。
注:雖然不能建立接口、抽象類的執行個體,但是可以存在它們的引用。
(2)方法注入(Method Injection);
通過方法,讓依賴者與被依賴者産生依賴關系,
如上代碼Code 12-12所示,在DBManager的方法中包含IDB類型的參數(NO.1處),我們在調用方法時,需要向它傳遞一些通路資料庫的對象(NO.2、NO.3以及NO.4處)。
通過方法産生的依賴關系,一般在方法體内部有效。
(3)屬性注入(Property Injection)。
通過屬性,讓依賴者與被依賴者産生依賴關系,
如上代碼Code 12-13所示,DBManager中包含一個公開的IDB類型屬性,在必要的時候,可以設定該屬性(NO.2、NO.3以及NO.4處)的值。
通過屬性産生的依賴關系比較靈活,它的有效期一般介于"構造注入"和"方法注入"之間。
注:在很多場合,三種依賴注入的方式可以組合使用,即我們可以先通過"構造注入"讓依賴者與被依賴者産生依賴關系,後期再使用"屬性注入"的方式更改它們之間的依賴關系。"依賴注入(DI)"是以"依賴倒置""為前提的。
"控制轉換(Inversion Of Control)"強調程式運作控制權的轉移,一般形容在軟體系統中,架構主導着整個程式的運作流程,如架構确定了軟體系統主要的業務邏輯結構,架構使用者則在架構已有的基礎上擴充具體的業務功能,為此編寫的代碼均由架構在适當的時機進行調用。
"控制轉換"改變了我們對程式運作流程的一貫認識,程式不再受開發者控制,
圖12-16 程式控制權的轉移
如上圖12-16所示,架構負責調用開發者編寫的代碼,架構控制整個程式的運轉。
注:"控制轉換(IoC)、依賴倒置(DIP)以及依賴注入(DI)是三個不同性質的概念,"控制轉換"強調程式控制權的轉移,注重軟體運作流程;"依賴倒置"是一種降低代碼依賴程度的理論指導思想,它注重軟體結構;"依賴注入"是對象之間産生依賴關系的一種具體實作方式,它注重程式設計實作。筆者認為有的書籍将三者做相等或者相似的比較是不準确的。
通常,又稱"控制轉換(IoC)"為"好萊塢原則(Hollywood Principle)",它建議架構與開發者編寫代碼之間的關系是:"Don't call us,we will call you.",即整個程式的主動權在架構手中。
架構與開發者編寫的代碼之間有"調用"與"被調用"的關系,是以避免不了依賴的産生,"依賴注入"是架構與開發者編寫代碼之間相結合的一種方式。任何一個架構的建立者不僅僅要遵循"依賴倒置原則",使建立出來的架構與架構使用者之間的依賴程度最小,還應該充分考慮兩者之間産生依賴的方式。
注:"架構建立者"指開發架構的團隊,"架構使用者"指使用架構開發應用程式的程式員。
本章首先介紹了面向對象的三大特征:封裝、繼承和多态,它們是面向對象的主要内容。之後介紹了面向對象的軟體系統開發過程中不可避免的代碼依賴,還提到了不合理的代碼依賴給我們系統開發帶來的負面影響,有問題就要找出解決問題的方法,随後我們從認識"具體"和"抽象"開始,逐漸地了解可以降低代碼依賴程度的具體方法,在這個過程中,"依賴倒置(DIP)"是我們前進的理論指導思想,"高内聚、低耦合"是我們追求的目标。
1.簡述"面向對象"的三大特征。
A:從對象基礎、對象擴充以及對象行為三個方面來講,"面向對象(OO)"主要包含三大特征,分别是:封裝、繼承和多态。封裝是前提,它強調代碼子產品化,将資料以及相關的操作組合成為一個整體,對外隻公開必要的通路接口;繼承是在封裝的前提下,建立新類型的一種方式,它建議有族群關系的類型之間可以發生自上而下地衍生關系,處在族群底層的類型具備高層類型的所有特性;多态強調對象的多種表現行為,它是建立在繼承的基礎之上的,多态同時也是降低代碼依賴程度的關鍵。
2.簡述"面向抽象程式設計"的具體含義。
A:如果說"面向對象程式設計"教我們将代碼世界中的所有事物均看成是一個整體——"對象",那麼"面向抽象程式設計"教我們将代碼中所有的依賴關系都建立在"抽象"之上,一切依賴均是基于抽象的,對象跟對象之間不應該有直接具體類型的引用關系。"面向接口程式設計"是"面向抽象程式設計"的一種。
3."依賴倒置原則(DIP)"中的"倒置"二字作何解釋?
A:正常邏輯思維中,高層子產品依賴底層子產品是天經地義、理所當然的,而"依賴倒置原則"建議我們所有的高層子產品不應該直接依賴于底層子產品,而都應該依賴于一個抽象,注意這裡的"倒置"二字并不是"反過來"的意思(即底層子產品反過來依賴于高層子產品),它隻是說明正常邏輯思維中的依賴順序發生了變化,把所有違背了正常思維的東西都稱之為"倒置"。
4.在軟體設計過程中,為了降低代碼之間的依賴程度,我們遵循的設計原則是什麼?我們設計的目标是什麼?
A:有兩大設計原則主要是為了降低代碼依賴程度,即:單一職責原則(SRP)和依賴倒置原則(DIP)。我們在軟體設計時追求的目标是:高内聚、低耦合。
作者:周見智
出處:http://www.cnblogs.com/xiaozhi_5638/
首發公衆号,掃描二維碼關注公衆号,分享原創計算機視覺/深度學習/算法落地相關文章