
親,我的簡書已不再維護和更新了,所有文章都遷移到了我的個人博客:https://mikefighting.github.io/,歡迎交流。
什么是正交性?
正交性是從幾何學中借鑒過來的,比如上圖中的X軸和Y軸,它們就是正交的。這里的X軸和Y軸的發(fā)展是完全獨立的,X軸的伸展不會影響到其投影到Y(jié)軸的內(nèi)容。從軟件開發(fā)的角度來看,就是一個方法,類,模塊的改動不對另一個方法,類,模塊造成影響,那么它們就是正交的。比方說你改了數(shù)據(jù)庫的表結(jié)構(gòu)但是不影響到UI,改了UI層的展示方式不能要求數(shù)據(jù)庫schema跟著變更,那么這二者就是正交的。
影響正交性的危害有哪些?
如果缺少正交性,就會嚴重影響到軟件的維護性,這種危害將會隨著項目的迭代而越來越嚴重。比方說你移動了一個方法的位置,可能就會造成嚴重的bug,因為各個方法相互依賴,你就必須理清楚所有的方法,才可以增加一個小功能。在比如說模塊A依賴了模塊B,那么模塊B的改動就需要模塊A重新加載模塊B,這樣它兩者之間其實就缺失了模塊的概念了。比如下面這個圖:

