代码的整洁与否是一个程序员的个人卫生问题。一个程序员穿着可以稍邋遢一些,但是代码要写的干净、利落。
如果你想成为一个更好的程序员,除了要学习语法、设计模式之外,还要学习如何写出整洁有效的代码,这本书会教你如何写出这样的代码。书中代码全部为 Java 语言,如果没有接触过 Java 语言,读起来可能会有点困难。

这本书大致可以分为三个部分:第一部分占据了大约一半的章节,主要介绍编写整洁代码的原则、模式和实践,读起来比较容易理解。如果你读完这部分感觉已经掌握了如何写好整洁代码,那么你要失望了,其实你只学到一点皮毛。要知道,容易学会的东西一般价值都不高。当然如果你是天才,学什么都快,就当我没说。

第二部分主要是对几个复杂性不断增加的案例的研究,这是最有价值的一部分,也是最难读的一部分。在这部分你会读大量的 Java 代码,然后逐渐掌握如何写出整洁有效的代码。

第三部分是从研究案例得到的一些启示与灵感。如果你仔细读了第二部分,这部分将是对你的回报,否则这部分对你来说可能所值无几。

这本书是基于 Java 语言写的,因为语法的差异性,有些规则并不适合 OC。下面我主要针对 OC 总结的一些 Tips。

命名

命名是令程序员最头疼的事之一,良好的命名习惯是写好整洁代码的基本素养,命名遵循的原则就是:有意义且不误导读者。在 Apple 的官方文档 Coding Guidelines for Cocoa 中,有规范的命名规则。作为一名 iOS 开发者,有必要读一下这篇文档。如果说 Apple 官方文档是针对 OC 的定制规则,那么下面说的将是一些通用规则。

1.有意义的命名

只有编程初学者才会用 a,b,sss,tyq 去做变量或常量名,这点相信大家都不会了。在命名的时候,想要表达什么,就去用什么样的名字。在 OC 的编码习惯中,基本都是用完整的单词,尽量不要用缩写,多个单词采用”驼峰标记法”。如果你深受 Windows 的 C 语言 API 毒害的话,赶快忘掉那该死的”匈牙利标记法”吧。例如下面的代码中:

1
2
3
UILabel *nameLbl = [UILabel new];  // nameLbl 中不应把 'Label' 缩写
NSString *logStr = @"this is log"; // 即使是 NSString 类型,也不应该缩写为 Str
UIViewController *loginVC = [UIViewController new]; // 不建议把 ViewController 所以为 VC

Apple 官方指定的一些缩写,在命名时这些变量可以使用缩写。

尽量避免使用一些无意义的”魔法数字”,可以使用枚举、常量、宏或者其他方式代替,在进行全局搜索时也很方便。例如下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1,2,3 各代表什么?
if (state == 1) {...}
if (state == 2) {...}
if (state == 3) {...}

// 如果写成这样,则很明确
typedef NS_ENUM(NSUInteger, VideoState) {
VideoStateOpen = 1,
VideoStatePause,
VideoStateClose
}

if (state == VideoStateOpen) {...}
if (state == VideoStatePause) {...}
if (state == VideoStateClose) {...} // 换成 switch 或许更好一些

// 还有这种的,40.0f 和 23.0f 是什么鬼?为什么不定义为有名字的常量?
CGFloat viewHeight = 40.0f + 23.0f;

2.避免误导

有意义的命名是先决条件,在有意义的基础上,还要做到不能误导读者。不要使用关键字或者一些专属名词命名;不要使用双关语;不要单个使用 ‘o’、’l’ 这种字母,以免与 ‘0’ 、 ‘1’ 混淆读者。下面示例引以为戒:

1
2
3
4
5
6
7
8
9
10
11
12
// list 在很多语言中是一种容器类型,尽量不要作为变量名
NSArray *list = [NSArray new];

// o or 0,1 or l 很容易混淆,尽管现在的 IDE 会对数字和字母有高亮区分,还是不建议这样写
int a = 1
if (O == 1) {
a = O1;
} else {
l = 01;
}

//

OC 中变量、属性、方法、类的命名

在进行团队合作时,通常团队内部会统一一套编码风格,否则在定义”个人信息”时有的用 ‘personData’,有的用 ‘personInfo’ 岂不是乱套了。在没有统一的时候,应按照如下规则。

