blog.sunnyxx.com

我是前言

学习objc时,尤其是先学过其他编程语言再来看objc时,总会对objc的声明的关键字interface感到有点奇怪,在其它面向对象的语言中通常由class关键字来表示,而interface在java中表示的却大约相当于objc的protocol,这个关键字的区别究竟代表了objc语言的设计者怎样的思想呢,在objc类设计中需要注意哪些问题呢?接下来对这个问题进行一些思考和探究.


interface?

先来段Wiki:

In object-oriented programming, a protocol or interface is a common means for unrelated objects to communicate with each other. These are definitions of methods and values which the objects agree upon in order to cooperate.

接口约定了对象间交互的属性和方法,使得对象间无需了解对方就可以协作。
说的洋气点就是解耦嘛,细心点也能发现Wiki中interfaceprotocol表示了相近的语义。
引用我和项目组架构师讨论有关interface的问题时他的说法:

interface就是一个object定义的可以被外界影响的方式

说着他指了下旁边桌子上放着的一把伞,说,这把伞我可以打开它,打开这个动作就是它的一个interface,桌子旁边还放着一个盒子,虽然它和伞都放在这张桌子上,但是它们之间永远不会互相影响,所以:

interface只存在于能互相影响的两者间


@interface生成了class?

学习objc时最早接触的就是怎么写一个类了,从.h中写@interface声明类,再从.m中写@implementation实现方法,所以,objc中写一个@interface就相当于c++中写一个class。但这是真的么?

写个小test验证一下:
有两个类,SarkDarkSark类只有.m文件,其中只写@implementationDark类只有.h头文件,其中只写@interface,然后如下测试代码:

1
2
Class sarkClass = NSClassFromString(@"Sark");
Class darkClass = NSClassFromString(@"Dark");

NSClassFromString方法调用了runtime方法,根据类名将加载进runtime的这个类找出来,没有这个类就回返回空(Nil)。
结果是sarkClass存在,而darkClass为空,说明什么?是否说明其实@implementation才是真正的Class?
进一步,不止能取到这个没有@interface的类,还可以正常调用方法(因为万能的runtime)

如下面的测试代码:

1
2
Sark *sark = [Sark new];
[sark speak];

要是没有@interface的声明,类名,方法名都会报错说找不到,但是可以像下面一样绕一下:

1
2
3
Class cls = NSClassFromString(@"Sark");
id obj = [cls performSelector:NSSelectorFromString(@"new")];
[obj performSelector:NSSelectorFromString(@"speak")];

其实,从rewrite后的objc代码可以发现,对于消息的发送,恰恰就是会被处理成类似上面的代码,使用字符串mapping出Classselctor等再使用objc_msgSend()进行函数调用,如下面所示:

1
2
3
// 经过clang -rewrite-objc 命令重写后的代码
Sark *sark = ((id (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Sark"), sel_registerName("new"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)sark, sel_registerName("speak"));

对比@interface和@implementation

@interface 我们干过的事:

  1. 继承
  2. 声明协议
  3. 定义实例变量(@interface后面加大括号那种)
  4. 定义@property
  5. 声明方法

@implementation 我们干过的和可以干的事:

  1. 继承
  2. 定义实例变量
  3. 合成属性(@synthesize和@dynamic)
  4. 实现方法(包括协议方法)

@implementation干一些事情用的相对较少,但是是完全合法的,如这样用:

1
2
3
@implementation Sark : NSObject {
NSString *_name;
}

通过对比可以发现,@interface对objc类结构的合成并无决定性作用,加上无决定性是因为如果没有@interface会丢失一些类自省的原始数据,如属性列表和协议列表,但对于纯粹的对象消息发送并无影响。
所以说,可以得出这么一个结论,objc中@interface就是为了给调用者看的,是和调用者的一个protocol,没错,就是protocol

对比@interface和@protocol

与其把@implementation扯进来不如对比下@protocol

我理解objc的@interface@protocal间唯一的区别就是是否和一个类型绑定,这让我想起来鸭子类型(Duck typing), wiki链接

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

Duck type在objc的体现无疑就是@protocol了,我们常用id<XXXDelegate> delegate的方式声明一个delegate,我们无需care这货到底是什么类型,我们只知道他能干什么就可以work了。同样的功能我也可以使用XXXDelegate *delegate的方式来定义,只不过这样的话这个类又需要耦合一个XXXDelegate类型,而这个delegate类是它原本并不需要关心的。

所以说,@interface@protocol的强类型升级版。

举个NSObject的栗子最合适:

1
2
3
@interface NSObject <NSObject> {
Class isa;
}

NSObject之所以成为NSObject,绝大多数都是<NSObject>协议定义的方法,实体类@interface定义的唯一一个变量isa指针,为了继承链和消息传递。
除了<NSObject>协议外,NSObject还有很多Category来补充它的功能,其实仔细想想,Category更像protocol,一个补充协议,同样不能添加实例变量,但是和@interface一样需要与Class绑定。

进一步来讲,自从属性能自动合成变量之后,在头文件@interface中写大括号声明实例变量的情况越来越少(可以参见近几个版本iOS SDK中类头文件里这种写法几乎消失),因此,@interface@protocol的差别进一步缩小。


类与接口的设计原则 - 电视和遥控器

我喜欢将Classinterface的关系比喻成电视+遥控器,那么objc中的消息机制就可以理解成:
用户(caller)通过遥控器(interface)上的按钮(methods)发送红外线(message)来操纵电视(object)
所以,有没有遥控器,电视都在那儿,也就是说,有没有interface,class都是存在的,只是这种存在并没有意义,就好像这个电视没人会打开,没人会用,没人能看,一堆废铁摆在那儿。

对比简洁的遥控器,一个拥有很多按钮的老式电视遥控器,我们经常会用到的按钮能有几个呢?

所以,在设计一个类的interface的时候,如同在设计遥控器应该有怎样功能的按钮,要从调用者的角度出发,区分边界,应该时刻有以下几点考虑:

  1. 这个方法或属性真的属于这个类的职责么?(电视遥控器能遥控空调?)
  2. 这个方法或属性真的必须放在.h中(而不是放在.m的类扩展中)么?
  3. 调用者必须看文档才能知道这个类该如何使用么?(同一个业务需要调用者按顺序调用多次(而不是将这些细节隐藏,同时提供一个简洁的接口)才行)
  4. 调用者是否可以很容易发现类内部的变量和实现方式?(脑补下电视里面一块电路板漏在外面半截- -)

objc的@interface设计技巧Tips

看过不少代码,从@interface设计上多少就能看出作者的水平,分享下我对于这个问题的一些拙见。

只暴露外部需要看到的

比如,有如下一个类(这个类无意义,主要关注写法):

1
2
3
4
5
6
7
8
// Sark.h
@interface SarkViewController : NSObject <NSXMLParserDelegate /*1*/, NSCopying> {
NSString *_name; // 2
IBOutlet UITextField *_nameTextField; // 2
}
@property (nonatomic, strong) NSXMLParser *parser; // 3
- (IBAction)nameChangedAction:(id)sender; // 4
@end

这个interface出现的问题:

  1. 类内部自己使用的协议,如<NSXMLParserDelegate>不应该在头文件@interface中声明,而应该在类扩展中声明;公开由外部调用的协议,如<NSCopying>则写在这儿是正确的。
  2. 实例变量IBOutlet不应出现在这儿定义,这将类的内部实现暴露了出去,自从属性可以自动合成后,这里就更应该清净了。
  3. 内部使用的属性对象不要暴露在外,应该移动到类扩展中。
  4. 调用者对IBAction同样不需要关心,那么就不应该放在这儿。

合理分组子功能

  • 将相同功能的一组属性或方法写在一起

使用这个类或者对其进行修改时,一般都是从功能上找,所以把同一功能模块的一组属性或方法写在一块

  • 纯操作方法的子功能(无需向类添加变量)使用Category分块
  • 在头文件中也可以使用类扩展将interface按功能分区

Category里不能添加实例变量,但是类扩展可以,一般都在.m中作为私有interface使用,同样在头文件里作为分区使用,如,ReactiveCocoa中的RACStream.h

避免头文件污染

首先,类实现内部.m文件中使用的其他interface应该在.m文件import,如果也写在header中就会造成对调用者的污染;当interface中出现其他Classprotocol时,可以使用前置声明@class XXX@protocol XXX;当模块(一组类)内部间需要有一些定义(如常量、类型)而又不需要模块使用者知道时,使用一个内部头文件在模块中使用。

避免接口过度设计

考虑调用者的使用方便是很必要的,过火了反而增加了复杂度:

1
2
3
4
5
6
7
8
@interface Sark : NSObject
- (instancetype)init;
- (instancetype)initWithName:(NSString *)name;
- (instancetype)initWithName:(NSString *)name sex:(NSString *)sex;
- (instancetype)initWithName:(NSString *)name sex:(NSString *)sex age:(NSInteger)age;
- (instancetype)initWithName:(NSString *)name sex:(NSString *)sex age:(NSInteger)age friends:(NSArray *)friends;
// 无数多个 //
@end

提供了一组这样的方法,调用者可能只能用到其中的一个,那这样倒不如只留一个接口。

避免单例的滥用

单例模式固然好用,但感觉有点过度,将接口设计成单例入口前需要考虑一下:

  1. 这个类表达的含义真的只能有一个实例么?(如UIApplication)还是只是为了好调用而已?
  2. 这个单例持有的内存一直存在
  3. 是否能用类方法代替?
  4. 这个单例对象是否能成为另一个单例对象的属性?如果是,应该作为属性

隐藏继承关系中的私有接口

感谢@像条狗在飞在留言中提出的问题,问题大概可以总结为:当子类需要使用父类的一个私有属性(方法)时,需要把这个属性(方法)放到父类的header中,但暴露给子类的同时暴露给了外部调用者,如何解决?

我的方案是:建立一个私有header,使用类扩展定义父类需要暴露给子类的属性(方法),然后在各自的.m文件中引用,如:

有Father类和Son类,继承关系,可以考虑建一个如FatherPrivate.h的私有header:

1
2
3
4
5
// FatherPrivate.h
@interface Father ()
@property (nonatomic, copy) NSString *privateThingSonNeed;
- (void)privateMethodNeedsSonOverride;
@end

同时在Father.m和Son.m中同时import这个私有header,这样,Father和Son内部对于定义的属性和方法都是透明的,而对外部是隐藏的(因为两个类的header中都没有import这个私有header)


总结

  • @implementation合成了Class,而非@interface@interface@protocol的强类型升级版,它们和Category都表示了相近的含义
  • 我们应该善于面向接口编程,划清边界,将类的实现隐藏在调用者所见之外,使主调和被调者之间保持最少知识原则
  • @interface本身就是最好的文档

References

http://en.m.wikipedia.org/wiki/Interface_(object-oriented_programming)
http://zh.wikipedia.org/wiki/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B


原创文章,转载请注明源地址,blog.sunnyxx.com

原创文章,转载请注明原地址:blog.sunnyxx.com 
对博主有意思?新浪微博@我就叫Sunny怎么了 
or 微信搜索订阅号sunnyxx或扫下面的逗比狗 

objc@interface的设计哲学与设计技巧的更多相关文章

  1. JavaScript设计原则与编程技巧

    1 设计原则概述 <UNIX/LINUX设计哲学>设计准则 ① 小既是美. ② 每个程序只做一件事情. ③ 快速建立原型. ④ 舍弃高效率而取可移植性. ⑤ 避免强制性的图形化界面交互. ...

  2. Java面向接口编程,低耦合高内聚的设计哲学

    接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极大的降低程序中各个模块之间的耦合,提高系统的可维护性以及可扩展性. 因此,很多的软件架构设计理念都倡导"面向接口编程"而 ...

  3. React的设计哲学 - 简单之美

    React最初来自Facebook内部的广告系统项目,项目实施过程中前端开发遇到了巨大挑战,代码变得越来越臃肿且混乱不堪,难以维护.于是痛定思痛,他们决定抛开很多所谓的“最佳实践”,重新思考前端界面的 ...

  4. 跟vczh看实例学编译原理——一:Tinymoe的设计哲学

    自从<序>胡扯了快一个月之后,终于迎来了正片.之所以系列文章叫<看实例学编译原理>,是因为整个系列会通过带大家一步一步实现Tinymoe的过程,来介绍编译原理的一些知识点. 但 ...

  5. Python的设计哲学探究

    在Python shell中输入import this就会在屏幕上打印出来Python的设计哲学,如下: In [25]: import this The Zen of Python, by Tim ...

  6. Responsive设计的十个基本技巧(转)

    什么是Responsive设计?有的同学认为Responsive设计是自适应布局,也有的同学认为Responsive是网格布局.其实这些想法都不正确.Wikipedia对Responsive做 了详细 ...

  7. 畅谈Spring设计哲学

    自己从学习编程开始到现在有一个习惯:一直喜欢把软件开发中的技术和思路放到实际生活中去类比考虑.自己平常也喜欢开一些关于软件哲学的书籍,事实证明这些书籍对自己的学习新技术很有很大的帮助.数学是一切学科的 ...

  8. 开关电源PCB设计中的布线技巧

    开关电源PCB设计中的布线技巧关键字:布线 开关电源 走线 一.引言 开关电源是一种电压转换电路,主要的工作内容是升压和降压,广泛应用于现代电子产品.因为开关三极管总是工作在 “开” 和“关” 的状态 ...

  9. Python的设计哲学

    Beautiful is better than ugly. 优美胜于丑陋 Explicit is better than implicit. 明了胜于晦涩 Simple is better than ...

随机推荐

  1. jpa delete related

    delete deleteAll deleteInBatch notice List<Ap> apList = .deleteInBatch(apList)

  2. 关于主机FTP连接不上,无法列出目录,列表错误,上传速度慢,掉速的解决办法

    FTP是一种文件传输协议,它支持两种模式: 一种方式叫做Standard (也就是 Active,主动方式), 一种是 Passive (也就是PASV,被动方式). Standard模式 FTP的客 ...

  3. bs4_2

     QQ:231469242 欢迎交流 Parsing HTML with the BeautifulSoup Module Beautiful Soup是用于提取HTML网页信息的模板,Beautif ...

  4. 仿照jquery封装一个自己的js库(二)

    本篇为完结篇.主要讲述如何造出轮子的高级特性. 一. css方法的高级操作 先看本文第一部分所讲的dQuery css方法 //css方法 dQuery.prototype.css=function( ...

  5. IsPostBack--Asp.net

    .net程序员首先需要了解什么是IsPostBack.msdn上边有IsPostBack的定义:获取一个值,该值指示该页是否正为响应客户端回发而加载,或者它是否正被首次加载和访问.如果是为响应客户端回 ...

  6. redis删除list中指定index的值

    Redis的List删除命令: lrem : lrem mylist 0 "value"    //从mylist中删除全部等值value的元素   0为全部,负值为从尾部开始. ...

  7. OWIN support for the Web API 2 and MVC 5 integrations in Autofac

    Currently, in the both the Web API and MVC frameworks, dependency injection support does not come in ...

  8. WinForm使用皮肤图文步骤

    Winfrom本身样式提供的是Windows经典样式.. 不说多丑也绝称不上好看..有时为了用户体验就不得不需要想办法弄漂亮一点..皮肤包会是一个不错的选择.. 不废话了..开整.. 首先从网上下载免 ...

  9. Java内存区域-- 运行时数据区域

    jvm在执行Java程序时,会把它所管理的内存划分为若干个不同的数据区.这些区域都有各自的用途,以及创建和销毁的时间. 有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销 ...

  10. js使用split函数按照多个字符对字符串进行分割的方法

    这篇文章主要介绍了js使用split函数按照多个字符对字符串进行分割的方法,实例分析了split函数的使用技巧,非常具有实用价值,需要的朋友可以参考下   本文实例讲述了js使用split函数按照多个 ...