編碼規(guī)范簡單來說就為了保證寫出來的代碼具備三個原則:可復(fù)用、易維護、可擴展. 這其實也是面向?qū)ο蟮幕驹瓌t. 可復(fù)用, 簡單來說就是不要寫重復(fù)的代碼, 有重復(fù)的部分要盡量封裝起來重用.易維護, 就是不要把代碼復(fù)雜化, 不要去寫巨復(fù)雜邏輯的代碼, 而是把復(fù)雜的邏輯代碼拆分開一個個小的模塊, 這也是Do one thing的概念, 每個模塊(或者函數(shù))職責(zé)要單一, 這樣的代碼會易于維護, 也不容易出錯. 可擴展則是要求寫代碼時要考慮后面的擴展需求, 這個屬于架構(gòu)層面的東東, 利用對應(yīng)的設(shè)計模式來保證,本內(nèi)容主要討論第1點。
本章通過命名規(guī)范和編碼規(guī)范來詳細(xì)討論
- 命名規(guī)范
- 編碼規(guī)范
1. 命名規(guī)范
主要涉及常量和變量命名、枚舉命名、類及其方法命名,以及分類及其方法命名。
1.1 使用 #define 預(yù)處理定義常量。定義一個 ANIMATION_DURATION 常量來表示 UI 動畫的一個常量時間,這樣代碼中所有使用 ANIMATION_DURATION 的地方在編譯階段都會被替換成 0.3。無類型信息,不便于調(diào)試。不推薦
#define ANIMATION_DURATION 0.3
1.2 使用類型常量,對于局部常量通常以字符 k 開頭,且需要以 static const 修飾。推薦
static const NSTimeInterval kAnimationDuration = 0.3;
1.3 使用類型常量,外部可見,則通常以定義該常量所在類的類名開頭。
// 數(shù)值常量
EOCViewClass.h
extern const NSTimeInterval EOCViewClassAnimationDuration;
EOCViewClass.m
const NSTimeInterval EOCViewClassAnimationDuration = 0.3;
// 字符串常量
EOCViewClass.h
extern NSString *const EOCViewClassStringConstant;
EOCViewClass.m
NSString *const EOCViewClassStringConstant = @"EOCStringConstant";
1.4 用枚舉表示狀態(tài)、選項、狀態(tài)碼
// 通用枚舉值
typedef NS_ENUM(NSInteger, RoomType) {
RoomType_Normal, // 虛擬診室
RoomType_Exam, // 考試診室
RoomType_Talk, // 討論區(qū)診室
RoomType_XGuang, // 能力 X 光
};
1.5 需要以按位或操作來組合枚舉的都應(yīng)使用 NS_OPTIONS 宏來定義。
// 位移相關(guān)操作的枚舉值
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
// 然后就可以通過組合的方式來使用
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
1.6 變量和對象的命名,給一個對象命名時建議采用 ’修飾+類型‘ 的方式,對于 BOOL 類型,應(yīng)加上 is/has 前綴。私有實例變量前加一個下劃線,如:_myPrivateVarible
@property (nonatomic, strong) UIButton *closeBtn; // 表示關(guān)閉的button,是 UIButton 類型
@property (nonatomic, strong) UILabel *titleLabel; // 表示標(biāo)題的label,是 UILabel 類型
@property (nonatomic, assign) BOOL isShowLeft; // 表示是否顯示Left,是 BOOL 類型
- (BOOL)isEqualToString:(NSString *)aString;
- (BOOL)hasPrefix:(NSString *)aString;
// 這是一個成員變量,添加一個下劃線
NSString *_userId;
1.7 方法的命名,應(yīng)該全部使用有意義的單詞組成,且以小寫字母開頭,多單詞組合時,后面的單詞首字母大寫,參數(shù)過多時,推薦每個參數(shù)各占一行。如果是私有方法建議加上 'private'/'p_' 作為前綴,很容易的將其同公共方法區(qū)分開
- (id)initWithAccount:(NSString *)account
password:(NSString *)psw;
- (id)privateMethod;
- (id)p_method;
1.8 類的命名,首字母大寫,之后每個單詞首字母都大寫,使用能夠反映類功能的名詞短語,使用前綴來避免命名空間沖突,如:ABC+類名(ABCUserInfo)
// 這是一個模型
@interface UserModel : JSONModel
// 這是一個視圖
@interface BaseNavView : BaseView
// 這是一個視圖控制器
@interface LoginVC : BaseVC
// 這是一個接口
@interface Login_Post : BaseRestApi
// 添加前綴
// 這是一個模型
@interface TLQUserModel : JSONModel
// 這是一個視圖
@interface TLQBaseNavView : BaseView
// 這是一個視圖控制器
@interface TLQLoginVC : BaseVC
// 這是一個接口,POST 方式
@interface TLQLogin_Post : BaseRestApi
// 這是一個接口,GET 方式
@interface TLQCreatePatient_Get : BaseRestApi
1.9 分類的命名,與類命名相同,此外需添加要擴展的類名和 “+”,如:NSString+JudgeString
// 與字符串操作相關(guān)的分類
// 這是文件名
NSString+JudgeString.h
// 這是類名
@interface NSString (JudgeString)
1.10 協(xié)議的命名,與類命名相同,需添加 "Delegate" 后綴,如:ReplyViewDelegate
// 協(xié)議的命名
@protocol ShaftAdviceViewDelegate;
1.11 圖片命名,使用英文,首字母大寫,之后每個單詞首字母都大寫并添加模塊作為前綴,避免沖突圖片應(yīng)該與類文件一樣,按模塊分組放置
// 這是沙河路徑
<Application_Home>/Documents/
<Application_Home>/Library/Preferences/
<Application_Home>/Library/Caches/
<Application_Home>/tmp/
// 這是用戶頭像路徑
<Application_Home>/Library/Preferences/Icons/(userId).png
// 這是 app 圖片資源, xxx 表示圖片唯一名稱
<Application_Home>/Library/Caches/Icons/(xxx).png
// 這是用戶信息路勁
<Application_Home>/Library/Preferences/Infos/(userId).plist
// 這是數(shù)據(jù)庫路勁
<Application_Home>/Documents/app.sqlite
2. 編碼規(guī)范
- 讓別人能讀懂的代碼
- 可擴展的代碼
- 可測試的代碼
2.1 判斷 nil 或者 YES/NO
// Preferred:
if (someObject) { ... }
if (!someObject) { ... }
// Not preferred:
if (someObject == YES) { ...}
if (someObject != nil) { ...}
2.2 條件賦值.
// Preferred:
result = object ? : [self createObject];
// Not preferred:
result = object ? object : [self createObject];
2.3 初始化方法,推薦使用字面量方式.
// Preferred:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;
// Not Preferred:
NSArray *names = [[NSArray alloc] initWithObjects:@"Brian",@"Matt",@"Chris",@"Alex",@"Steve", nil];
NSDictionary *productManagers = [[NSDictionary alloc] initWithObjectsAndKeys:@"iPhone", @"Kate", @"iPad", @"Kamal", nil];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *shouldUseLiterals = [NSNumber numberWithInteger:10018];
2.4 BOOL 賦值,使用三目運算符,更簡潔.
// Preferred:
BOOL isAdult = age > 18;
// Not preferred:
BOOL isAdult;
if (age > 18)
{
isAdult = YES;
}
else
{
isAdult = NO;
}
// Preferred:
result= num1 > num2 ? num1:num2;
// Not preferred:
if (num1 > num2)
{
result = num1;
}
else
{
result = num2;
}
2.5 拒絕死值,使用變量,即間接的方式,每次修改的時候容易被遺忘,地方多了修改就麻煩了.
// Preferred:
if (car == Car.Nissan)
// or
const int adultAge = 18; if (age > adultAge) { ... }
// Not preferred:
if (carName == "Nissan")
// or
if (age > 18) { ... }
2.6 嵌套判斷,一旦發(fā)現(xiàn)某個條件不符合,立即返回,條理更清晰.
// Preferred:
if (!user.UserName) return NO;
if (!user.Password) return NO;
if (!user.Email) return NO;
return YES;
// Not preferred:
BOOL isValid = NO;
if (user.UserName)
{
if (user.Password)
{
if (user.Email) isValid = YES;
}
}
return isValid;
2.7 參數(shù)過多,可以聚合成一個 model 類,代碼更簡潔.
// Preferred:
- (void)registerUser(User *user)
{
// to do...
}
// Not preferred:
- (void)registerUserName:(NSString *)userName
password:(NSString *)password
email:(NSString *)email
{
// to do...
}
2.8 Block 的循環(huán)引用問題,解決方法很簡單, 就是在block體內(nèi)define一個strong的self, 然后執(zhí)行的時候判斷下self是否還在, 如果在就繼續(xù)執(zhí)行下面的操作, 否則return或拋出異常.
// weakSelf 可能被釋放,導(dǎo)致奇怪的錯誤
__weak typeof(self) weakSelf = self;
dispatch_block_t block = ^{
[weakSelf doSomething]; // weakSelf != nil
// preemption, weakSelf turned nil
[weakSelf doSomethingElse]; // weakSelf == nil
};
// strongSelf 不會被釋放 - 推薦使用
__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething]; // strongSelf != nil
// preemption, strongSelf still not nil
[strongSelf doSomethingElse]; // strongSelf != nil
}
else {
// Probably nothing...
return;
}
};
2.9 建議:#import 頭文件按模塊來分,更直觀.
// Cell
#import "LogInCell.h"
#import "LogOutCell.h"
// Setting
#import "AppPreference.h"
#import "AppDelegate.h"
// VC
#import "LoginVC.h"
#import "UserInfoVC.h"
#import "SettingVC.h"
// Notify
#import "LoginNotify.h"
// View
#import "DSTabbar.h"
#import "InvitationView.h"
// Api
#import "LoginStatus_Get.h"
2.10 建議:@class與#import,如需要繼承類或執(zhí)行協(xié)議,可以在.h中進行#import類或協(xié)議;其他情況下,在.h中聲明用@classs聲明此類即可。這樣可以減少因頭文件依賴引起重復(fù)編譯,提高編譯速度,也解決了兩個類的相互引用問題.
// 在 .h 文件中通過 class 關(guān)鍵字來引用
@class PatientInfo;
@interface CreatePatient_Get : BaseRestApi
@property (nonatomic, strong) PatientInfo *patientInfo;
@end
// 而在 .m 文件中通過 import 關(guān)鍵字來引用
#import "PatientInfo.h"
@implementation CreatePatient_Get
@end
2.11 注釋,可以采用 “ /**/ ” 和 // 兩種注釋符號,涉及多行注釋時,第一種。對于一行代碼的注釋可放在前一行或本行上,不允許放在下一行。顯而易見的代碼不需要加注釋.
// 這里是多行注釋
/**
* 登錄接口
*
* @param account 賬號
* @param psw 密碼
*/
- (id)initWithAccount:(NSString *)account
password:(NSString *)psw;
// 這里是單行注釋,寫在上一行
// 病程ID
@property (nonatomic, strong) NSString *status_id;
// 這里是單行注釋,寫在同一行
@property (nonatomic, strong) NSString *status_id; // 病程ID
2.12 注冊通知時,在視圖控制器銷毀的時候,需要刪除通知.
// 刪除通知
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[LoginNotify sharedInstance] removeLoginObserver:self];
}
2.13 啟動了一個定時器,在控制器銷毀的時候,需要關(guān)閉定時器.
// 關(guān)閉定時器
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (_timer) {
dispatch_source_cancel(_timer);
_timer = nil;
}
}
2.14 建議:在添加通知的時候,可以把每個通知都包裝成獨立的方法,就可以復(fù)用,而不需要重復(fù)的寫相同的通知。以功能模塊來區(qū)分通知的類型。
// Preferred:
// 與登錄相關(guān)的通知
@interface LoginNotify : NSObject
+ (instancetype)sharedInstance;
#pragma mark - login
- (void)addLoginObserver:(id)target selector:(SEL)selector;
- (void)removeLoginObserver:(id)target;
- (void)postLoginNotify;
#pragma mark - louout
- (void)addLogoutObserver:(id)target selector:(SEL)selector;
- (void)removeLogoutObserver:(id)target;
- (void)postLogoutNotify;
#pragma mark - automatic login
- (void)addAutomaticLoginObserver:(id)target selector:(SEL)selector;
- (void)removeAutomaticLoginObserver:(id)target;
- (void)postAutomaticLoginNotify;
@end
// Not preferred:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
2.15 建議:對于某些功能,需要依賴其他的功能才能完成整個過程,可以把這些相互依賴的功能包裝成一個獨立的方法,便于復(fù)用。
// 這是登錄,包括郵箱登錄接口、手機登錄接口、用戶信息接口、消息提示接口
// Preferred:
// 該 LogingHelper 類方法包裝了登錄相關(guān)的邏輯和數(shù)據(jù)的交互
@interface LoginHelper : NSObject
+ (void)loginWithUser:(NSString *)username
Password:(NSString *)password
Completion:(void (^)(BOOL success, NSString *errorMsg, Login_Post *loginPost,UserInfo_Get *userinfoGet))block;
@end
// Not preferred:
Login_Post *loginApi = nil;
if ([username isValidateMobile])
{
// 手機登錄
loginApi = [[Login_Post alloc] initWithAccount:username password:password];
}
else
{
// 郵箱登錄
loginApi = [[Email_Post alloc] initWithAccount:username password:password];
}
if (llginApi.code != 0)
{
// 登錄失敗,返回
return;
}
UserInfo_Get *userInfoApi = [[UserInfo_Get alloc] initWithUserId:loginApi.user_id];
if (userInfoApi.code != 0)
{
// 獲取用戶信息失敗,返回
return;
}
// 其他相互依賴的功能....
2.15 建議:在使用第三發(fā)庫的時候,比如 AFNetworking 進行網(wǎng)絡(luò)請求??梢猿橄蟪鲆粋€基類,把網(wǎng)絡(luò)相關(guān)的請求重新封裝,通過繼承的方式,來復(fù)用這些功能。好處就是:需要更新 AFNetworking 庫,或者遷移到其他的網(wǎng)絡(luò)庫,只需要修改基類的方法。各種 Loading、下拉刷新......等
2.15 建議:所有需要重復(fù)使用的代碼,都可以包裝成一個方法