在对基本类型的变量或属性进行命名时,遵循有意义且不误导原则;在对对象类型的变量或属性进行命名时,经常在名字后面加上对象类型。不要觉得长,OC 的语法命名一向都很长,看 API 就知道了。

1
2
3
4
5
6
7
8
9
10
// 基本类型命名
CGRect avatarViewFrame = {0,0,40,40};

// 对象类型命名,多以 '名称+类型后缀'
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIView *tagView;
@property (nonatomic, copy) NSDictionary *namesArray;

// 也有一些特例,例如 NSString 类型有时候就不带后缀
@Property (nonatomic, copy) NSString *name;

方法命名,要遵循一下几条原则:

  • 以小写字母开始,之后单词的首字母大写,即”驼峰标识”。
  • 如果方法代表对象接收的动作,以动词开始。尽量不要使用 ‘get’、’set’ 命名,set 方法可以使用 ‘set’ 开头命名。
  • 如果方法返回接收者的属性,以 接收者 + 接收的属性 命名。
  • 参数名以小写字母开始,之后的单词首字母大写,不要使用缩写。
1
2
3
4
5
6
7
8
9
10
11
12
// 动词开头
- (void)pushToLoginViewController;
- (void)downloadImageWithURLString:(NSString *)imageURL;
- (CGSize)logoViewSize; // 接受者 + 属性

// set 方法
- (void)setUserName:(NSString *)userName;

// 一些反例
— (NSInteger)getNumber;
- (void)showimage;
- (instancetype)initWithRequest:(NSURLRequest *) req;

类命名,一般在开发项目时,会规定类得前缀。因为 Apple 的 API 前缀一般为两个大写字母,为了避免冲突,自定义类名一般前缀为三个大写字母。例如:

1
2
3
4
5
6
// 所有前缀为 BLC(BoolChow) 缩写
BLCLoginViewController.h
BLCLoginViewController.m

BLCNetWork.h
BLCNetWork.m

在 Xcode 中,选中项目在右侧工具栏中可以设置类的前缀,这样在新建类时会默认加上前缀。如下图:

设置前缀

设置前缀示例

注释

好的代码是不用注释的,好的注释是你想办法不去写注释,听着有点废话,但确实如此。之因为写注释,是因为代码写的比较乱,怕其他人看不懂,因为标上清晰的注释就能掩盖代码的丑陋。但是往往注释写太多,代码显得就 low 了。

OC 的命名一般都使用单词全拼,很少使用缩写。有时候一个方法的名字特别长,读起来像一句话。所以在 OC 中,如果名字起得好,很少用的到注释。当然,在一些开源框架、SDK 或者官方 API 中,头文件中很多方法都会标有大量注释,方便使用者理解,这并不矛盾。为了注释到恰到好处,下面从”什么时候写”,”怎么写”,”在哪写”三个方面简述几条注释的规则。

1.当代码表述不清,或者容易误导读者的时候,加以注释。

最好情况下是将代码表述清楚,但是有时候想要表述的内容太多,代码无法表述,这时候要加上注释进行辅助表述。例如 ViewController 的生命周期方法:

1
2
3
4
// 想要表述的内容太多,以至于方法名字不能表述出来。
- (void)loadView; // This is where subclasses should create their custom view hierarchy if they aren't using a nib. Should never be called directly.

- (void)viewDidLoad; // Called after the view has been loaded. For view controllers created in code, this is after -loadView. For view controllers unarchived from a nib, this is after the view is set.

有时候代码表述的有歧义,可能会误导读者,这时候要加上注释进行辅助表述。而且尽量不要写误导性代码。

在写 SDK 或者其他开源框架时,需要在头文件中添加注释,表述每个方法的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

// Mantle 中,MTLJsonAdapter.h 中对每个方法都做了详细的注释,方便使用者理解
@interface MTLJSONAdapter : NSObject