比如上圖中的Interactors,Authorizer和Entities就因為出現(xiàn)了依賴環(huán)而導致正交性缺失。這就導致這三個模塊的發(fā)布必須依賴考慮到和其它兩個模塊之間的兼容性,任何依賴這三者之一的模塊都必須同時兼容其余的兩個模塊,如果一個模塊出了問題就會很難定位,因為它們之間是一個環(huán)形的結(jié)構(gòu),很難確定是Interactors調(diào)用的Entities出了問題,還是其自身出了問題(因為Entities調(diào)用Authorizer而Authorizer又會調(diào)用Interactor)。這種維護的成本會嚴重影響到軟件的開發(fā)效率。曾經(jīng)在YouTube上看到過一篇演講給出的結(jié)論是:
程序員平均寫代碼的時間不超過10%,其它90%的時間在看代碼。
起初還不認同,但是隨著所做項目越來越大,迭代次數(shù)越來越多,項目的年代越來越久遠,這種感覺就越強烈。90%的時間用于熟悉所有的業(yè)務(wù)邏輯,熟悉層層嵌套的if else,熟悉各個方法調(diào)用之后產(chǎn)生的副作用。而真正需要加的功能更或許也就幾行代碼而已。既然正交性這么重要,下面就來聊聊影響正交性的常見因素有哪些。
影響正交性的因素有哪些?
不必要的屬性
屬性是我們在保存某個數(shù)值并且在某個時刻使用的常用方式,它往往可以保持和所屬對象相同的生命周期。但是屬性的使用同時卻帶來了弊端:
使用屬性的方法會產(chǎn)生副作用,而副作用是讓方法之間缺少正交性的關(guān)鍵因素。
考慮如下的方法:
@interface MFPeopertyShowController ()
@property (nonatomic, assign) CGFloat level;
@end
@implementation MFPeopertyShowController
- (void)viewDidLoad {
[super viewDidLoad];
...
NSDictionary *fullData = @{
...
@"level":@(10.0),
@"name":@"NoBody",
...
};
...
[self p_a:fullData];
...
[self p_b];
...
[self p_c];
}
- (void)p_a:(NSDictionary *)data {
self.level = [data[@"level"] floatValue];
//其它業(yè)務(wù)邏輯
}
- (void)p_b {
if (self.level < 2) {
// ...
}else if (self.level < 4) {
// ...
}else if (self.level < 6) {
// ...
}else {
// ...
}
...
}
- (void)p_c {
//用到level做了其它的并且賦值
...
}
這個例子中我們?yōu)榧墑e作為屬性,在p_a中對其賦值,之后調(diào)用了p_b方法,這個方法中用到了level屬性。到這里p_a和p_b方法就缺少了正交性。我們必須先調(diào)用p_a,然后再調(diào)用p_b,并且任何對屬性level產(chǎn)生的影響都將影響到p_b和p_c。再加上很多人會對這個p_a這個方法命名極其不規(guī)范,導致不熟悉該業(yè)務(wù)的人在解決bug時發(fā)現(xiàn)把p_c移到最上面好像可以解決,試了之后發(fā)現(xiàn)又引入了新的bug,仔細研究才發(fā)現(xiàn)是以為level的值設(shè)置錯了。然后就他需要全局去搜索這個level,看看都有哪些地方用了,或者對它設(shè)置了值,最后發(fā)現(xiàn)搜索到了N個......。這里舉得例子可能不太貼切,但是最終的結(jié)果是相同的屬性的使用導致了各個方法之間沒有了正交性,每個方法之后的重構(gòu)或者新增功能都必須考慮到這個屬性值。
怎么做才可以盡量減少屬性的使用呢?在這個方法中,我們其實可以讓p_a方法返回level,然后將它做為參數(shù)傳入p_b,最后p_c傳入一個level,并且返回一個新的level。這樣做有以下幾個優(yōu)點:
- 各個方法之間的關(guān)系很清晰,如果改動了本來的順序,編譯器就直接警告你了。
- 一個方法不依賴其它方法了,它所依賴的只是一個輸入的值。
- 有一天如果這個方法在另外一個項目中能用,這時,只需要做很少的改動就行了。
- 在多線程中,如果是屬性則需要利用加鎖等手段來保證線程安全。但是如果用了局部變量,則就不會出現(xiàn)競態(tài)條件了,也就無需加鎖。
- 利用局部變量往往可以提高程序的性能,因為編譯器會將某些局部變量(OC對象除外)直接存儲在CPU的寄存器堆中,而不是在內(nèi)存中(參考CSAPP第3章,第5章)。
但是有時候使用屬性是不好避免的,比如我們給一個組件傳遞了一個數(shù)據(jù),想在組件被用戶點擊的時候?qū)?shù)值傳遞出去,因為在OC中這是基于Target-Action實現(xiàn)的,除了屬性,我們沒有辦法來保存數(shù)據(jù)以便在Action的方法中傳遞,如果這里是一個Block能夠捕獲主局部變量,我們就可以少寫個屬性了,其實Android開發(fā)中事件的回調(diào)用的是匿名內(nèi)部類,剛好用的就是這種思想。
單例模式的濫用
在一個應用程序中如果某個對象應該是唯一的,那么需要用到單例模式,比如UIApplication對象,每個應用對應一個??赡苁且驗閱卫J綄崿F(xiàn)簡單的緣故,導致它很容易被濫用。比如很多人用單例模式來傳值。 單例傳值確實很簡單,一個單例能夠解決需要將參數(shù)層層傳遞到目標對象的繁瑣工作。它卻帶來了維護的災難。 因為單例嚴重影響了各個類之間的正交性,頁面A正在是用著這個值,然后跳轉(zhuǎn)到頁面B,頁面B改了之后頁面A的值就變了。試想下,如果這個是單例是公司級的工具,每個業(yè)務(wù)線都在用,你根本看不到其它業(yè)務(wù)線的代碼,獨立測試順利通過了,集成之后出現(xiàn)Bug(集成之后代碼的測試程度往往會小于獨立測試的強度,結(jié)果導致線上Bug)。除此之外單例更容易出現(xiàn)線程安全的問題,我就曾經(jīng)見到過因為單例的非線程安全而造成難以排查的Crash。
單例模式不是用來傳值的,用單例傳值往往會造成維護的災難。
違背最小知道原則
最小知道原則告訴我們:一個類對其它類知道的越少越好。還用一個中比較有趣說法是:編寫害羞的代碼,讓一個類暴露的越少越好。這樣兩個類之間就越正交,一個類的變動對另一個類的影響也就最少。比如下面這個例子:
- (void)processDate(NSDate aData, MFSelection aSelection) {
TimeZone tz = aSelection.getRecorder().getLocation().getTimeZone();
...
}
在這個方法中我們需要的是一個TimeZone的對象,然而我們需要層層尋找,在這個過程中我們不經(jīng)意間依賴了Recorder和Location這兩個本來沒有必要依賴的類,忽然有一天發(fā)現(xiàn)從Recoder中獲取時區(qū)的方式會有問題,那么我們就需要查找到整個項目改動所有的方法。應該怎樣解決呢?給MFSelection添加一個getLocationTimeZone的方法,將上文獲取時區(qū)的方法放到里面即可。這樣processDate所在的類就只知道了一個MFSelection類,而不知道其內(nèi)部的其它類。再舉個例子:
@interface MFPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end
這是一個Person類,它有一個firstName和lastName,這時服務(wù)端返回的數(shù)據(jù),可是有一個場景需要展示fullName。大多數(shù)人會在調(diào)用Person類的地方自己做個字符拼接來得到所需要的fullName。后來這種場景越來越多,你就拼接的地方也會越來越多。再后來用戶體驗師發(fā)現(xiàn)fullName的展示可以優(yōu)化下,在firstName和lastName中間加上一個特殊符號會更好。這時需要改的地方就會很多。在這里,Person類是無需外界知道其fullName的拼接過程的,所以我們應該給Person添加一個屬性:
@property (nonatomic, copy) NSString *fullName;
其實這里還有一點需要注意,如果沒有對Person屬性進行寫的需求,要將其變成readonly,這樣它就更好的保證了自身的封裝性。
濫用繼承
繼承是很多人利用實現(xiàn)復用的手段,但它會嚴重影響到程序的正交性。在繼承中,子類在開發(fā)新功能時要考慮到父類的代碼邏輯,父類變動更會影響到很多子類。除此之外因為父類往往會加一些模板方法,而模板方法的邏輯在父類中。這就導致新人在熟悉代碼的時候要將父類也熟悉一遍。父類的方法調(diào)用依賴子類的實現(xiàn),子類又天生的依賴了父類,這就導致了環(huán)形的依賴,容易產(chǎn)生難以排查的Bug:子類調(diào)用了方法A,但是不知道怎么又觸發(fā)了方法N,調(diào)試了很久就才發(fā)現(xiàn)是父類的方法A調(diào)用了B,B又調(diào)用了C...最后調(diào)用到了N。所以能不用繼承的時候就盡量不用繼承,改用組合。
小結(jié)
為了讓寫的代碼保持正交性,就要盡力避免和其它方法或者類持有相同的對象,要盡量避免使用繼承。同時要滿足最小知道原則,減少它暴露的信息。
參考資料:
《程序員的修煉之道--從小工到專家》
《深入理解計算機系統(tǒng)》
《Clean Architecture》