# Effective Objective-C 2.0 Tips 總結 Chapter 3 & Chapter 4
## Chapter 3 接口與 API 設計
- Tips 15 使用前綴避免明明空間沖突
- Objective-C 沒有命名空間,所以我們在起名時要設法避免命名沖突
- 避免命名沖突的方法就是使用前綴
- 應用中的所有名稱都需要加前綴(包括實現文件中的全局變量和純 C 函數)
- Tips 16 提供“全能(designated)初始化方法”
- 一個會被所有初始化方法調用到的初始化方法
- 當底層數據存儲機制變化時,只需要修改這個方法就可以了,不需要改動其他初始化方法
- 如果超類的全能初始化方法不適用于子類,或是與超類不同,那么需要覆蓋這個超類方法
- 子類的全能初始化方法都應該調用超類的對應方法,逐級向上
- Tips 17 實現 description 方法
- 在數組字典等集合對象打印時,都會調用對象的 `description` 方法,方便調試
- 系統默認的 `description` 方法對于自定義的對象并沒有輸出較為有用的內容,所以可以實現這個方法方便我們顯示對象
- 在調試時會調用 `debugDescription` 方法(也就是在調試時 lldb 中輸入 po 時調用的將會是 `debugDescription`),所以實現他可以幫助我們調試時獲得更多的信息
- 可以使用 `NSDictionary` 來實現 `description` 方法,這樣顯示和輸出都會比較方便,例如:
```
// Header File
// 這里我略微修改了下原書中的示例代碼
@interface EOCLocation : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic) CGFloat latitude;
@property (nonatomic) CGFloat longitude;
@end
// 我們要是可以使用 NSLog(@"%@", eoc_location) 直接輸出這個對象的經緯度(也就是所有屬性)就好了,那么可以參考下面的寫法實現 description 方法
@implementation EOCLocation
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",
[self class],
self,
@{
@"title": self.title,
@"latitude": @(self.latitude),
@"longitude": @(self.longitude),
}];
}
@end
```
- Tips 18 盡量使用不可變對象
- 減少 side effect,在使用了一段時間的 RAC 和學習函數式思想后,一定程度上理解了不可變對象的好處
- 具體開發實踐中,應盡量把對外公布的屬性設為只讀,并且有必要時才對外公布,否則使用私有屬性
- 對于只讀屬性,可以不用指定內存管理語義(也就是 strong,weak,copy)
- 對外只讀的屬性可以在對象內部,也就是類擴展(Class-Extension 也叫 Class-Continuation)中重新聲明為可讀寫的
- 可以使用 GCD 來設置讀寫操作為同步操作
- 就算屬性設置為只讀,在外部仍可以使用 KVC 來訪問這些屬性,例如:`[object setValue:@"value" forKey:@"propertyName"]`
- 集合屬性(Array,Set,Dictionary)可以提供只讀屬性供外界使用(內部保存可變類型的變量,返回該變量的不可變拷貝),并提供操相應的操作方法,例如下面例子中,使用 `-addFriend:` 和 `-removeFriend:` 方法來實現對 `friends` 集合的操作,這樣保證了添加或刪除盆友的操作對象是知情的。對于直接修改 `friends` 集合的操作對象是不知情的,這樣可能會導致對象內各數據的不一致。
```
@interface EOCPerson : NSObject
@property (nonatomic, strong, readonly) NSSet *friends;
@end
@implementation EOCPerson {
NSMutableSet *_internalFriends;
}
- (NSSet *)friends {
return [_internalFriends copy];
}
- (void)addFriend:(EOCPerson *)person {
[_internalFriends addObject:person];
}
- (void)removeFriend:(EOCPerson *)person {
[_internalFriends removeObject:person];
}
@end
```
- 不要在返回的對象上查詢其是否是可變對象并對其進行操作,同上條這樣對對象集合屬性的直接修改,容易產生 bug
- Tips 19 使用清晰而協調的命名方式
- 方法名的風格要保證與自己的代碼或是需要集成的框架一致,也就是上下文需要一致,這點最重要放第一
- 起名遵循 Objective-C 的命名規范,這樣的接口名字一定程度上提示了接口的作用
- 方法名言簡意賅,從左到右讀起來最好像一個日常用于中的句子
- 方法名里不要使用縮略后的類型名稱
- Objective-C 的方法名相較其他語言要長一些,但是可以更好地表達方法的作用,以及各個參數的意義,比如:
```
Rectangle *recgangle = new Rectangle(5.0f, 10.0f);
// 不如下面的命名方式
Rectangle *recgangle = [Rectangle initWithSize:(float)width :(float)height];
// 不如下面的命名方式
Rectangle *recgangle = [Rectangle initWithWidth:(float)width andHeight:(float)height];
```
- Tips 20 為私有方法名加前綴
- 因為在 Objective-C 中沒有私有方法,所有對象都可以響應任意消息,并且可以通過 runtime 獲取對象可以相應的消息,所以我們使用特定的命名來區分私有方法
- 在使用 Category 或繼承系統中或第三方庫中的類的時候,可以防止命名沖突
- C 語言中使用 `_` 下劃線作為系統內部函數的開頭所以我們不能使用 `_` 作為私有方法的前綴(蘋果的官方庫也使用 `_`)
- 原書作者建議使用 `p_` 來作為私有方法的前綴,個人建議使用開發中項目使用的前綴小寫來作為類前綴,比如上文的 `EOCPerson` 中添加私有方法可以使用 `eco_privateMethodName:`,這樣的前綴在第三方類庫中出現重復的概率比較小
- Tips 21 理解 Objective-C 錯誤模型
- ARC 在默認情況下并不是異常安全的,也就是拋出異常的時候,在作用域末尾應該釋放的對象將不會被釋放
- 可以使用 `-fobjc-arc-exceptions` 來告訴編譯器需要生成異常安全的代碼,但是這樣會引入一些額外代碼,并且在不拋出異常時也會執行這部分代碼
- 就算不使用 ARC 使用異常也很容易寫出內存泄漏的代碼,因為需要在拋出異常前清理所有申請的資源,所以現在我們只在非常罕見(嚴重錯誤,比如:抽象類中的方法沒有實現)的情況下拋出異常,拋出之后不需要考慮回復的問題,并且退出應用,這樣就不用編寫復雜的異常安全代碼
- 對于不嚴重的錯誤,我們通過返回 nil/0 或是使用 `NSError` 來處理,`NSError` 中包含了錯誤處理所需的各種信息,我們自己的錯誤需要規劃和設置好對應的 Error Domain,Error Code
- 一般通過 delegate 來傳遞錯誤 `- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error` 或是輸入參數返回錯誤 `- (BOOL)doSomething:(NSError **)error`
- Tips 22 理解 `NSCopying` 協議
- 實現 `NSCopying` 接口可以讓類實現拷貝(`copy`)方法,`- (id)copyWithZone:(NSZone *)zone` 中的 `zone` 是以前開發時使用的內存區參數,目前已經不使用了,可以不用考慮他
- 實現 `NSMutableCopying` 協議支持可變拷貝(`mutableCopy`)方法
- 對象拷貝時需要決定是深拷貝還是淺拷貝,一般情況下用淺拷貝
- 絕大多數情況下 `NSCopying` 實現的都是淺拷貝,所以如果使用深拷貝,建議創建一個單獨的方法來完成
## Chapter 4 協議(Protocol)和分類(Category)
- Tips 23 使用委托(delegate)和數據源(data source)協議進行對象間通信
- 委托模式(delegate pattern):對象把應對某個行為的責任委托給了另一個類
- 類似我們經常使用的 `UITableView`,`UITableViewDelegate` 和 `UITableViewDataSource` 分別定義了如何處理事件的接口和如何提供數據的接口,實現這兩個接口為 `UITableView` 提供交互邏輯和顯示數據,`UITableView` 本身只負責顯示獲取到的數據
- 委托模式同樣適用于異步事件,比如網絡請求完成后,回調委托對象將結果傳遞回去,實現事件的異步處理
- 使用委托對象的對象中的委托對象屬性需要設置為 weak,防止循環引用
- 使用委托中的方法時,使用 `respondsToSelector:` 先查詢委托對象是否實現了該方法,特別是在協議中使用 `@option` 關鍵字標注的可選方法
- 委托中的方法名要清晰明確,需要說明事件的來源,當前的事件,以及為什么委托對象需要獲取這個事件,所有委托方法都需要將發起委托的對象發送到委托對象(作為第一個參數),讓委托對象判斷事件來源
- 針對需要進行多次調用的委托對象(例如網絡加載時下載進度),可以通過結構體等方法,在設置委托對象的時候,一次檢查需要響應的方法并記錄,之后在使用的時候,直接通過記錄結果來判斷是否實現了某個方法,不用每次都使用 `respondsToSelector:` 方法來查詢是否實現,例:
```
@interface EOCNetworkFetcher() {
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags;
}
@end
@implementation EOCNetworkFetcher
- (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate {
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
@end
// 在需要調用 delegate 方法的時候
if (_delegateFlags.didUpdateProgressTo) {
[_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}
```
- Tips 24 將類的實現代碼分散到便于管理的多個 Category 中
- 在開發的過程中,類的代碼只會越來越大,那么我們可以通過分類機制將類的代碼打散,根據業務分散到不同的分類中
- 應該把私有方法放到叫(Private)的分類中,隱藏實現細節
- Tips 25 總是為第三方分類的分類名稱加前綴
- 如果分類中出現同名方法,容易出現奇怪的 bug,所以在為其他類添加分類的時候,分類名稱和分類中的方法需要添加你自己使用的前綴
- Tips 26 勿在分類中聲明屬性
- 分類中可以定義方法(包括 getter 和 setter),但是不要定義屬性,因為在分類中定義的屬性不會生成實例變量
- 雖然有 `objc_setAssociatedObject` 魔法可以用,但是這容易導致內存管理問題,因為無法使用屬性記錄內存管理語義,但是建議一般情況下不使用
- 分類的主要作用是擴張類的功能,而不是封裝數據
- Tips 27 使用 Class-Continuation 分類,隱藏實現細節
- Class-Continuation 分類必須定義在該類的實現文件中,并且可以聲明實例變量,并且建議僅以此種方式增加實例變量
- 頭文件中聲明為只讀的屬性,可以在實現文件中的 Class-Continuation 分類中擴展為可讀寫
- 私有方法原型,和私有屬性,都可以放到 Class-Continuation 分類中
- 在 Class-Continuation 分類中可以聲明實現的接口,并且外部不會知道
- 可以通過私有屬性很好的封裝 C++/Objective-C++ 的代碼,提供 Objective-C 的接口給其他代碼使用
- Tips 28 通過協議提供匿名對象
- 使用類似 `@property(nonatomic, weak) id<ProtocolName> delegate;` 提供匿名類型對象作為 delegate,可以隱藏類名
- 對于類型不重要,只需要提供可向應方法的對象,可以使用匿名對象,隱藏實現細節
## 對于 Chapter 1 的補充
第一章第四條中,多用類型常量,少用 `#define` 預處理指令中,建議大家使用類型常量而不是 `#define` 來定義常量,這里增加一個補充內容,swift 中,我們可以使用 `struct` 中的靜態變量來聲明常量,這樣帶來的一個好處是使用和分類管理非常方便
Xcode 8.0 帶的 clang 4.0 后開始支持類常量,也就是定義屬性的時候,可以加入 `class` 來修飾屬性,這樣這個屬性是屬于類的,于是乎,我們可以這樣使用常量了
```
NSString *notificationName = XXXConstant.notificationNames.XXXUserDidLoginNotificationName;
```
看上去比類型常量長一些,不過似乎還算比較好看
定義的時候需要這樣定義:
```
@interface XXXConstantNotificationNames : NSObject
@property(nonatomic, readonly) NSString *XXXUserDidLoginNotificationName;
@end
@interface XXXConstant : NSObject
@property(nonatomic, class, copy) XXXConstantNotificationNames *notificationNames;
@end
```
并且,類常量是不會被 synthesize 的,也就是說編譯器不會自動為類常量創建相應的變量,所以在實現文件中,我們需要這么寫
```
@implementation XXXConstantNotificationNames
- (NSString *)XXXUserDidLoginNotificationName {
return @"XXXUserDidLoginNotificationName";
}
@end
@implementation XXXConstant
static XXXConstantNotificationNames *_notificationNames = nil;
+ (void)load {
_notificationNames = [[XXXConstantNotificationNames alloc] init];
}
- (XXXConstantNotificationNames *) {
reutrn _notificationNames;
}
@end
```
看上去比定義一個 `kXXXUserDidLoginNotificationName` 字符串常量,麻煩了非常多,但是相信在項目代碼量不斷增加,以及工程變得越來越復雜以后,這樣的做法對于代碼管理上是非常有幫助的