/// Attempts to parse a JSON dictionary into a model object.
///
/// modelClass - The MTLModel subclass to attempt to parse from the JSON.
/// This class must conform to <MTLJSONSerializing>. This
/// argument must not be nil.
/// JSONDictionary - A dictionary representing JSON data. This should match the
/// format returned by NSJSONSerialization. If this argument is
/// nil, the method returns nil.
/// error - If not NULL, this may be set to an error that occurs during
/// parsing or initializing an instance of `modelClass`.
///
/// Returns an instance of `modelClass` upon success, or nil if a parsing error
/// occurred.
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;

/// Attempts to parse an array of JSON dictionary objects into a model objects
/// of a specific class.
///
/// modelClass - The MTLModel subclass to attempt to parse from the JSON. This
/// class must conform to <MTLJSONSerializing>. This argument must
/// not be nil.
/// JSONArray - A array of dictionaries representing JSON data. This should
/// match the format returned by NSJSONSerialization. If this
/// argument is nil, the method returns nil.
/// error - If not NULL, this may be set to an error that occurs during
/// parsing or initializing an any of the instances of
/// `modelClass`.
///
/// Returns an array of `modelClass` instances upon success, or nil if a parsing
/// error occurred.
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;

@end

2.注释要求简洁、明确、有意义。

既然是辅助表述,就不要啰嗦一顿还没说清楚,也不要喃喃自语。不需要注释的不要画蛇添足,强行注释;需要注释的,应以最简洁的语言将想要表达的意思表述清楚。

1
2
3
4
5
6
7
8
// 例如下面的注释,纯属多余。因为命名已经将意思表达的很清楚。
@property (nonatomic, strong) UIImageView *avatarImageView; ///< 头像视图
@property (nonatomic, strong) NSString *userName; ///< 用户名

/** 播放视频 */
- (void)playVideo {
...
}

3.在正确的位置注释

一般在类的头文件中注释,在源文件中不需要再次注释。例如下面代码,为什么要注释两次?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface CodeReview : NSObject
/**
* 更新用户信息
* @param userModel 用户信息 model
*/
- (void)updateUserInfoWithModel:(UserModel *)userModel;
@end

@implementation CodeReview
/** 更新用户信息 */
- (void)updateUserInfoWithModel:(UserModel *)userModel {
...
}
@end

尽量不要在过程代码中添加注释。除非在过程中有一段不易于理解的代码,可以加注释阐述一下。尤其是有一段代码需要其他开发者注意,这时候可以加注释起到放大、警示作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 下面的方法中有两个问题,一是过程代码中太多的注释;二是方法中执行事件太多,一个方法原则上只执行一件事,后面会说到。
- (void)func {
// 暂停播放
[self.player pause];

// 获取视频播放进度
CGFloat totalTime = self.player.currentItem.Duration.value /
self.player.currentItem.Duration.value.timescale;
CGFloat currentTime = self.player.currentTime.value /
self.player.currentTime.timescale;
NSString *playerProgress = [NSString stringWithFormat:@"%.2f",currentTime / currentTime];

// 上传视频播放进度
NetWorkManager *manager = [NetWorkManager new];
[manager uploadViewProgress:playerProgress];

...
}

注释的样式因注释的内容不同,有着不同的格式。有些格式是能被 Xcode 识别的,有些格式是不能被识别的。在使用 Xcode 编写代码时,会有代码自动提示功能,同时如果一个如果这个变量或方法的注释被识别,则也会显示在代码提示栏中,例如下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface Documentation : NSObject

@property (nonatomic, strong) NSString *personalInfo; ///< 个人信息

@end

#import "ViewController.h"
#import "Documentation.h"

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

Documentation *documentation = [Documentation new];
documentation.pers
}

document

swift 的注释与 OC 略有不同,有兴趣的可以看一下这篇文章对于 OC ,常用的注释方式有一下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1.文件信息的注释,在创建文件时编译器会为我们生成,不可被识别
//
// CodeReivew.h
// BlogTest
//
// Created by boolChow on 17/2/18.
// Copyright © 2016年 xxx All rights reserved.
//

2.头文件(.h)中方法的注释,Xcode8 快捷键 'option + command + /',可以被识别。
/**
* <#description#>
*
* param <#param description#>
* return <#return value description#>
*/

3.源文件(.m)中私有方法的注释,可以和上面一样,也可以按照如下方式。
/** <#descriotion#> */
- (void)func {...}

