耦合其實就是程序之間的相關性。程序員
程序之間絕對沒有相關性是不可能的,不然也不可能在一個程序中啓動,以下圖:編程
這是一個Linux中socket TCP編程的程序流程圖,在圖中的TCP服務器端,socket()、bind()接口、listen()接口、accept()接口之間確定存在着相關(就是要調用下一個接口程序必需先調用前一個接口),也就是耦合,不然整個TCP服務器端就創建不起來,以及改變了bind()中的傳入的數據,好比端口號,那麼接下來的listen()監聽的端口,accept()接收鏈接的端口也會改變,因此它們之間有很強的相關性,屬於緊耦合。因此耦合就是代碼的相關性,若是還不明白,也不要緊,繼續看下去,相信你會懂的,哈哈。設計模式
(1)數據之間耦合數組
在同一個結構體或者類中,如:服務器
typedef struct Person網絡
{架構
int age;socket
char* name;函數
}Person;單元測試
class Person
{
private:
int age_m;
bool namePresent_m;
std::string name_m;
};
在上面的結構體和類中,年齡和名字兩個基本數據單元組合成了一我的數據單元,這兩個數據之間就有了耦合,由於它們互相知道,在一些操做中可能須要互相配合操做,固然這兩種數據耦合性是比較低的,可是namePresnet_m是判斷name_m是否存在的數據,因此這兩個數據之間耦合性就高不少了。
(2)函數之間的耦合性
函數若是在一個類中也會互相存在耦合性,好比下面例子:
Class Person
{
Public:
Int getAge(){return age_m;};
Void setAge_v(int age){age_m = age;};
Std::string getName(){return name;};
Void setName(std::string name){name_m = name;};
Private:
Int age_m;
Std::string name_m;
};
其中的getAge()和setAge_v()接口操做的是同一個數據,可以互相影響,存在着很明顯的耦合,可是getName()和getAge()兩個接口相關性就不明顯的,可是也會存在耦合性,由於getName()可以訪問的類中數據,getAge()也能訪問,若是程序員編寫代碼不注意,也會把在兩個接口中調用到了相同數據,互相形成了影響。
除了封裝在一個類中的函數之間有耦合性,外部的函數也會根據業務須要產生耦合,好比剛開始說的網絡編程的例子中,socket()、listen()、bind()、accept()之間就產生了很強的耦合。
以及在兩個類中,好比:
Class Fruit
{};
Class Apple:Fruit
{};
Class FruitFactory
{
Public:
Furit* getFruit(){Fruit* fruit_p = new Apple(); return fruit_p; }
};
Class Person
{
Public:
Void eatFruit(Fruit* furit);
};
FruitFactory fruitFactory;
Fruit* fruit = fruitFactory.getFruit();
Person person;
If (fruit != NULL)
{
person.eatFruit(fruit);
}
上面的FruitFactory和Person兩個類之間產生了數據耦合,而getFruit()和eatFruit()兩個接口之間也產生了耦合。
(3)數據與函數之間的耦合
從(2)中的程序也能看出,eatFruit()這個接口和Fruit這個數據產生了耦合,若是不先建立Fruit,那麼接下來的eatFruit()操做也沒有意義,若是強制調用,甚至可能形成程序崩潰,產生coredump。
上面例子的耦合仍是比較明顯的,有一些不明顯的耦合,以下:
Speaker speaker;
speaker.PowerOn() ;
speaker.PlayMusic() ;
表面上是 PlayMusic()對PowerOn()有依賴性,是函數之間的耦合,但背後的緣由是 PowerOn()函數讓播放器處於通電狀態:
PowerOn(){
this.isPowerOn = true;
}
//只有通了電,播放器才能正常播放音樂
PlayMusic() {
if(this.isPowerOn)
Play();
}
這兩個函數是經過 this .isPowerOn 這個數據進行溝通的 。 這本質上仍是數據和函數之間的耦合。
常常聽到「解耦」這個詞,那是否是耦合都是很差的?
這個要根據代碼的耦合性特色來分析,首先看一下下面幾個問題吧,看完,相信你們也有答案了。
(1)耦合能夠消除嗎?
通過上面那麼多例子,你們也意思到耦合無處不在,因此是不能消除的。
(2)那既然不能消除,那解耦的意思是什麼?
解耦就是下降程序模塊之間的耦合性。
(3)那解耦的目的是什麼?
解耦的目的是爲了加強一個模塊的可移植性、可複用性,就像人的腎能夠移植,可是血管卻移植很難移植,爲何,由於血管這個「模塊」和身體這個「系統」之間的「耦合性」太強,關聯地方太多,移植工做量超大。以及減小模塊與外部模塊的關聯,內部模塊的修改對外部影響比較少,這也和用人的腎移植和血管移植相似,每一個器官中都有血管,一旦移植,全部器官都要動,耗時耗力。
那可移植和可複用有什麼好處?好比咱們用程序在電腦寫出了一個俄羅斯方塊的遊戲,後來客戶也要在手機端作這個遊戲,這時候就可以複用電腦端的俄羅斯方塊的遊戲策略和邏輯,只須要把界面替換掉就行,業務和策略部分基本不用修改,若是電腦端的遊戲界面和業務邏輯的程序「渾然一體」,那幾乎就須要從新翻新一遍。
(4)那全部程序都須要解耦嗎?
不是的,有些時候,咱們反而須要加強程序的耦合性,這就是平時說的「高內聚,低耦合」,其中的內聚其實也是耦合,或者說程序的相關性。以下面例子:
在界面上的不一樣位置要顯示多種不一樣的圖形,如三角形、正方形等 ,這裏全部的信息濃縮在下面兩個數組裏 。
一個是 shape 數組 : { 」三角形」,」正方形",」長方形」,"菱形」} 。
一個是 position 數組 : { pointl, point2 , poin口 , point4 } 。
兩個數組的元素個數是同樣多的,它們是一對一的關係 。 好比, 第一個 positio口就是第一個 shape 的位置信息 。 那麼代碼以下:
for(int i = O; i <count, i++){
Draw(shape[i] , position[i]);
}
這樣作方便但很差!它會爲之後的修改埋藏隱患 。 由於兩個數組元素之間的對應關係,並無獲得正式認可。這時候就須要加強它們之間的關聯,把隱式的關聯轉成顯式的關聯。以下:
Dictionary die = {「三角形」: pointl,
「正方形」: point2 ,
「長方形」: point3 ,
「菱形」 : point4 } ;
//Draw()函數不再用擔憂會畫錯了
foreach(var item in dic){
. Draw(item.key,item.value);
}
平時編程中使用的結構體和類封裝也是一樣,把一些有關聯的數據和方法組合起來,顯式加強它們之間關聯性,方便使用和移植。
(1)貫徹面向接口編碼的原則
程序不可能沒有改動的,可是儘可能把改動放在一個模塊的內部,接口不要變,就算須要改變,最好使用適配器模式增長一個適配程序。由於接口就是一個程序與外部的關聯處,保持接口不變,就是保持該模塊和外部模塊的耦合性不變,這樣才能保證它的可移植性可重用以及不被外部模塊的修改而影響。
(2)保證一個模塊的可測試(單元測試)
若是一個模塊是能夠單獨進行單元測試的,意味着它能夠移植到其餘程序上,耦合性低。
(3)能夠學習一下設計模式的設計思想。
(4)讓模塊對內有完整的邏輯
解耦的根本目的是拆除元素之間沒必要要的聯繫,一個核心原則就是讓每一個模塊的邏輯獨立而完整。其中包含兩點,一是對內有完整的邏輯 , 而所依賴的外部資源儘量是不變量;二是對外體現的特性也是「不變量」(或者儘量作到不變量),讓別人能夠放心地依賴我。有的函數光明磊落,它和外界數據的溝通僅限於函數的參數和返回值,那麼這種函數給人的感受能夠用兩個字形容:靠譜。它把本身所須要的數據都明確標識在參數列表裏,把本身能提供的全集中在返回值裏。若是你須要的某項數據不在參數裏,你就會儂賴上別人,由於你多半須要指名道姓地標明某個第三方來特供;同理,若是你提供的數據不全在返回值和參數裏,別人會依賴上你 。有的函數讓人以爲神祕莫測,規律難尋:它所須要的數據不所有體如今參數列表裏,有的隱藏在函數內部,這種不可靠的變量行爲很難預測;它的產出也不集中在返回值,而多是修改了藏在某個不起眼角落裏的資源。 這樣的函數須要人們在使用過程當中和它不斷地磨合,才能掌握它的特性。前者使用起來放心,並且是可移植、可複用的,後者使用時須要當心翼翼 ,並且很難移植。
實例一
在上面介紹的一個例子:
PowerOn(){
this.isPowerOn = true;
}
//只有通了電,播放器才能正常播放音樂
PlayMusic() {
if(this.isPowerOn)
Play();
}
這裏的PowerOn()接口和PlayMusic()接口在同一個類中,isPowerOn變量是內部私有變量,這樣寫法是沒問題。若是isPowerOn是一個全局變量,而PlayMusic()接口中程序相對複雜一些,可能就會在外部調用時候忘記了先調用PowerOn給isPowerOn設置爲true。爲了讓PlayMusic()的接口邏輯獨立而完整,就須要顯式給PlayMusic()傳入isPowerOn參數,如PlayMusic(bool isPowerOn),即便是在一個類中,爲了防止在外部調用時建議在使用PlayMusic()前添加一個判斷isPowerOn接口,如:
Speaker speaker;
If (speaker.isPowerOn())
{
speaker.PlayMusic();
}
這樣在後來有人修改該部分程序時,知道先通電在播放音樂。
實例二
一我的要讀書 :
Person person = new Person();
person.ReadBook(book);
//ReadBook 函數裏的邏輯以下:
void ReadBook( Book book) {
//要求人看書以前要先戴眼鏡,因此第一步必須是戴眼鏡的動做
WearGlasses(this.MyGlasses) ; //Person類中有一個名爲 MyClasses的成員
Read (book) ;
若是這我的沒有眼鏡,this.myGlasses 變量爲 null ,直接調用person. ReadBook(book);會出現異常,怎麼辦呢?
優化一:經過成員函數注入
因而打個補丁邏輯吧,在 ReadBook 以前先給他配副眼鏡 :
person.setMyGlasses(new Glasses()); //先爲person 配副眼鏡
person.ReadBook(book);
如上,加上了 person.setMyGlasses(new Glasses());這行代碼,這個 bug 就解決了。 可解決得不夠完美,由於這要求每一個程序員都須要記住調用 person.ReadBook(book)以前,先給成員賦值:
person.setMyGlasses(new Glasses());
這很容易出問題。 由於 ReadBook是一個 public 函數,使用上不該該有隱式的限定條件。
優化二:經過構造函數的注入
咱們能夠爲 Person的構造函數添加一個 glasses 參數:
public Person (Glasses glasses) {
this.MyGlasses = glasses ;
}
這樣, 每當程序員去建立一個 Person 的時候,都會被逼着去建立一個 Glasses 對象 。 程序員不再用記憶一些額外需求了。這樣邏輯便實現了初步的 自我完善。
當 Person類建立得多了,會發現構造函數的注人會帶來以下問題 : 由於 Person 中的不少其餘 函數行爲,如吃飯、跑步等,其實並不須要眼鏡,而喜歡讀書的人畢竟是少數,因此person.ReadBook(book);這句代碼的調用次數少得可憐。爲了一個偏僻的 ReadBook 函數,就要讓每一個Person都必須配一副眼鏡(不管他讀不讀書),這不公平。也對,咱們應該讓各自的需求各自解決。
那麼,還有更好的方法嗎? 下面介紹的「優化三」進一步解決了這個問題。
優化三:經過普通成員函數的注入
因而能夠進行下一步修改:恢復爲最初的無參構造函數,並單獨爲 ReadBook 函數添加一個glasses參數:
void ReadBook(Book book , Glasses glasses ) (
WearGlasses(glasses);
Read (book);
對該函數的調用以下 :
person.ReadBook(book , new Glasses ());
這樣只有須要讀書的人,纔會被配一副眼鏡,實現了資源的精確分配。
但是呢,如今每次讀書時都須要配一副新眼鏡:new Glasses(),仍是太浪費了,其實只
須要一副就夠了 。
優化四:封裝注入
好吧,每次取本身以前的眼鏡最符合現實需求 :
person.ReadBook(book,person.getMyGlasses()) ;
這又回到了最初的問題:person.getMyGlasses()參數可能爲空 ,怎麼辦?
乾脆讓 person.getMyGlasses()封裝的 get 函數本身去解決這個邏輯吧:
Glasses getMyGlasses(){
if(this.myGlasses==null)
this.myGlasses =new Glassess();
return this.myGlasses;
}
//而後返回到最初的ReadBook代碼。ReadBook裏的邏輯是默認取本身的眼鏡
void ReadBook(Book book) {
WearGlasses(this.getMyGlasses()) ;
Read(book);
}
對 ReadBook 函數的調用以下 :
person.ReadBook(book);
這樣每次讀書時,就會複用同一副眼鏡了,也不會影響 person 的其餘函數。
嗯,大功告成了。最終的這段ReadBook代碼是最具移植性的,稱得上獨立而完整。
能夠看到,從優化一到優化四,繞了一圈,每一步修改都很是小,每一步都是解決-個小問題,可能每一步遇到的新問題是以前並無預料到的。優化一到優化三分別是3種依賴注入的手段:屬性注入、構造函數注入和普通函數注入。它們並無優劣之分,只有應用場合之分,這裏咱們是用一個案例將它們串起來介紹了。同時你們經過這個小小的例子也能夠體會到:寫精益求精的代碼,是須要工匠精神的。讓每個模塊獨立而完整,其內涵是豐富的 。 它把本身所須要的東西全列在清單上,讓外界提供,本身並不私藏。這意味着和外界的關聯是單向的,這樣每一個模塊都變得規規矩矩,容易被使用。若是模塊要被替換,拿掉時也不會和周圍模塊藕斷絲連 。
沒有絕對好的程序,瞭解耦合性只是爲了寫出比較好的程序,可是在寫程序中過於執着於耦合性,反而不美。不過,平時編寫程序時候也要注意和思考,慢慢就能得到一些「感受」,也就養成了良好的編程習慣。
寫出好代碼的途徑,一是要有必定的知識積累,多看看書,站在前人的肩膀上,不只僅是代碼數量積累,二是對代碼進行審計,審計本身代碼找出本身一些很差的代碼編寫習慣,之後有意識去更改。