iOS 开发iOS程序猿

编码规范

2018-03-01  本文已影响39人  于先笙

编码规范简单来说就为了保证写出来的代码具备三个原则:可复用、易维护、可扩展. 这其实也是面向对象的基本原则. 可复用, 简单来说就是不要写重复的代码, 有重复的部分要尽量封装起来重用.易维护, 就是不要把代码复杂化, 不要去写巨复杂逻辑的代码, 而是把复杂的逻辑代码拆分开一个个小的模块, 这也是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. 编码规范

  1. 让别人能读懂的代码
  2. 可扩展的代码
  3. 可测试的代码

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 建议:所有需要重复使用的代码,都可以包装成一个方法

上一篇 下一篇

猜你喜欢

热点阅读