4.属性的注释,常用两种
@property (nonatomic, strong) UIView *view; ///< <#description#>
@property (nonatomic, strong) UIButton *button; //!< <#description#>

5.过程代码注释,一般使用 '// <#description#>'。
6.枚举类型注释,一般使用 '/** <#descriotion#> */'。

在 Xcode 中,// 这种注释是不能被识别的,能识别的一般有 /** *//////!///<//!<。另外,对于因为废弃而注释掉的代码,或者在调试过程中注释掉的代码,在进行代码提交之前一定要删掉!

格式

排版格式是影响代码整洁度的一个重要因素,虽然现在大部分 IDE 都对代码进行了排版,但是还有一些地方是 IDE 不能做到的,这需要我们手动去排版。我下面将”从小->大”讲述一下格式的细节。

1.大括号 ‘{}’

对于大括号,有的习惯将在方法后面紧跟大括号的左半部分 ‘{‘,有的习惯换一行在写 ‘{‘,我习惯前者。

1
2
3
4
5
6
7
8
9
10
11
// 普通方法
- (void)func {
...
}

// if else 等类似语句,else 跟在 if 语句结束后面,而不是换行。如果 if 语句中只有一句代码,也建议加上大括号。
if (condition) {
...
} else {

}

2.空格

在适当的地方使用空格,能够使代码显得更加清晰。

定义方法时,方法的 ‘-‘ 与方法返回值间添加空格,参数之间添加空格,方法名与方法开头的大括号之间添加空格

1
2
3
4
5
6
7
8
9
// 建议
- (UIView *)createBannerWithImage:(UIImage *)image frame:(CGRect)frame {
...
}

// 不建议
-(UIView *)createBannerWithImage : (UIImage *)image frame:(CGRect)frame{
...
}

定义属性时,@property 与属性关键字之间空格,多个属性关键字之间使用空格,属性类型与属性名称之间使用空格,’*’紧跟属性名称。

1
2
3
4
5
6
// 建议
@property (nonatomic, strong, readonly) NSString *password;

// 不建议
@property (nonatomic,strong,readonly)NSString*password;
@property (nonatomic, strong, readonly) NSString * password;

运算符两侧之间添加空格。

1
2
3
4
5
6
7
8
9
10
11
// 建议
if (isOpen == YES) {
...
}

if (username != nil) {
...
}

self.backgroundColor = [UIColor clearColor];
self.state = [value isEqualString:@""] ? @"close" : @"open";

添加注释时,’//‘与内容之间添加空格,注释之间如果有英文单词,中英文之间添加空格。

1
2
3
// 建议
// 根据 URL 对内容进行预加载
- (void)prelaodDataWithURL:(NSString *)url;

3.空行

恰当是用空行能够使代码结构更加清晰,但是滥用空行会使代码显得更加糟糕。加空行的原则是:相关的代码组成一个”代码块”,两个代码块之间添加空行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 建议范例
- (instancetype)initWithUserModel:(UserModel *)userModel {
self = [super init];

if (self) {
_userModel = userModel;
_imageArray = [NSArray array];
_attribute = [NSDictionary dictionary];

[self loadData];
}

return self;
}

// 不建议
- (UIView *)func {
CGFloat height = self.avatarImageView.frame.size.height;
CGFloat width = self.avatarImageView.frame.size.width;

UILabel *descriptionLabel = [UILabel new];

descriptionLabel.frame = CGRectMake(20.f,30.f,height,width);

...

return newView;

}

4.模块分类

对于头文件,可以分为系统 API、Pod文件、自定义 Model、自定义 View、自定义 Controller、自定义 Utils 等。具体分类依个人情况,只要合理即可。另外,个人习惯按照头文件长度从小到大排列。虽然现在有一些插件可以管理头文件,但是一开始就写清晰更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// System
#import <CoreData/CoreData.h>
#import <CoreMedia/CoreMedia.h>
#import <AVFoundation/AVFoundation.h>

// Pod
#import <Mantle/Mantle.h>
#import <YYImage/YYImage.h>
#import <CocoaLumberjack/DDLog.h>
#import <AFNetworking/AFNetworking.h>

// Model
#import "FeedModel.h"
#import "UserModel.h"
#import "CommentModel.h"

// View
#improt "BannerView.h"
#import "PersonalInfoView.h"

// Controller
#import "VideoDetailViewController.h"
#import "PersonalInfoViewController.h"

对于属性分类,类似于头文件,例如按照 Data、View、Bool、Custom Class 等类型。依个人喜好,合理即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Data
@property (nonatomic, assign) CGFloat maxHeight;
@property (nonatomic, strong) NSString *username;
@property (nonatomic, strong) NSString *password;
@property (nonatomic, copy) NSDictionary *params;

// View
@property (nonatomic, strong) UIView *subView;
@property (nonatomic, strong) UIView *leftLine;
@property (nonatomic, strong) UITableView *feedTableView;

// Custom Class
@Property (nonatomic, strong) TimelineRequest *request;
@property (nonatomic, strong) UserInfoManager *userInfoManager;

一个类中,方法之间通过 ‘#pragma mark - xxx’ 进行模块划分。例如在一个 ViewController 中,按照 init Method、Setup Method、LifeCycle Method、Public Method、Private Method等。例如下面途中即按照模块进行分割。

separateCodeBlock

5.一些原则

对于代码的书写格式,需要遵循一些原则。类之间如何进行归类划分,属于设计模式的范畴,这里只从一个类说起。对于一个类文件,垂直方向代码长度最多建议 700~800 行(超过 500 行就有点不能忍了),如果超过了 1000 行,则应进行拆分抽取,否则可阅读行会很差;水平方向,建议最多不要超过 80 个字符,如果超过,建议进行换行。例如下面注册 Notification 时:

1
2
3
4
5
6
7
8
// 不建议
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateData) name:ZHIDidDeleteMediaNotification object:nil];

