iOS 程式碼耦合的處理
耦合是每個程式設計師都必須面對的話題,也是容易被忽視的存在,怎麼處理耦合關係到我們最後的程式碼質量。今天Peak君和大家聊聊耦合這個基本功話題,一起捋一捋iOS程式碼中處理耦合的種種方式及差異。
簡化場景
耦合的話題可大可小,但原理都是相通的。為了方便討論,我們先將場景進行抽象和簡化,只討論兩個類之間的耦合。
假設我們有個類Person,需要喝水,根據職責劃分,我們需要另一個類Cup來完成喝水的動作,程式碼如下:
123456789 | //Person.h@interfacePerson:NSObject-(void)drink;@end//Cup.h@interfaceCup:NSObject-(id)provideWater;@end |
很明顯,Person和Cup之間要配合完成喝水的動作,是無論如何都會產生耦合的,我們來看看在Objective C下都有哪些耦合的方式,以及不同耦合方式對以後程式碼質量變化的影響。
方式一:.m引用
這種方式直接在.m檔案中匯入Cup.h,同時生成臨時的Cup物件來呼叫Cup中的方法。程式碼如下:
123456789101112131415161718 | #import "Person.h"#import "Cup.h"@implementation Person-(void)drink{Cup*c=[Cup new];id water=[cprovideWater];[selfsip:water];}-(void)sip:(id)water{//sip water}@end |
這應該是不少同學會選擇的做法,要用到某個類的功能,就import該類,再呼叫方法,功能完成提交測試一氣呵成。
這種方式初看起來沒什麼毛病,但有個弊端:Person與Cup的耦合被埋進了Person.m檔案的方法實現中,而.m檔案一般都是業務邏輯程式碼的重災區,當Person.m的程式碼量膨脹之後,如果Person類交由另一位工程師來維護,那這位新接手的同學無法從Person.h中一眼看出Person類和哪些類之間有互動,即使在Person.m中看drink的宣告也沒有任何線索,要理清楚的話,只能把Person.m檔案從頭到尾讀一遍,對團隊效率的影響可想而知。
方式二:.h Property
既然直接在.m中引用會導致耦合不清晰,我們可以將耦合的部分放入Property中,程式碼如下:
123456789101112131415161718 | //Person.h@interfacePerson:NSObject@property(nonatomic,strong)Cup*cup;-(void)drink;@end//Person.m@implementation Person-(void)drink{id water=[self.cup provideWater];[selfsip:water];}-(void)sip:(id)water{//sip water}@end |
這樣,我們只需要掃一眼Person.h就能明白,Person類對哪些類產生了依賴,比直接在.m中引用清晰多了。
不知道大家有沒有好奇過,為什麼在Objective C中會有.h檔案的存在,為什麼不像Java,Swift一樣一個檔案代表一個類?使用.h檔案有利有弊。
.h檔案最大的意義在於將宣告和實現相隔離。宣告是告訴外部我支援哪些功能,實現是支撐這些功能背後的程式碼邏輯。在我們閱讀一個類的.h檔案的時候,它最主要的作用是透露兩個資訊:
- 我(Person類)依賴了哪些外部元素
- 我(Person類)提供哪些介面供外部呼叫
所以.h檔案應該是我們程式碼耦合的關鍵所在,當我們猶豫一個類的Property要不要放到.h檔案中去宣告時,要思考這個Property是不是必須暴露給外部。一旦暴露到.h檔案中,就增加了依賴和耦合的機率。有時候Review程式碼,只要看.h檔案是否清晰,就大概能猜測這個類設計者的水平。
當我們把Cup類做為Person的Property宣告時,就表明Person與Cup之間存在必要的依賴,我們把這種依賴放到標頭檔案中來,起到一目瞭然的效果。這比方式一清晰了不少,但有另一個問題,Cup暴露出去以後,外部元素可以隨意修改,當內部執行drink的時候,可能另一個執行緒將cup置空了,影響正常的業務流程。
方式三:.h ReadOnly Property
方式二中,Person類在對Cup產生依賴的同時,也承擔了cup隨時被外部修改的風險。當然做直觀的做法是將Cup類作為ReadOnly的property,同時提供一個對外的setter:
123456 | //Person.h@interfacePerson:NSObject@property(nonatomic,strong,readonly)Cup*cup;-(void)setPersonCup:(Cup*)cup;-(void)drink;@end |
有同學可能會問,這和上面的做法有什麼區別,不一樣都有讀寫的介面嗎?最大的區別是增加了檢查和干擾的入口。
當我Debug的時候,經常需要檢查某個Propery到底是被誰修改了,Setter中設定一個斷點除錯起來方便不少。同時,我們還可以使用Xcode的Caller機制,檢視當前Setter都被那些外部類呼叫了,分析類與類之間的關聯是很有幫助。
Person.m中Setter方法還提供了我們拓展功能的入口,比如我們需要在Setter中增加多執行緒同步Lock,當Person.m中的其他方法在使用Cup時,Setter必須等待完成才能執行。又比如我們可以在Setter中實現Copy On Write機制:
12345 | //Person.m-(void)setPersonCup:(Cup*)cup{Cup*anotherCup=[cup copy];_cup=anotherCup;} |
這樣,Person類就可以避免和外部類共享同一個Cup,杜絕使用同一個水杯的衛生問題 ;)
總之,單獨的Setter方法讓我們對程式碼有更大的掌控能力,也為後續接手維護你程式碼的同學帶來了方便,利己利人。
方式四:init 注入
使用帶Setter的Property雖然看上去好了不少,但Setter方法可以被任意外部類隨時隨刻呼叫,對於Person.m中使用Cup的方法來說,多少有些不安心,萬一用著用著被別人改了呢?
為了避免被隨意修改,我們可以採用init注入的方式,Objective C中的designated initializer正是為此而生:
12345 | //Person.h@interfacePerson:NSObject-(instancetype)initWithCup:(Cup*)cup;-(void)drink;@end |
去掉Property,將Cup的設定放入init方法中,這樣Person類對外就只提供一次機會來設定Cup,init之後,外部類就沒有其他機會來修改Cup了。
這是使用最多,也是比較推薦的方式。只在物件被建立的時候,去建立與其他物件的關係,把可變性降低到一定程度。那這種方式是否也有什麼缺點呢?
通過init的方式設定cup,杜絕了外部因素的影響,但如果內部持有了cup物件,那麼內部的函式呼叫依然可以通過各種姿勢與Cup類產生耦合,比如:
12345678910111213141516171819202122232425 | //Person.m@interfacePerson()@property(nonatomic,strong)Cup*myCup;@end@implementation Person-(instancetype)initWithCup:(Cup*)cup{self=[superinit];if(self){self.myCup=cup;}returnself;}-(void)drinkWater{id water=[self.myCup provideWater];[selfsip:water];}-(void)drinkMilk{id milk=[self.myCup provideMilk];[selfsip:milk];}@end |
Person內部的方法可以通過Cup所有對外的介面來產生耦合,此時我們對於兩個類之間的耦合,就主要靠對Cup.h標頭檔案來解讀了。如果Cup類設計合理,標頭檔案結構清晰的話,這其實不算太糟糕的場景。那還有沒有其他方式呢?
方式五:parameter 注入
用Property持有的方式,在Person物件的整個生命週期內,耦合的可能性一直存在,原因在於Property對於.m檔案來說是全域性可見的。我們可以用另一種方式讓耦合只發生在單個方法內部,即parameter injection:
12345678910 | //Person.h@interfacePerson:NSObject-(void)drink:(Cup*)cup;@end//Person.m-(void)drink:(Cup*)cup{id water=[cup provideWater];[selfsip:water];} |
這種方式的好處在於:Person和Cup的耦合只發生在drink函式的內部,一旦函式呼叫結束,Person和Cup之間就結束了依賴關係。從時間和空間的跨度上來說,這種方式比持有Property風險更小。
可要是在Person中存在多處Cup的依賴,比如有drinkWater,drinkMilk,drinkCoffee等等,反而又不如Property直觀方便了。
方式六:單例引用
單例的優劣有很多優秀的技術文章分析過了,Peak君只強調其中一點,也是平時review程式碼和Debug發現最多的問題緣由:單例中的狀態共享。
上面的例子中,我們可以把Cup做成單例,程式碼如下:
12345 | //Person.m-(void)drink{id water=[[Cup sharedInstance]provideWater];[selfsip:water];} |
這種方式產生的耦合不但和方式一同樣隱蔽,而且是最容易導致程式碼降級的,隨著版本的不停迭代,我們很有可能會得到下面的一個類關聯圖:
所有的物件都依賴於同一個物件的狀態,所有的物件都對這個物件的狀態擁有讀寫許可權,最後的結果很有可能是到處打補丁修Bug,按下葫蘆浮起瓢。
使用單例類似的場景很常見,比如我們在單例中持有某個使用者的資訊,在使用者登出之後,忘記清除之前使用者的資訊就會導致奇怪的bug,而且單例一旦零散的分佈在專案的各個角落,要逐一處理十分困難。
方式七:繼承
繼承是一種強耦合關係,網路上有不少關於繼承(inheritance)和組合(compoisition)之間優劣的對比文章了,這裡不做贅述。繼承確實能在初期很方便的建立清晰的物件模型,重用和多型看著也很美妙,問題在於這種強耦合關係在理解上很容易產生分歧,比如什麼樣物件之間可以被確立為父子關係,哪些子類的行為可以放到父類中給其他子類使用,在多層繼承的時候這些問題會變得更加複雜。所以Peak君建議儘可能的少用繼承關係來描述物件,除非是一目瞭然毫無異議的父子關係。
我就不強行來一波父類定義來舉例了,比如什麼ObjectWithCup這類。
方式八:runtime依賴
使用runtime來處理耦合是Objective C獨特的方式,而且耦合度非常之低,甚至可以說感覺不到耦合的存在,比如:
123456789101112 | //Person.m-(void)drink:(id)obj{id water=nil;SEL sel=NSSelectorFromString(@"provideWater");if([obj respondsToSelector:sel]){water=[obj performSelector:sel];}if(water){[selfsip:water];}} |
既不需要匯入Cup的標頭檔案,也不需要知道Cup到底支援哪些方法。這種方式的問題也正是由於耦合度太低了,讓開發者感知不到耦合的存在,感知不到類之間的關係。如果哪天有人把provideWater改寫成getWater,drink方法如果沒有同步到,Xcode編譯時不會提示你,runtime也不會crash,但是業務流程卻沒有正常往下走了。
這也是為什麼我們不推薦用Objective-C runtime的黑魔法去做業務,只是在無副作用的場景下去完成一些資料的獲取操作,比如使用AOP去log日誌。
方式九:protocol依賴
這並不是一種獨立的耦合方式,protocol可以結合上述各種耦合方式來進一步降低耦合,也是在複雜類關係設計中推薦的方式,比如我們可以定義這樣一個protocol:
12345678910111213 | @protocol LiquidContainer-(id)provideWater;-(id)provideCoffee;@end//Person.h@interfacePerson:NSObject-(void)drink:(id)container;@end |
上述的方式中,無論是Property持有還是parameter注入,都可以使用protocol來降低依賴,protocol的好處在於他只規定了方法的宣告,並不限定具體是那個類來實現它,給後期的維護留下更大的空間和可能性。有關protocol的用處和重要性可以單獨開一篇文章來講。
更復雜的場景
以上是一些常見的類耦合方式,描述的兩個類A,B之間的耦合方式。從上面的描述中,我們可以大致感知到兩個類使用不同的方式所導致的耦合的深淺,這種耦合深淺度說白了就是:互相呼叫函式和訪問狀態的頻次。理解這種耦的深淺可以幫助我們大致去量化兩個物件之間的耦合度,從而在更復雜的場景中去分析一個模組或者一種架構方式的耦合度。
在更復雜的場景中,比如A,B,C三個類之間也可以採用類似的方法去分析,A,B,C三者可以是如下關係:
分析三個類或者更多類之間的耦合關係的時候,也是先拆解成若干個兩個類分析,比如左邊我們分析AB,BC,AC三組耦合,進而去感知ABC作為一個整體的耦合度。很顯然,右邊的方式看著比左邊的好,因為只需要分析AB和BC。在我們選用設計模式重構程式碼的時候,也可以依照類似的方式來分析,從而選擇耦合度最低,最貼合我們業務場景的模式。
我們的原則是:類與類之間呼叫的方法,依賴的狀態要越少越好,在Objective C這門語言環境下,書寫分類清晰,介面簡潔的標頭檔案非常重要。
良性的耦合
前面的分析重在嘗試去量化和感知耦合的深淺,但並不是每一次方法呼叫都是有風險的,有些耦合可以稱作是良性的。
如果將我們的程式碼進行高度抽象,所有的程式碼都可以被歸為兩類:Data和Action。一個Class中的Property是Data,而Class中的函式則是Action,我之前寫過的一篇關於函式式的文章中提到過,真正讓我們程式碼變得危險的是狀態的變化,即改變Data。如果一個函式是純函式,既不依賴於外部狀態,也不修改外部狀態,那麼這個函式無論被呼叫多少次都是安全的。如果兩個類,比如上面舉例的Person和Cup,二者互相呼叫的都是純函式,那麼二者之間的耦合可以看做是良性的,並不會導致程式的狀態維護混亂,只是會讓程式碼的重構變得困難,畢竟耦合的越深,重構改動的程式碼就越多。
所以我們在做設計的時候,應該儘可能使不同元素之間的耦合是良性的,這就涉及到狀態的維護問題,先看下圖中兩種不同的設計方式:
圖中紅色的圓圈代表每個類或者功能單位所持有的狀態。依照圖中上方的設計方式,每個單位各自處理自己的狀態變化,這些狀態之間還互相存在依賴的話,耦合越深,開發除錯和重構就越難,程式碼就降級越厲害。如果按照圖中下方的方式,將狀態變化的部分全部都集中到一起處理,維護起來就輕鬆很多了,這也是為什麼很多App都有model layer這一設計的原因,將App狀態(各類model)的變化處理獨立出來作為一個layer,上層(業務層)只是作為model layer的展現和互動的外殼。這種設計技巧,大可以應用於一個App架構的處理,小可以到一個小功能模組的設計。
結束語
上面總結了我們常用的一些耦合方式,目的在於分析不同程式碼的書寫方式,對於我們最後耦合所產生的影響。最後值得一提的是,上面有些耦合方式並沒有絕對的優劣之分,不同的業務場景下可能選擇的方式也不同,比如有些場景確實需要持有Property,有些場景單例更合適,關鍵在於我們能明白不同方式對於我們程式碼後期維護所產生的影響,這篇文章有些地方可能比較抽象,其中很多都是個人感悟和總結,或有不妥之處,請閱讀之後選擇性的吸收,希望能對大家平常寫程式碼處理耦合帶來一些幫助。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
相關推薦
iOS 程式碼耦合的處理
耦合是每個程式設計師都必須面對的話題,也是容易被忽視的存在,怎麼處理耦合關係到我們最後的程式碼質量。今天Peak君和大家聊聊耦合這個基本功話題,一起捋一捋iOS程式碼中處理耦合的種種方式及差異。 簡化場景 耦合的話題可大可小,但原理都是相通的。為了方便討論,我們先將場景進行抽象
iOS驗證裝置是否支援TouchID和指紋是否匹配列印錯誤程式碼分析處理?
錯誤原因:1、com.apple.LocalAuthentication Code=-6 Biometry is not available on this device.(不支援在iPhone 4s,
iOS 圖像處理 - 圖像拼接
資源 class 圖像拼接 span str screen oat right contex 解決這個問題:將兩個圖像拼接在一起 前提:須要加入Framework:CoreGraphics.framework 源代碼: - (UIImage *) combine:(
ios safari瀏覽器 處理javascript的註釋
safari不能用//來寫註釋,因為Safari把多行代碼放在一行。今天遇到這個奇葩的問題。要寫註釋,需要用/* ---*/本文出自 “北京看看” 博客,請務必保留此出處http://kankan.blog.51cto.com/372369/1944850ios safari瀏覽器 處理javascript的
IOS使用批處理打包
provision detail ons onf arch bsp 參數 打包 light 之前咱們講過 使用命令行打包 下面咱們介紹使用腳本打包,其實腳本和命令行沒有太大的本質區別。 以下是腳本文件: #註意:腳本目錄和xxxx.xcodeproj要在同一個目
iOS 程式碼設定檢視圓角
在某些檢視我們可能需要做圓角處理,此處以UIImageView為例,我們一般的寫法通常是 imageView.layer.mastToBounds = YES; imageView.layer.cornerRadius = imageView.frame.size.width / 2.0;
iOS App中斷處理
- (void)handleInterruption:(NSNotification *)noti { AVAudioSessionInterruptionType type = [noti.userInfo[AVAudioSessionInterruptionTypeKey]
AndroidStudio GiT 處理衝突(兩端更新程式碼,處理程式碼衝突)
1.如果同時有不止1人修改了,專案中的同一個檔案,此時點選pull 會彈出一個訊息,提示說會 override覆蓋掉你的本地版本,此時,點選androidStudio上面的 updateProject按鈕 此時選擇: merge Using Stash 然後點選ok,會提示你有
小程式在頁面中使用的相容ios的時間處理
最近在做小程式。又遇到了一個老問題,後臺返回的時間格式,如2018-09-08 10:00:00,直接處理成9月8日 10:00 在ios中報NAN。處理方式想在頁面中直接進行處理。 新建.wxss檔案 由於wxss不支援es6,所以不能使用es6語法 var fi
python程式碼批量處理圖片resize
出差做PPT,要放一些圖片上去,原圖太大必須resize,十幾張圖片懶得一一處理了,最近正好在學python,最好的學習方式就是使用,於是寫了一個批量處理圖片resize的程式碼,在寫的過程中,熟悉了python自己的os模組和opencv的cv2模組。 程式碼
iOS-程式碼混淆加固策略
對於IOS來說,由於系統是封閉的,APP上架需要通過App Store,安全性來說相當高。但是對於大廠和知名APP而言,別人給的安全保障永遠沒有自己做的來得踏實。所以對於大廠、少部分企業級和金融支付類應用來說加固是相當重要的。 下面是目前幾個專業加固大廠提供的加固策略 網
iOS程式碼程式設計規範 根據專案經驗彙總
帶出幾十位從零開始學iOS的實習生或試用期的開發人員後,覺得真的是千人千面,每個人寫的程式碼都風格迥異,如果沒有一個文件規範,每次都和新人進行口頭的說教,大概自己是不用敲程式碼了,所以吃了虧了就開始編寫iOS的程式設計規範。由於本人在寫iOS程式碼前一直是C語言的開發,所以很多規範都受C語言的影響。
C# 模擬傳送請求到java後臺 java程式碼接收處理引數的問題
前段時間接到一個需求,對接一個C#寫的工具類,給我們的系統後臺上傳資料。 需求不難,很常見,於是為了方便。我就這樣寫了(java框架SSH): C#模擬請求的程式碼 public static void Main(string[] args) {
重構-改善既有的程式碼設計-處理概括關係(11-2)
11.6.提煉子類(Extract Subclass) type Employee struct { _rate int } func (e *Employee) getRate() int { return e._rate }
重構-改善既有的程式碼設計-處理概括關係(11-1)
11.1.欄位上移(Pull Up Field) 11.2.函式上移(Pull Up Method) 11.3.建構函式本體上移(Pull Up Constructor Body) 11.4.函
iOS--上線被拒如何從蘋果返回的崩潰日誌iOS.crash檔案處理找崩點(看這篇就懂了)
2017年底了,現在蘋果上線的越來越嚴,導致被拒的次數也是越來越特多。我們從蘋果給的提示可以看出我們大概崩潰的位置,但是作為程式設計師的我們,找到具體崩潰的點才能更好的修復。 AppStore稽核沒有通過,給了3個crashLog.txt檔案,可是開啟後都是十六進位制的東東(根本不知道
ffmpeg ios程式碼加字幕相關
ios上呼叫ffmpeg命令的配置方式可以參考: http://blog.csdn.net/leixiaohua1020/article/details/47072673 No such filter: 'subtitles' Error opening filters!
IOS 去空格處理 特殊字元處理
1. 去掉字串中兩邊的空格 NSCharacterSet *whiteSpace = [NSCharacterSetwhitespaceAndNewlineCharacterSet]; NSString *strr = [[NSStringalloc]initWith
iOS有關圖片處理的總結 (二)------圖片的混合模式
上一篇我們看了圖片的載入方式,接下來我們看看圖片一下常用的混合模式,我覺得這個最好是先自己玩一玩photoshop,上面有很多混合模式可以自己試驗,這裡我們用程式碼進行嘗試修改圖片的混合模式,這樣可以看到不同的圖片效果。 這裡我們就必須用到繪圖。常用的方法是: [ima
秒殺活動倒計時 iOS程式碼實現
IOS關於大型網站搶購、距活動結束,剩餘時間倒計時的實現程式碼,程式碼比較簡單,大家根據需求適當的新增修改刪除程式碼 1.定義4個 Label 來接收倒計時: @property (weak, nonatomic) IBOutlet UILabel *d