<翻译>[Code Smells]#import被滥用!如何管理文件的依赖关系?

#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。

这也是为什么我在单独的头文件中声明协议,而不是将协议的声明放在其他类的文件中。这样可以保证依赖关系的简洁。

##代码异味:在.m文件中过多的使用#import

我们很少在实现文件中使用@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加进来,也可以彻底的把它们清掉。下面的情况在你身上很可能会发生:

  1. 你会在新建一个类的时候习惯性的加上一批#import,因为那些是你比较常用的一些工具类或其他的。但是,你实际上可能根本就不会用到所有的这些工具。
  2. 你在删掉类中的某个属性变量或方法或协议等的时候,忘记了要同时删掉与它对应的头文件的引用。

基本上,这是比较混乱的管理方式。一次不经意的把混乱的@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)。采用这样的方法后,你自己都有可能对结果感到惊讶。“高內聚”正在由一个纯粹的概念变成一个你可以真实感受到的东西。

##总结

让我们再加把劲!下面是管理文件依赖关系时需要注意的一些地方:

###头文件中

  1. 使用#import引用父类和要遵循的协议。
  2. 使用提前声明(forward-declare)处理其他的。
  3. 尽量清掉其他的#import。
  4. 在单独的头文件中声明协议,减少不必要的依赖关系。
  5. 不要过多的使用提前声明(forward declaration),否则就Large Class了。

###实现文件中

  1. 将那些根本就没用到的#import干掉。
  2. 假如仅仅是传递对象,没有向对象发送消息,就把#import换成@class。
  3. 对于可以模块化的东西,尽量做成一个单独的库。
  4. 不要过多的使用#import,否则就Large Class了。

好,我们来检查一下自己的代码吧!

原文链接:http://qualitycoding.org/file-dependencies/

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结