// 建议
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateData)
name:UpdateDataNotification
object:nil];

为了提高可阅读行,一个类中的方法,最好按照调用顺序进行编写,并进行模块分类。这样在其他人阅读代码时不用跳来跳去。

按照正常人的审美观,适当的缩进、空格、空行、对齐,可以提高代码整洁度与美感。按照正常人的逻辑思维,例如从小到大、由表及里、从短到长等一些逻辑顺序去划分模块,编写代码,可以提高代码可阅读性。按照正常人的单元理解能力,垂直方向编写适当范围行数代码,水平方向按照适当范围换行,也可以提高代码的可阅读行。

方法与数据结构

1.只做一件事

方法是对一段过程代码的封装,为了保证代码的整洁性,这段代码最好只执行一个事件,即一个方法最好只做一件事。先理解怎样才算是”一件事”,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {
[self setupNotification];
[self setupNavgationItme];
[self setupTableView];
}

- (void)setupTableView {
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, -64.0f, MTScreenWidth, MTScreenHeight) style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.tableFooterView = [UIView new];
self.tableView.tableHeaderView = self.tableHeaderView;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.backgroundColor = [UIColor colorWithHexString:@"f2f2f2"];
[self.view addSubview:self.tableView];

[self.tableView registerClass:[CommonCell class] forCellReuseIdentifier:kCommonCellReuseIdentifier];
}

上面代码中,viewDidLoad 方法做了三个操作:注册通知、初始化导航栏、初始化 tableView,这是三件事还是一件事呢?写过 OC 代码的都知道,这些操作”均属于初始化一些基本信息的操作”,这三个操作都在同一个抽象层级上,因此算是一件事。

同样的 setupTableView 方法中,分别进行了:新建 tableView 对象,设定 tableView 代理、数据、footerView 等一系列信息,注册 cell。那么这算几件事呢?这些操作均属于初始化 tableView,因此这个方法也就只做了”初始化 tableView“这一件事。

刚才提到了”同一个抽象层级”,一个方法中应该只有一个抽象层级。如果混杂不同的抽象层级,会让人迷惑,方法看起来就像一个垃圾桶。例如下面的代码:

1
2
3
4
5
6
7
- (void)viewDidLoad {
self.dataURL = [NSString stringWithFormat:@"%@%@%@",host,baseURL,userID];
[self setupView];
[self setupNotification];

[self uploadData];
}

上述代码虽然只有几行,确混论不堪。拼接 dataURL 属于较低抽像层级;初始化视图和注册通知属于中间抽象层级;上传数据属于较高抽象层级。短短几行尚且能够读懂,多了之后读起来会乱七八糟。不建议这样写。

