数据耦合的代码例子c语言,代码耦合的处理
耦合是每個(gè)程序員都必須面對(duì)的話題,也是容易被忽視的存在,怎么處理耦合關(guān)系到我們最后的代碼質(zhì)量。今天Peak君和大家聊聊耦合這個(gè)基本功話題,一起捋一捋iOS代碼中處理耦合的種種方式及差異。
簡(jiǎn)化場(chǎng)景
耦合的話題可大可小,但原理都是相通的。為了方便討論,我們先將場(chǎng)景進(jìn)行抽象和簡(jiǎn)化,只討論兩個(gè)類之間的耦合。
假設(shè)我們有個(gè)類Person,需要喝水,根據(jù)職責(zé)劃分,我們需要另一個(gè)類Cup來(lái)完成喝水的動(dòng)作,代碼如下:
//Person.h
@interface Person : NSObject
- (void)drink;
@end
//Cup.h
@interface Cup : NSObject
- (id)provideWater;
@end
很明顯,Person和Cup之間要配合完成喝水的動(dòng)作,是無(wú)論如何都會(huì)產(chǎn)生耦合的,我們來(lái)看看在Objective C下都有哪些耦合的方式,以及不同耦合方式對(duì)以后代碼質(zhì)量變化的影響。
方式一:.m引用
這種方式直接在.m文件中導(dǎo)入Cup.h,同時(shí)生成臨時(shí)的Cup對(duì)象來(lái)調(diào)用Cup中的方法。代碼如下:
#import "Person.h"
#import "Cup.h"
@implementation Person
- (void)drink {
Cup* c = [Cup new];
id water = [c provideWater];
[self sip:water];
}
- (void)sip:(id)water
{
//sip water
}
@end
這應(yīng)該是不少同學(xué)會(huì)選擇的做法,要用到某個(gè)類的功能,就import該類,再調(diào)用方法,功能完成提交測(cè)試一氣呵成。
這種方式初看起來(lái)沒(méi)什么毛病,但有個(gè)弊端:Person與Cup的耦合被埋進(jìn)了Person.m文件的方法實(shí)現(xiàn)中,而.m文件一般都是業(yè)務(wù)邏輯代碼的重災(zāi)區(qū),當(dāng)Person.m的代碼量膨脹之后,如果Person類交由另一位工程師來(lái)維護(hù),那這位新接手的同學(xué)無(wú)法從Person.h中一眼看出Person類和哪些類之間有交互,即使在Person.m中看drink的聲明也沒(méi)有任何線索,要理清楚的話,只能把Person.m文件從頭到尾讀一遍,對(duì)團(tuán)隊(duì)效率的影響可想而知。
方式二:.h Property
既然直接在.m中引用會(huì)導(dǎo)致耦合不清晰,我們可以將耦合的部分放入Property中,代碼如下:
//Person.h
@interface Person : NSObject
@property (nonatomic, strong) Cup *cup;
- (void)drink;
@end
//Person.m
@implementation Person
- (void)drink {
id water = [self.cup provideWater];
[self sip:water];
}
- (void)sip:(id)water
{
//sip water
}
@end
這樣,我們只需要掃一眼Person.h就能明白,Person類對(duì)哪些類產(chǎn)生了依賴,比直接在.m中引用清晰多了。
不知道大家有沒(méi)有好奇過(guò),為什么在Objective C中會(huì)有.h文件的存在,為什么不像Java,Swift一樣一個(gè)文件代表一個(gè)類?使用.h文件有利有弊。
.h文件最大的意義在于將聲明和實(shí)現(xiàn)相隔離。聲明是告訴外部我支持哪些功能,實(shí)現(xiàn)是支撐這些功能背后的代碼邏輯。在我們閱讀一個(gè)類的.h文件的時(shí)候,它最主要的作用是透露兩個(gè)信息:
我(Person類)依賴了哪些外部元素
我(Person類)提供哪些接口供外部調(diào)用
所以.h文件應(yīng)該是我們代碼耦合的關(guān)鍵所在,當(dāng)我們猶豫一個(gè)類的Property要不要放到.h文件中去聲明時(shí),要思考這個(gè)Property是不是必須暴露給外部。一旦暴露到.h文件中,就增加了依賴和耦合的幾率。有時(shí)候Review代碼,只要看.h文件是否清晰,就大概能猜測(cè)這個(gè)類設(shè)計(jì)者的水平。
當(dāng)我們把Cup類做為Person的Property聲明時(shí),就表明Person與Cup之間存在必要的依賴,我們把這種依賴放到頭文件中來(lái),起到一目了然的效果。這比方式一清晰了不少,但有另一個(gè)問(wèn)題,Cup暴露出去以后,外部元素可以隨意修改,當(dāng)內(nèi)部執(zhí)行drink的時(shí)候,可能另一個(gè)線程將cup置空了,影響正常的業(yè)務(wù)流程。
方式三:.h ReadOnly Property
方式二中,Person類在對(duì)Cup產(chǎn)生依賴的同時(shí),也承擔(dān)了cup隨時(shí)被外部修改的風(fēng)險(xiǎn)。當(dāng)然做直觀的做法是將Cup類作為ReadOnly的property,同時(shí)提供一個(gè)對(duì)外的setter:
//Person.h
@interface Person : NSObject
@property (nonatomic, strong, readonly) Cup *cup;
- (void)setPersonCup:(Cup*)cup;
- (void)drink;
@end
有同學(xué)可能會(huì)問(wèn),這和上面的做法有什么區(qū)別,不一樣都有讀寫的接口嗎?最大的區(qū)別是增加了檢查和干擾的入口。
當(dāng)我Debug的時(shí)候,經(jīng)常需要檢查某個(gè)Propery到底是被誰(shuí)修改了,Setter中設(shè)置一個(gè)斷點(diǎn)調(diào)試起來(lái)方便不少。同時(shí),我們還可以使用Xcode的Caller機(jī)制,查看當(dāng)前Setter都被那些外部類調(diào)用了,分析類與類之間的關(guān)聯(lián)是很有幫助。
Person.m中Setter方法還提供了我們拓展功能的入口,比如我們需要在Setter中增加多線程同步Lock,當(dāng)Person.m中的其他方法在使用Cup時(shí),Setter必須等待完成才能執(zhí)行。又比如我們可以在Setter中實(shí)現(xiàn)Copy On Write機(jī)制:
//Person.m
- (void)setPersonCup:(Cup*)cup {
Cup* anotherCup = [cup copy];
_cup = anotherCup;
}
這樣,Person類就可以避免和外部類共享同一個(gè)Cup,杜絕使用同一個(gè)水杯的衛(wèi)生問(wèn)題 ;)
總之,單獨(dú)的Setter方法讓我們對(duì)代碼有更大的掌控能力,也為后續(xù)接手維護(hù)你代碼的同學(xué)帶來(lái)了方便,利己利人。
方式四:init 注入
使用帶Setter的Property雖然看上去好了不少,但Setter方法可以被任意外部類隨時(shí)隨刻調(diào)用,對(duì)于Person.m中使用Cup的方法來(lái)說(shuō),多少有些不安心,萬(wàn)一用著用著被別人改了呢?
為了避免被隨意修改,我們可以采用init注入的方式,Objective C中的designated initializer正是為此而生:
//Person.h
@interface Person : NSObject
- (instancetype)initWithCup:(Cup*)cup;
- (void)drink;
@end
去掉Property,將Cup的設(shè)置放入init方法中,這樣Person類對(duì)外就只提供一次機(jī)會(huì)來(lái)設(shè)置Cup,init之后,外部類就沒(méi)有其他機(jī)會(huì)來(lái)修改Cup了。
這是使用最多,也是比較推薦的方式。只在對(duì)象被創(chuàng)建的時(shí)候,去建立與其他對(duì)象的關(guān)系,把可變性降低到一定程度。那這種方式是否也有什么缺點(diǎn)呢?
通過(guò)init的方式設(shè)置cup,杜絕了外部因素的影響,但如果內(nèi)部持有了cup對(duì)象,那么內(nèi)部的函數(shù)調(diào)用依然可以通過(guò)各種姿勢(shì)與Cup類產(chǎn)生耦合,比如:
//Person.m
@interface Person ()
@property (nonatomic, strong) Cup *myCup;
@end
@implementation Person
- (instancetype)initWithCup:(Cup*)cup {
self = [super init];
if (self) {
self.myCup = cup;
}
return self;
}
- (void)drinkWater {
id water = [self.myCup provideWater];
[self sip:water];
}
- (void)drinkMilk {
id milk = [self.myCup provideMilk];
[self sip:milk];
}
@end
Person內(nèi)部的方法可以通過(guò)Cup所有對(duì)外的接口來(lái)產(chǎn)生耦合,此時(shí)我們對(duì)于兩個(gè)類之間的耦合,就主要靠對(duì)Cup.h頭文件來(lái)解讀了。如果Cup類設(shè)計(jì)合理,頭文件結(jié)構(gòu)清晰的話,這其實(shí)不算太糟糕的場(chǎng)景。那還有沒(méi)有其他方式呢?
方式五:parameter 注入
用Property持有的方式,在Person對(duì)象的整個(gè)生命周期內(nèi),耦合的可能性一直存在,原因在于Property對(duì)于.m文件來(lái)說(shuō)是全局可見(jiàn)的。我們可以用另一種方式讓耦合只發(fā)生在單個(gè)方法內(nèi)部,即parameter injection:
//Person.h
@interface Person : NSObject
- (void)drink:(Cup*)cup;
@end
//Person.m
- (void)drink:(Cup*)cup {
id water = [cup provideWater];
[self sip:water];
}
這種方式的好處在于:Person和Cup的耦合只發(fā)生在drink函數(shù)的內(nèi)部,一旦函數(shù)調(diào)用結(jié)束,Person和Cup之間就結(jié)束了依賴關(guān)系。從時(shí)間和空間的跨度上來(lái)說(shuō),這種方式比持有Property風(fēng)險(xiǎn)更小。
可要是在Person中存在多處Cup的依賴,比如有drinkWater,drinkMilk,drinkCoffee等等,反而又不如Property直觀方便了。
方式六:單例引用
單例的優(yōu)劣有很多優(yōu)秀的技術(shù)文章分析過(guò)了,Peak君只強(qiáng)調(diào)其中一點(diǎn),也是平時(shí)review代碼和Debug發(fā)現(xiàn)最多的問(wèn)題緣由:單例中的狀態(tài)共享。
上面的例子中,我們可以把Cup做成單例,代碼如下:
//Person.m
- (void)drink {
id water = [[Cup sharedInstance] provideWater];
[self sip:water];
}
這種方式產(chǎn)生的耦合不但和方式一同樣隱蔽,而且是最容易導(dǎo)致代碼降級(jí)的,隨著版本的不停迭代,我們很有可能會(huì)得到下面的一個(gè)類關(guān)聯(lián)圖:
單例引用.png
所有的對(duì)象都依賴于同一個(gè)對(duì)象的狀態(tài),所有的對(duì)象都對(duì)這個(gè)對(duì)象的狀態(tài)擁有讀寫權(quán)限,最后的結(jié)果很有可能是到處打補(bǔ)丁修Bug,按下葫蘆浮起瓢。
使用單例類似的場(chǎng)景很常見(jiàn),比如我們?cè)趩卫谐钟心硞€(gè)用戶的信息,在用戶登出之后,忘記清除之前用戶的信息就會(huì)導(dǎo)致奇怪的bug,而且單例一旦零散的分布在項(xiàng)目的各個(gè)角落,要逐一處理十分困難。
方式七:繼承
繼承是一種強(qiáng)耦合關(guān)系,網(wǎng)絡(luò)上有不少關(guān)于繼承(inheritance)和組合(compoisition)之間優(yōu)劣的對(duì)比文章了,這里不做贅述。繼承確實(shí)能在初期很方便的建立清晰的對(duì)象模型,重用和多態(tài)看著也很美妙,問(wèn)題在于這種強(qiáng)耦合關(guān)系在理解上很容易產(chǎn)生分歧,比如什么樣對(duì)象之間可以被確立為父子關(guān)系,哪些子類的行為可以放到父類中給其他子類使用,在多層繼承的時(shí)候這些問(wèn)題會(huì)變得更加復(fù)雜。所以Peak君建議盡可能的少用繼承關(guān)系來(lái)描述對(duì)象,除非是一目了然毫無(wú)異議的父子關(guān)系。
我就不強(qiáng)行來(lái)一波父類定義來(lái)舉例了,比如什么ObjectWithCup這類。
方式八:runtime依賴
使用runtime來(lái)處理耦合是Objective C獨(dú)特的方式,而且耦合度非常之低,甚至可以說(shuō)感覺(jué)不到耦合的存在,比如:
//Person.m
- (void)drink:(id)obj
{
id water = nil;
SEL sel = NSSelectorFromString(@"provideWater");
if ([obj respondsToSelector:sel]) {
water = [obj performSelector:sel];
}
if (water) {
[self sip:water];
}
}
既不需要導(dǎo)入Cup的頭文件,也不需要知道Cup到底支持哪些方法。這種方式的問(wèn)題也正是由于耦合度太低了,讓開(kāi)發(fā)者感知不到耦合的存在,感知不到類之間的關(guān)系。如果哪天有人把provideWater改寫成getWater,drink方法如果沒(méi)有同步到,Xcode編譯時(shí)不會(huì)提示你,runtime也不會(huì)crash,但是業(yè)務(wù)流程卻沒(méi)有正常往下走了。
這也是為什么我們不推薦用Objective-C runtime的黑魔法去做業(yè)務(wù),只是在無(wú)副作用的場(chǎng)景下去完成一些數(shù)據(jù)的獲取操作,比如使用AOP去log日志。
方式九:protocol依賴
這并不是一種獨(dú)立的耦合方式,protocol可以結(jié)合上述各種耦合方式來(lái)進(jìn)一步降低耦合,也是在復(fù)雜類關(guān)系設(shè)計(jì)中推薦的方式,比如我們可以定義這樣一個(gè)protocol:
@protocol LiquidContainer (NSObject)(此處圓括號(hào)替換尖括號(hào))
- (id)provideWater;
- (id)provideCoffee;
@end
//Person.h
@interface Person : NSObject
- (void)drink:(id(LiquidContainer))container;(此處圓括號(hào)替換尖括號(hào))
@end
上述的方式中,無(wú)論是Property持有還是parameter注入,都可以使用protocol來(lái)降低依賴,protocol的好處在于他只規(guī)定了方法的聲明,并不限定具體是那個(gè)類來(lái)實(shí)現(xiàn)它,給后期的維護(hù)留下更大的空間和可能性。有關(guān)protocol的用處和重要性可以單獨(dú)開(kāi)一篇文章來(lái)講。
更復(fù)雜的場(chǎng)景
以上是一些常見(jiàn)的類耦合方式,描述的兩個(gè)類A,B之間的耦合方式。從上面的描述中,我們可以大致感知到兩個(gè)類使用不同的方式所導(dǎo)致的耦合的深淺,這種耦合深淺度說(shuō)白了就是:互相調(diào)用函數(shù)和訪問(wèn)狀態(tài)的頻次。理解這種耦的深淺可以幫助我們大致去量化兩個(gè)對(duì)象之間的耦合度,從而在更復(fù)雜的場(chǎng)景中去分析一個(gè)模塊或者一種架構(gòu)方式的耦合度。
在更復(fù)雜的場(chǎng)景中,比如A,B,C三個(gè)類之間也可以采用類似的方法去分析,A,B,C三者可以是如下關(guān)系:
三類耦合.png
分析三個(gè)類或者更多類之間的耦合關(guān)系的時(shí)候,也是先拆解成若干個(gè)兩個(gè)類分析,比如左邊我們分析AB,BC,AC三組耦合,進(jìn)而去感知ABC作為一個(gè)整體的耦合度。很顯然,右邊的方式看著比左邊的好,因?yàn)橹恍枰治鯝B和BC。在我們選用設(shè)計(jì)模式重構(gòu)代碼的時(shí)候,也可以依照類似的方式來(lái)分析,從而選擇耦合度最低,最貼合我們業(yè)務(wù)場(chǎng)景的模式。
我們的原則是:類與類之間調(diào)用的方法,依賴的狀態(tài)要越少越好,在Objective C這門語(yǔ)言環(huán)境下,書(shū)寫分類清晰,接口簡(jiǎn)潔的頭文件非常重要。
良性的耦合
前面的分析重在嘗試去量化和感知耦合的深淺,但并不是每一次方法調(diào)用都是有風(fēng)險(xiǎn)的,有些耦合可以稱作是良性的。
如果將我們的代碼進(jìn)行高度抽象,所有的代碼都可以被歸為兩類:Data和Action。一個(gè)Class中的Property是Data,而Class中的函數(shù)則是Action,我之前寫過(guò)的一篇關(guān)于函數(shù)式的文章中提到過(guò),真正讓我們代碼變得危險(xiǎn)的是狀態(tài)的變化,即改變Data。如果一個(gè)函數(shù)是純函數(shù),既不依賴于外部狀態(tài),也不修改外部狀態(tài),那么這個(gè)函數(shù)無(wú)論被調(diào)用多少次都是安全的。如果兩個(gè)類,比如上面舉例的Person和Cup,二者互相調(diào)用的都是純函數(shù),那么二者之間的耦合可以看做是良性的,并不會(huì)導(dǎo)致程序的狀態(tài)維護(hù)混亂,只是會(huì)讓代碼的重構(gòu)變得困難,畢竟耦合的越深,重構(gòu)改動(dòng)的代碼就越多。
所以我們?cè)谧鲈O(shè)計(jì)的時(shí)候,應(yīng)該盡可能使不同元素之間的耦合是良性的,這就涉及到狀態(tài)的維護(hù)問(wèn)題,先看下圖中兩種不同的設(shè)計(jì)方式:
model layer.png
圖中紅色的圓圈代表每個(gè)類或者功能單位所持有的狀態(tài)。依照?qǐng)D中上方的設(shè)計(jì)方式,每個(gè)單位各自處理自己的狀態(tài)變化,這些狀態(tài)之間還互相存在依賴的話,耦合越深,開(kāi)發(fā)調(diào)試和重構(gòu)就越難,代碼就降級(jí)越厲害。如果按照?qǐng)D中下方的方式,將狀態(tài)變化的部分全部都集中到一起處理,維護(hù)起來(lái)就輕松很多了,這也是為什么很多App都有model layer這一設(shè)計(jì)的原因,將App狀態(tài)(各類model)的變化處理獨(dú)立出來(lái)作為一個(gè)layer,上層(業(yè)務(wù)層)只是作為model layer的展現(xiàn)和交互的外殼。這種設(shè)計(jì)技巧,大可以應(yīng)用于一個(gè)App架構(gòu)的處理,小可以到一個(gè)小功能模塊的設(shè)計(jì)。
結(jié)束語(yǔ)
上面總結(jié)了我們常用的一些耦合方式,目的在于分析不同代碼的書(shū)寫方式,對(duì)于我們最后耦合所產(chǎn)生的影響。最后值得一提的是,上面有些耦合方式并沒(méi)有絕對(duì)的優(yōu)劣之分,不同的業(yè)務(wù)場(chǎng)景下可能選擇的方式也不同,比如有些場(chǎng)景確實(shí)需要持有Property,有些場(chǎng)景單例更合適,關(guān)鍵在于我們能明白不同方式對(duì)于我們代碼后期維護(hù)所產(chǎn)生的影響,這篇文章有些地方可能比較抽象,其中很多都是個(gè)人感悟和總結(jié),或有不妥之處,請(qǐng)閱讀之后選擇性的吸收,希望能對(duì)大家平常寫代碼處理耦合帶來(lái)一些幫助。
總結(jié)
以上是生活随笔為你收集整理的数据耦合的代码例子c语言,代码耦合的处理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: HALCON联合C#检测表面缺陷——检测
- 下一篇: 长度单位