编码规范
编码规范简单来说就为了保证写出来的代码具备三个原则:可复用、易维护、可扩展. 这其实也是面向对象的基本原则. 可复用, 简单来说就是不要写重复的代码, 有重复的部分要尽量封装起来重用.易维护, 就是不要把代码复杂化, 不要去写巨复杂逻辑的代码, 而是把复杂的逻辑代码拆分开一个个小的模块, 这也是Do one thing的概念, 每个模块(或者函数)职责要单一, 这样的代码会易于维护, 也不容易出错. 可扩展则是要求写代码时要考虑后面的扩展需求, 这个属于架构层面的东东, 利用对应的设计模式来保证,本内容主要讨论第1点。
本章通过命名规范和编码规范来详细讨论
- 命名规范
- 编码规范
1. 命名规范
主要涉及常量和变量命名、枚举命名、类及其方法命名,以及分类及其方法命名。
1.1 使用 #define 预处理定义常量。定义一个 ANIMATION_DURATION 常量来表示 UI 动画的一个常量时间,这样代码中所有使用 ANIMATION_DURATION 的地方在编译阶段都会被替换成 0.3。无类型信息,不便于调试。不推荐
#define ANIMATION_DURATION 0.3
1.2 使用类型常量,对于局部常量通常以字符 k 开头,且需要以 static const 修饰。推荐
static const NSTimeInterval kAnimationDuration = 0.3;
1.3 使用类型常量,外部可见,则通常以定义该常量所在类的类名开头。
// 数值常量
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 用枚举表示状态、选项、状态码
// 通用枚举值
typedef NS_ENUM(NSInteger, RoomType) {
RoomType_Normal, // 虚拟诊室
RoomType_Exam, // 考试诊室
RoomType_Talk, // 讨论区诊室
RoomType_XGuang, // 能力 X 光
};
1.5 需要以按位或操作来组合枚举的都应使用 NS_OPTIONS 宏来定义。
// 位移相关操作的枚举值
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 类型,应加上 is/has 前缀。私有实例变量前加一个下划线,如:_myPrivateVarible
@property (nonatomic, strong) UIButton *closeBtn; // 表示关闭的button,是 UIButton 类型
@property (nonatomic, strong) UILabel *titleLabel; // 表示标题的label,是 UILabel 类型
@property (nonatomic, assign) BOOL isShowLeft; // 表示是否显示Left,是 BOOL 类型
- (BOOL)isEqualToString:(NSString *)aString;
- (BOOL)hasPrefix:(NSString *)aString;
// 这是一个成员变量,添加一个下划线
NSString *_userId;
1.7 方法的命名,应该全部使用有意义的单词组成,且以小写字母开头,多单词组合时,后面的单词首字母大写,参数过多时,推荐每个参数各占一行。如果是私有方法建议加上 'private'/'p_' 作为前缀,很容易的将其同公共方法区分开
- (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
// 与字符串操作相关的分类
// 这是文件名
NSString+JudgeString.h
// 这是类名
@interface NSString (JudgeString)
1.10 协议的命名,与类命名相同,需添加 "Delegate" 后缀,如:ReplyViewDelegate
// 协议的命名
@protocol ShaftAdviceViewDelegate;
1.11 图片命名,使用英文,首字母大写,之后每个单词首字母都大写并添加模块作为前缀,避免冲突图片应该与类文件一样,按模块分组放置
// 这是沙河路径
<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
// 这是数据库路劲
<Application_Home>/Documents/app.sqlite
2. 编码规范
- 让别人能读懂的代码
- 可扩展的代码
- 可测试的代码
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 嵌套判断,一旦发现某个条件不符合,立即返回,条理更清晰.
// 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 参数过多,可以聚合成一个 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 的循环引用问题,解决方法很简单, 就是在block体内define一个strong的self, 然后执行的时候判断下self是否还在, 如果在就继续执行下面的操作, 否则return或抛出异常.
// weakSelf 可能被释放,导致奇怪的错误
__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,如需要继承类或执行协议,可以在.h中进行#import类或协议;其他情况下,在.h中声明用@classs声明此类即可。这样可以减少因头文件依赖引起重复编译,提高编译速度,也解决了两个类的相互引用问题.
// 在 .h 文件中通过 class 关键字来引用
@class PatientInfo;
@interface CreatePatient_Get : BaseRestApi
@property (nonatomic, strong) PatientInfo *patientInfo;
@end
// 而在 .m 文件中通过 import 关键字来引用
#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 启动了一个定时器,在控制器销毁的时候,需要关闭定时器.
// 关闭定时器
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (_timer) {
dispatch_source_cancel(_timer);
_timer = nil;
}
}
2.14 建议:在添加通知的时候,可以把每个通知都包装成独立的方法,就可以复用,而不需要重复的写相同的通知。以功能模块来区分通知的类型。
// Preferred:
// 与登录相关的通知
@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 建议:对于某些功能,需要依赖其他的功能才能完成整个过程,可以把这些相互依赖的功能包装成一个独立的方法,便于复用。
// 这是登录,包括邮箱登录接口、手机登录接口、用户信息接口、消息提示接口
// Preferred:
// 该 LogingHelper 类方法包装了登录相关的逻辑和数据的交互
@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 建议:在使用第三发库的时候,比如 AFNetworking 进行网络请求。可以抽象出一个基类,把网络相关的请求重新封装,通过继承的方式,来复用这些功能。好处就是:需要更新 AFNetworking 库,或者迁移到其他的网络库,只需要修改基类的方法。各种 Loading、下拉刷新......等
2.15 建议:所有需要重复使用的代码,都可以包装成一个方法