一个好的方法应该遵循这样的原则:在尽量短小的情况下,所有过程代码属于同一抽象层级,按照自顶向下的顺序书写代码。如果方法还能拆分,证明不合格。

2.逻辑语句

每种语言基本都会有 if-elsefor循环switch 等这些逻辑语句,过多的使用这些语句,会使代码变得冗长、丑陋。当你的一段代码嵌套2~3个 if-else 语句或者 for循环时,你就应该考虑一下是否有更优雅的写法呢。下面通过代码来分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 给一个变量赋值需要这么多行代码吗?
if (self.isFriend) {
self.permission = @"YES";
} else {
self.permission = @"NO"
}

for (int i=0; i<objcArray.count; i++) {
Object *obj = objcArray[i];
NSLog(@"%@",[obj description]);
}

// 或许这种写法更简洁呢
self.permission = self.isFriend ? @"YES" : @"NO";

for (Object *obj in objcArray) {
NSLog(@"%@",[obj description]);
}

1
2
3
4
5
6
7
8
9
// if 判断中包含多个条件
if (name != nil && password != nil && phone != nil && sex != nil) {...}

// 这种情况最好封装成方法,if 判断中不要有太多的判断参数
if ([self userInfoIsEmpty]) {...}

- (BOOL)userInfoIsEmpty {
return name != nil && password != nil && phone != nil && sex != nil;
}

这些语句是一些基本的语句,用起来简单,不用太走心,同时容易把代码写的冗长不堪。因此在写代码时要尽量减少这些语句的使用,用其他更加优雅的方式代替。

使用这些语句也暴露出一个问题,证明方法中有多处逻辑判断,每种条件下对应一个事件,是这个方法中包含太多的事件,极易违反”只做一件事”这一原则,也不易于阅读。

3.参数

一个方法,好情况下是没有参数,其次是一个、两个、三个(init 方法稍有特殊,可能会有多个参数)。当一个方法参数超过三个,那说明其中的一些参数可以封装为类了。

方法是对过程代码的封装,参数越多,暴露的内容就越多,封装性就越差。过多的参数,也不易于理解。

不建议向一个方法中传入布尔类型参数,否则的话就说明这个方法很有可能不只做一件事。YES 的时候会这样做,NO 的时候会那样做。

4.结构化编程

每个方法,每个方法中的每个代码块都应该有一个入口、一个出口。遵循这个原则,意味着每个方法中只有一个 return 语句,循环中不能有 breakcontinue 语句,更不能有 goto 语句。结构化编程规范,对于小方法助易不大,只有在大的方法中,这些规范才会有明显的好处。所以,在保持方法短小的前提下,偶尔出现 returnbreakcontinue 语句没有坏处,甚至比单入单出原则更有表达力。

5.时序性耦合

一个方法的过程代码中,有时会出现时序性耦合。例如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)viewDidLoad {
[self registerCell];
[self registerNotification];
[self setupData];
[self setupTableView];
}

- (void)registerCell {
[self.tableView registerClass:[EmptyTableViewCell class] forCellReuseIdentifier:kEmptyCellReuseIdentifier];
[self.TableView registerClass:[CommonCell class] forCellReuseIdentifier:kCommonCellReuseIdentifier];
}

- (void)setupTableView {
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, -64.0f, MTScreenWidth, MTScreenHeight) style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.tableFooterView = [UIView new];
self.tableView.tableHeaderView = self.tableHeaderView;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self.view addSubview:self.tableView];
}

上述代码中,在 viewDidLoad 方法中进行一些初始化操作。但是细心一些你会发现,”注册 cell“在”初始化 tableView“之前。但是在注册 cell 的时候会用到 tableView,这时候 tableView 还是 nil。所以这两个方法调用是有顺序的,否则就会出错。但是如果这样写,即使写对了,后面再修改代码时也容易颠倒位置,导致错误。那么最合适的做法应是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将两个方法合并
- (void)setupTableView {
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, -64.0f, MTScreenWidth, MTScreenHeight) style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.tableFooterView = [UIView new];
self.tableView.tableHeaderView = self.tableHeaderView;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self.view addSubview:self.tableView];

