正文
}
- (void)sip:(id)water
{
//sip water
}
@end
这样,我们只需要扫一眼Person.h就能明白,Person类对哪些类产生了依赖,比直接在.m中引用清晰多了。
不知道大家有没有好奇过,为什么在Objective C中会有.h文件的存在,为什么不像Java,Swift一样一个文件代表一个类?使用.h文件有利有弊。
.h文件最大的意义在于将声明和实现相隔离。声明是告诉外部我支持哪些功能,实现是支撑这些功能背后的代码逻辑。在我们阅读一个类的.h文件的时候,它最主要的作用是透露两个信息:
-
我(Person类)依赖了哪些外部元素
-
我(Person类)提供哪些接口供外部调用
所以.h文件应该是我们代码耦合的关键所在,当我们犹豫一个类的Property要不要放到.h文件中去声明时,要思考这个Property是不是必须暴露给外部。一旦暴露到.h文件中,就增加了依赖和耦合的几率。有时候Review代码,只要看.h文件是否清晰,就大概能猜测这个类设计者的水平。
当我们把Cup类做为Person的Property声明时,就表明Person与Cup之间存在必要的依赖,我们把这种依赖放到头文件中来,起到一目了然的效果。这比方式一清晰了不少,但有另一个问题,Cup暴露出去以后,外部元素可以随意修改,当内部执行drink的时候,可能另一个线程将cup置空了,影响正常的业务流程。
方式三:.h ReadOnly Property
方式二中,Person类在对Cup产生依赖的同时,也承担了cup随时被外部修改的风险。当然做直观的做法是将Cup类作为ReadOnly的property,同时提供一个对外的setter:
//Person.h
@interface Person : NSObject
@property (nonatomic, strong, readonly) Cup* cup;
- (void)setPersonCup:(Cup*)cup;
- (void)drink;
@end
有同学可能会问,这和上面的做法有什么区别,不一样都有读写的接口吗?最大的区别是增加了检查和干扰的入口。
当我Debug的时候,经常需要检查某个Propery到底是被谁修改了,Setter中设置一个断点调试起来方便不少。同时,我们还可以使用Xcode的Caller机制,查看当前Setter都被那些外部类调用了,分析类与类之间的关联是很有帮助。
Person.m中Setter方法还提供了我们拓展功能的入口,比如我们需要在Setter中增加多线程同步Lock,当Person.m中的其他方法在使用Cup时,Setter必须等待完成才能执行。又比如我们可以在Setter中实现Copy On Write机制:
//Person.m
- (void)setPersonCup:(Cup*)cup {
Cup* anotherCup = [cup copy];
_cup = anotherCup;
}
这样,Person类就可以避免和外部类共享同一个Cup,杜绝使用同一个水杯的卫生问题 ;)
总之,单独的Setter方法让我们对代码有更大的掌控能力,也为后续接手维护你代码的同学带来了方便,利己利人。
方式四:init 注入
使用带Setter的Property虽然看上去好了不少,但Setter方法可以被任意外部类随时随刻调用,对于Person.m中使用Cup的方法来说,多少有些不安心,万一用着用着被别人改了呢?
为了避免被随意修改,我们可以采用init注入的方式,Objective C中的designated initializer正是为此而生:
//Person.h
@interface Person : NSObject
- (instancetype)initWithCup:(Cup*)cup;
- (void)drink;
@end
去掉Property,将Cup的设置放入init方法中,这样Person类对外就只提供一次机会来设置Cup,init之后,外部类就没有其他机会来修改Cup了。
这是使用最多,也是比较推荐的方式。只在对象被创建的时候,去建立与其他对象的关系,把可变性降低到一定程度。那这种方式是否也有什么缺点呢?
通过init的方式设置cup,杜绝了外部因素的影响,但如果内部持有了cup对象,那么内部的函数调用依然可以通过各种姿势与Cup类产生耦合,比如:
//Person.m
@interface Person ()
@property (nonatomic, strong) Cup* myCup;