代码整洁之道
代码的整洁与否是一个程序员的个人卫生问题。一个程序员穿着可以稍邋遢一些,但是代码要写的干净、利落。
如果你想成为一个更好的程序员,除了要学习语法、设计模式之外,还要学习如何写出整洁有效的代码,这本书会教你如何写出这样的代码。书中代码全部为 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 | UILabel *nameLbl = [UILabel new]; // nameLbl 中不应把 'Label' 缩写 |
Apple 官方指定的一些缩写,在命名时这些变量可以使用缩写。
尽量避免使用一些无意义的”魔法数字”,可以使用枚举、常量、宏或者其他方式代替,在进行全局搜索时也很方便。例如下面例子:
1 | // 1,2,3 各代表什么? |
2.避免误导
有意义的命名是先决条件,在有意义的基础上,还要做到不能误导读者。不要使用关键字或者一些专属名词命名;不要使用双关语;不要单个使用 ‘o’、’l’ 这种字母,以免与 ‘0’ 、 ‘1’ 混淆读者。下面示例引以为戒:
1 | // list 在很多语言中是一种容器类型,尽量不要作为变量名 |
OC 中变量、属性、方法、类的命名
在进行团队合作时,通常团队内部会统一一套编码风格,否则在定义”个人信息”时有的用 ‘personData’,有的用 ‘personInfo’ 岂不是乱套了。在没有统一的时候,应按照如下规则。
在对基本类型的变量或属性进行命名时,遵循有意义且不误导原则;在对对象类型的变量或属性进行命名时,经常在名字后面加上对象类型。不要觉得长,OC 的语法命名一向都很长,看 API 就知道了。
1 | // 基本类型命名 |
方法命名,要遵循一下几条原则:
- 以小写字母开始,之后单词的首字母大写,即”驼峰标识”。
- 如果方法代表对象接收的动作,以动词开始。尽量不要使用 ‘get’、’set’ 命名,set 方法可以使用 ‘set’ 开头命名。
- 如果方法返回接收者的属性,以 接收者 + 接收的属性 命名。
- 参数名以小写字母开始,之后的单词首字母大写,不要使用缩写。
1 | // 动词开头 |
类命名,一般在开发项目时,会规定类得前缀。因为 Apple 的 API 前缀一般为两个大写字母,为了避免冲突,自定义类名一般前缀为三个大写字母。例如:
1 | // 所有前缀为 BLC(BoolChow) 缩写 |
在 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.注释要求简洁、明确、有意义。
既然是辅助表述,就不要啰嗦一顿还没说清楚,也不要喃喃自语。不需要注释的不要画蛇添足,强行注释;需要注释的,应以最简洁的语言将想要表达的意思表述清楚。
1 | // 例如下面的注释,纯属多余。因为命名已经将意思表达的很清楚。 |
3.在正确的位置注释
一般在类的头文件中注释,在源文件中不需要再次注释。例如下面代码,为什么要注释两次?
1 | @interface CodeReview : NSObject |
尽量不要在过程代码中添加注释。除非在过程中有一段不易于理解的代码,可以加注释阐述一下。尤其是有一段代码需要其他开发者注意,这时候可以加注释起到放大、警示作用。
1 | // 下面的方法中有两个问题,一是过程代码中太多的注释;二是方法中执行事件太多,一个方法原则上只执行一件事,后面会说到。 |
注释的样式因注释的内容不同,有着不同的格式。有些格式是能被 Xcode 识别的,有些格式是不能被识别的。在使用 Xcode 编写代码时,会有代码自动提示功能,同时如果一个如果这个变量或方法的注释被识别,则也会显示在代码提示栏中,例如下面的示例:
1 | @interface Documentation : NSObject |
swift 的注释与 OC 略有不同,有兴趣的可以看一下这篇文章对于 OC ,常用的注释方式有一下几种:
1 | 1.文件信息的注释,在创建文件时编译器会为我们生成,不可被识别 |
在 Xcode 中,//
这种注释是不能被识别的,能识别的一般有 /** */
、///
、//!
、///<
、//!<
。另外,对于因为废弃而注释掉的代码,或者在调试过程中注释掉的代码,在进行代码提交之前一定要删掉!
格式
排版格式是影响代码整洁度的一个重要因素,虽然现在大部分 IDE 都对代码进行了排版,但是还有一些地方是 IDE 不能做到的,这需要我们手动去排版。我下面将”从小->大”讲述一下格式的细节。
1.大括号 ‘{}’
对于大括号,有的习惯将在方法后面紧跟大括号的左半部分 ‘{‘,有的习惯换一行在写 ‘{‘,我习惯前者。
1 | // 普通方法 |
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 | // 建议 |
运算符两侧之间添加空格。
1 | // 建议 |
添加注释时,’//‘与内容之间添加空格,注释之间如果有英文单词,中英文之间添加空格。1
2
3// 建议
// 根据 URL 对内容进行预加载
- (void)prelaodDataWithURL:(NSString *)url;
3.空行
恰当是用空行能够使代码结构更加清晰,但是滥用空行会使代码显得更加糟糕。加空行的原则是:相关的代码组成一个”代码块”,两个代码块之间添加空行。
1 | // 建议范例 |
4.模块分类
对于头文件,可以分为系统 API、Pod文件、自定义 Model、自定义 View、自定义 Controller、自定义 Utils 等。具体分类依个人情况,只要合理即可。另外,个人习惯按照头文件长度从小到大排列。虽然现在有一些插件可以管理头文件,但是一开始就写清晰更好。
1 | // System |
对于属性分类,类似于头文件,例如按照 Data、View、Bool、Custom Class 等类型。依个人喜好,合理即可。
1 | // Data |
一个类中,方法之间通过 ‘#pragma mark - xxx’ 进行模块划分。例如在一个 ViewController 中,按照 init Method、Setup Method、LifeCycle Method、Public Method、Private Method等。例如下面途中即按照模块进行分割。
5.一些原则
对于代码的书写格式,需要遵循一些原则。类之间如何进行归类划分,属于设计模式的范畴,这里只从一个类说起。对于一个类文件,垂直方向代码长度最多建议 700~800 行(超过 500 行就有点不能忍了),如果超过了 1000 行,则应进行拆分抽取,否则可阅读行会很差;水平方向,建议最多不要超过 80 个字符,如果超过,建议进行换行。例如下面注册 Notification
时:
1 | // 不建议 |
为了提高可阅读行,一个类中的方法,最好按照调用顺序进行编写,并进行模块分类。这样在其他人阅读代码时不用跳来跳去。
按照正常人的审美观,适当的缩进、空格、空行、对齐,可以提高代码整洁度与美感。按照正常人的逻辑思维,例如从小到大、由表及里、从短到长等一些逻辑顺序去划分模块,编写代码,可以提高代码可阅读性。按照正常人的单元理解能力,垂直方向编写适当范围行数代码,水平方向按照适当范围换行,也可以提高代码的可阅读行。
方法与数据结构
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-else
、for循环
、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 | // if 判断中包含多个条件 |
这些语句是一些基本的语句,用起来简单,不用太走心,同时容易把代码写的冗长不堪。因此在写代码时要尽量减少这些语句的使用,用其他更加优雅的方式代替。
使用这些语句也暴露出一个问题,证明方法中有多处逻辑判断,每种条件下对应一个事件,是这个方法中包含太多的事件,极易违反”只做一件事”这一原则,也不易于阅读。
3.参数
一个方法,好情况下是没有参数,其次是一个、两个、三个(init 方法稍有特殊,可能会有多个参数)。当一个方法参数超过三个,那说明其中的一些参数可以封装为类了。
方法是对过程代码的封装,参数越多,暴露的内容就越多,封装性就越差。过多的参数,也不易于理解。
不建议向一个方法中传入布尔类型参数,否则的话就说明这个方法很有可能不只做一件事。YES 的时候会这样做,NO 的时候会那样做。
4.结构化编程
每个方法,每个方法中的每个代码块都应该有一个入口、一个出口。遵循这个原则,意味着每个方法中只有一个 return
语句,循环中不能有 break
或 continue
语句,更不能有 goto
语句。结构化编程规范,对于小方法助易不大,只有在大的方法中,这些规范才会有明显的好处。所以,在保持方法短小的前提下,偶尔出现 return
、break
、continue
语句没有坏处,甚至比单入单出原则更有表达力。
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/atomic
、strong/weak/copy
关键字修饰之外,最好还要标明读写属性。如果一个属性可以是 readonly
,就不要写成 readwrite
,不要让其他的类随意修改本类的属性。
对于方法,尽量不要暴露太多的方法。一个类不要提供给外部太多乱七八糟的方法,以保证类的封装性。在提供方法时,方法的参数最好标明 nonull/nullable
属性,而且一个方法不要有太多参数。
3.内聚
类应该只有少量的实体变量。类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。在 OC 中,如果一个属性在多个方法中被使用,是比较合理的;但是如果一个属性只在一个方法中使用,那么就有必要考虑一下这个属性的存在性,是否可以用局部变量代替。
本书精华
本书的第 14~16 章是对几个案例分析,第 17 张是一些总结性知识点,是本书的精华所在。如果用烹饪做比喻,那么前面的一些章节只是告诉你该用什么原料,油盐酱醋放的剂量以及火候;后面三章才是给你演示一遍整个烹饪过程,真正的授之以渔。
第 14 章《逐步改进》,是对一个自定义 Args
类的重构。通过逐步改进的方式,对原有类进行一步步分解。在这一过程中,遵循前面叙述的一些原则,对方法、变量进行大规模修改。其中有一点很值得学习,在进行重构之前,作者先写了一个覆盖这个类所有方法的单元测试。每次修改一些代码之后,都会跑一遍测试,如果测试通过,则继续修改;否则就需要找出问题,修复之后再继续。这非常值得我们学习,想象一下如果你不这样做,在重构完代码后,发现无法编译了,也不知道在哪个阶段修改出了问题,那会是多么糟糕的场景!
第 15 章《JUnit内幕》,是对 JUnit
框架部分代码的重构。同样,通过对代码的层层剖析,对代码的命名、函数的结构、模块的划分进行了一些改进,将前面讲的一些原则进行了推演。
第 16 章《重构SerialDate》,是对开源框架 SerialDate
的重构过程。先让代码跑通,然后从代码的注释开始,对代码进行修剪、结构调整。如书中所写,这一章分了两部分:”首先,让它工作”;”让它作对”。
第 17 章《味道与启发》,是一个总结性章节。对前面的一些规则,以及在重构过程中的一些启发的总结。如果你仔细阅读的前面的章节,尤其是第 14~16 章,这一章的内容你会充分的吸收。
总结
以上,是我对这本书做的一个总结。本书值得学习的地方远不止这些,如果能用一篇文章说清楚,作者干嘛还要写一本书。因此,如果你对代码的整洁性要求十分严苛,想成为一个更好的 coder,建议你读一读这本书。