[self.tableView registerClass:[EmptyTableViewCell class] forCellReuseIdentifier:kEmptyCellReuseIdentifier];
[self.TableView registerClass:[CommonCell class] forCellReuseIdentifier:kCommonCellReuseIdentifier];
}

类与对象

面向对象语言的特性就是抽象、封装、继承、多态,而”类”则将这些特性全部包含在内。所以一个好的类,便是将这些特性展现出来。

1.类应该短小

在前面已经提过,一个类文件代码长度再好保持在 700~800 行以内。虽然一个类文件可能会有多个类与分类,但是具体到每个类,也应该保持短小,方便理解和阅读。

这里说的短小,并不单指代码行数方面的衡量。对于一个类,最主要的衡量方式是:权责。一个类不能有太多的权责,即使代码行数比较短,但是负责了太多事情,这个类仍然是一个臃肿的类。如果一个类有了太多权责,那么这个类就需要拆分,从而保持整洁性。这样会带了另外一个问题:类爆炸,似的项目中有太多短小单一的类。然而每达到一定规模的系统都会包括大量逻辑和复杂性,系统应该由许多短小的类而不是少量巨大的类组成。

2.封装

程序设计的原则是高内聚、低耦合。因此,一个类不应该暴露太多的属性(变量)与方法。下面是在使用 OC 进行程序设计时的一些建议。

对于头文件的引入,大多在 .m 文件中引入。如果 .h 文件中需要使用到其他的类,优先使用 @class 关键字引入,不能解决需求的情况下,再在 .h 文件中引入其他类的头文件。

对于属性,除了使用 nonatomic/atomicstrong/weak/copy关键字修饰之外,最好还要标明读写属性。如果一个属性可以是 readonly,就不要写成 readwrite,不要让其他的类随意修改本类的属性。

对于方法,尽量不要暴露太多的方法。一个类不要提供给外部太多乱七八糟的方法,以保证类的封装性。在提供方法时,方法的参数最好标明 nonull/nullable 属性,而且一个方法不要有太多参数。

3.内聚

类应该只有少量的实体变量。类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。在 OC 中,如果一个属性在多个方法中被使用,是比较合理的;但是如果一个属性只在一个方法中使用,那么就有必要考虑一下这个属性的存在性,是否可以用局部变量代替。

本书精华

本书的第 14~16 章是对几个案例分析,第 17 张是一些总结性知识点,是本书的精华所在。如果用烹饪做比喻,那么前面的一些章节只是告诉你该用什么原料,油盐酱醋放的剂量以及火候;后面三章才是给你演示一遍整个烹饪过程,真正的授之以渔。

第 14 章《逐步改进》,是对一个自定义 Args 类的重构。通过逐步改进的方式,对原有类进行一步步分解。在这一过程中,遵循前面叙述的一些原则,对方法、变量进行大规模修改。其中有一点很值得学习,在进行重构之前,作者先写了一个覆盖这个类所有方法的单元测试。每次修改一些代码之后,都会跑一遍测试,如果测试通过,则继续修改;否则就需要找出问题,修复之后再继续。这非常值得我们学习,想象一下如果你不这样做,在重构完代码后,发现无法编译了,也不知道在哪个阶段修改出了问题,那会是多么糟糕的场景!

第 15 章《JUnit内幕》,是对 JUnit 框架部分代码的重构。同样,通过对代码的层层剖析,对代码的命名、函数的结构、模块的划分进行了一些改进,将前面讲的一些原则进行了推演。

第 16 章《重构SerialDate》,是对开源框架 SerialDate 的重构过程。先让代码跑通,然后从代码的注释开始,对代码进行修剪、结构调整。如书中所写,这一章分了两部分:”首先,让它工作”;”让它作对”。

第 17 章《味道与启发》,是一个总结性章节。对前面的一些规则,以及在重构过程中的一些启发的总结。如果你仔细阅读的前面的章节,尤其是第 14~16 章,这一章的内容你会充分的吸收。

总结

以上,是我对这本书做的一个总结。本书值得学习的地方远不止这些,如果能用一篇文章说清楚,作者干嘛还要写一本书。因此,如果你对代码的整洁性要求十分严苛,想成为一个更好的 coder,建议你读一读这本书。

参考资料