#import被滥用!如何管理文件的依赖关系?
像所有的基于C的语言一样,Objective-C通常都是成对的:一个头文件,一个实现文件。每一个文件都可以使用#import引入其他的头文件。假如你在写#import的时候不是很care,小心自己给自己埋了一个文件依赖的定时炸弹。假如这样一直不care下去会有什么后果呢?该如何才能拆掉这个炸弹呢?
##文件依赖关系 首先要干掉.m文件中那些没有必要的#import。为什么要这样做呢?因为#import会强制你添加其他的文件到当前的项目工程中。在一个单独的不会跟其他项目有交集的项目中这无所谓,但是,你要是想在其他的项目中重用这些原文件就有问题了,因为你不得不添加#import的那些文件(这些引添加的文件可能和项目工程没有任何关系)。
但是,在.h文件中的那些没有必要的#import引起的问题就更加严重了,完全就是指数级的问题了!仅仅是因为一个头文件引用了另一个头文件,而另一个头文件又会引用其他的头文件,如此下去。试想一下这样的依赖关系图:
A.h引用B.h和C.h,且B.h又引用了D.h。如此一来,若你要在项目工程中使用A,就必须同时把B,C,D也添加进来。你要知道,这是已经一个很简单的依赖关系了。但是,假如还有一些没有必要的#import混了进来,那么这个关系图可就要失控了。
##问题:不断增长的编译时间 文件依赖关系也会造成编译成本的增加。引入D.h后,Xcode就不得不重新编译D.m,B.m,和A.m。在一个小项目中这貌似没什么大不了的,但是,在大项目中,你就会有深陷泥潭难以前进一步的感觉。不得不说,人们总是告诉我:那不重要,赶快结束项目才是王道。但是,说这样的话的人有几个做过测试驱动开发(TDD)呢?测试驱动开发(TDD)可以对代码修改作出快速反馈(In TDD,unit tests give Feedback about the code you just changed.)。你减少的反馈越多,你就越处于有利的地位(The more you can tighten that Feedback loop,the more you can stay “in the zone”.)。即使最后只减少了几秒钟的编译时间,也会造成不一样的结果(Even a few seconds can make a difference.)。
##问题:那些隐式的依赖关系 “既然,头文件中使用#import会造成编译时间的增加,将#import写到实现文件不就可以了吗。”假如你这样想,就错了,在实现文件中也要避免乱用#import。在实现文件中,这种依赖关系依然是存在的,虽然不那么明显了。我们往下看:
还是使用前面的关系图,稍微变一下。在A.m中引用B.h和C.h,B.m又引用D.h。这里引用D不会引起重新编译的问题,是另一个问题。当你在项目工程中引入A的时候,你必须引入B,C,D,这个大家都知道。但是,真实的情况是这样的:你引入A的时候,看了一下A.m文件,知道要引入B和C。然后,只有你看了B.m才知道还有引入D。这种依赖关系是很隐秘的,因为有时你根本就看不到.m文件的,只有等到编译并提示错误时,才能猜测一下。
并且更加糟糕的是,你刚刚尝试着添加了B,编译后发现还有错误,接着尝试着添加了D,如此下去。在这种无聊的猜谜游戏面前是个人都会崩溃的。
##代码异味:在.h文件中过多的使用#import 现在,我们尝试优化一下文件依赖关系,首先从头文件开始,然后再实现文件。头文件中的Code Smell很明显:过多的使用#import。我们要决定哪些#import是必须的,那些是要避免的。
假设我们定义了一个Foo类,继承自类Superclass,并遵循两个协议,如下:
@interface Foo : Superclass <Protocol1,Protocol2> // ... @end
很明显,我们必须要#import定义了Superclass,Protocol1和Protocol2的头文件。
但是,那些属性变量、其他地方使用的协议以及方法的参数和返回值等涉及的类或协议等怎么处理呢?让我们看下面这个例子:
@interface Foo : Superclass <Protocol1,Protocol2> { Bar *bar; } @property(nonatomic,retain) id <DelegateProtocol> delegate; - (void)methodWithArg:(Baz *)baz; - (Qux *)qux; @end
在这个例子中,我们添加了Bar,DelegateProtocol,Baz,Qux这些类或协议。我们还要再写几个#import呢?答案是:一个也不需要写!我们只需要在@interface之前提前声明(forward-declare)一下它们即可:
@class Bar; @class Baz; @class Qux; @protocol DelegateProtocol;
可能你习惯将所有的@class提前声明(forward declaration)整合到一个里面,但是我推荐每一个都单独声明。这样做可以快速地检查是否有漏写或者重复,同时也可以方便查看有多少个@class声明。
注意:若要#import的类在框架(framework)中,像UIKit,只用#import这个框架(framework)就可以了,没有必要依次#import这个框架中的每个使用的类。一个框架(framework)就像一个已编译的代码块(a single prebuilt chunk),且都有一个主头文件,因此在同一个层面上,直接#import框架不会影响文件的依赖关系。在使用任何通用的框架( frameworks)或库(libraries)时,这样的#import方式都是合理的;当然,那些只针对特定项目的框架使用起来可能会有不同。
好,让我们回到例子中,我们仅仅需要#import父类以及需要遵循的协议的头文件:
#import "Superclass.h" #import "Protocol1.h" #import "Protocol2.h"
可能还有一些像枚举(enum)和类型定义(typedef)这样的非面向对象的声明,需要使用#import来引入。尽量避免这样的#import,因为一般来说,除了上面必须要#import的之外,其他的#import都是Code Smell。
这也是为什么我在单独的头文件中声明协议,而不是将协议的声明放在其他类的文件中。这样可以保证依赖关系的简洁。
我们很少在实现文件中使用@class等提前声明(forward declaration),因为在实现文件中我们基本上是给对象发送消息,而不是传递对象。( Though if your class is the middle-man of a delegation,you will find times when a method takes an argument from a return value and passes it back as its own return value. Then see if you can use forward declaration and avoid the #import.)
所以,在.m文件中我们无法像在头文件一样通过提前声明(forward declaration)来去掉没必要的#import。不管是.h文件还是.m文件,貌似随着时间的推移,#import的数量都会慢慢的增加。我们可以毫不费力气的把那些不是特别需要的#import加进来,也可以彻底的把它们清掉。下面的情况在你身上很可能会发生:
- 你会在新建一个类的时候习惯性的加上一批#import,因为那些是你比较常用的一些工具类或其他的。但是,你实际上可能根本就不会用到所有的这些工具。
- 你在删掉类中的某个属性变量或方法或协议等的时候,忘记了要同时删掉与它对应的头文件的引用。
基本上,这是比较混乱的管理方式。一次不经意的把混乱的@import删掉就可以削减掉一些不必要的文件依赖关系。在 why #import order matters中我会详细地说一下#import。
但是,尽管有时你清掉了那些没有必要的#import,你还是会陷入一层一层的#import列表之中。在开发的过程中的那种情绪,很容易使你将好些东西全部写到一个类中,造成了低內聚,高耦合。结果还是一个槽糕的依赖关系。
在Martin Fowler的书Refactoring中,他描述了一个称为Large Class的Code Smell,是因为在Large Class中包含了过多的属性变量。我想说:使用过多的#import也是Large Class的一种(甚至使用过多的@class这样的提前声明(forward declaration)也可以是Large Class的一种)。按照针对Large Class的建议:使用提炼类(Extract Class)或提炼子类(Extract Subclass)。采用这样的方法后,你自己都有可能对结果感到惊讶。“高內聚”正在由一个纯粹的概念变成一个你可以真实感受到的东西。
##总结
让我们再加把劲!下面是管理文件依赖关系时需要注意的一些地方:
###头文件中
- 使用#import引用父类和要遵循的协议。
- 使用提前声明(forward-declare)处理其他的。
- 尽量清掉其他的#import。
- 在单独的头文件中声明协议,减少不必要的依赖关系。
- 不要过多的使用提前声明(forward declaration),否则就Large Class了。
###实现文件中
- 将那些根本就没用到的#import干掉。
- 假如仅仅是传递对象,没有向对象发送消息,就把#import换成@class。
- 对于可以模块化的东西,尽量做成一个单独的库。
- 不要过多的使用#import,否则就Large Class了。
好,我们来检查一下自己的代码吧!
原文链接:https://www.f2er.com/javaschema/286023.html