36792bbae4c915c855138eead612da9d
那些设计iOS API需要知道的事

为了能够将我们项目中的代码能够在后续开发者使用(重用代码),通常使用的方法是将代码按照功能模块编写成API。那么我们就很有必要了解Objective-C语言中常见的编程范式(paradigm),同时还需了解各种可能碰到的陷阱。

命名

命名冲突的问题

Objective-C没有其他语言的那种内置命名空间(namespace)机制。因此,我们只能自己想办法来解决命名冲突问题。最常用的解决方式就是,仿照其他语言(C++)建立自己的namespace,例如,使用前缀。

所选前缀可以是与公司、应用程序或二者皆有关联之名。例如,ZAKER User Interface可以使用ZUI作为前缀。使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有“两字母前缀”(two-letter prefix)的权利,所以开发者选用的前缀应该是三个字母的。如果开发者使用了两个字母作前缀,那么很有可能开发者自定义的API和Apple的API冲突。

不仅仅是类名,应用程序中的所有名称都应该加前缀。如果要为既有类新增“分类”(category),那么一定要给“分类”及“分类”中的方法加上前缀。另外,类的实现文件中所用的纯C函数及全局变量也应该注意添加前缀。

如果使用了第三方库编写自己的代码,并准备将其发布为程序库供他人开发应用程序所用,则尤其要注意重复符号问题。这种情况下为了避免使用者使用了与你相同的第三方库,应该为第三方库都加上你自己的前缀。

第三方库引入使用前缀

命名方式

类、方法和变量的命名是Objective-C编程的重要环节。如果命名方式好,可以提高代码可读性,减少不必要的注释。
初学者通常会觉得Objective-C是门很繁琐的语言,因为其语法结构使得代码读起来和句子一样。命名中一般都带有“in”、“for”、“with”等介词,特别是在命名时还要讲究英文语法。例如:

NSString *text = @"This is a good idea.";
NSString *newText = [text stringByReplacingOccurrencesOfString:@"idea" withString:@"think"];

上面的代码虽然用了比较啰嗦的方式描述一个看上去很简单的表达式。对于执行替换的那个方法,代码读起来就像日常语言里的那个句子:“Take text and give me a new string by replacing the occurrences of the string 'idea' with the string 'think'”。
这个句子准确描述了开发者想做的事。在命名不像Objective-C这般繁琐的语言中,类似的程序可能会写成:

string text = "This is a good idea.";
string new Text = text.replace("idea", "think");

上面代码这样写,看起来方法名简洁很多,但是带来的代码不可读性却是非常大的。首先,我们不知道 text.replace 方法的两个参数到底按照什么顺序解读(除非查看方法声明);再者,这两个参数谁替换谁?

另外,和大多数语言一样,Objective-C也是采用“驼峰式大小写命名法”(camel casing)——以小写字母开头,其后每个单词首字母大写。

方法命名

清晰的方法名从左至右读起来好似一段文章。并不是说非得按照那些命名规则来给方法起名,不过这样做可以令代码变得更好维护,使他人更容易读懂。
虽然类似C++或Java中那种函数命名简单,但是,若想知道每个参数的用途,就得查看函数原型,这会令代码难于读懂。
NSString这个类展示了一套良好的命名习惯。下面列举几个方法及命名缘由:

1)+ (instancetype)string;
工厂方法(factory method),用于创建新的空字符串。方法名清晰地描述了返回值的类型。

2)+ (instancetype)stringWithString:(NSString *)string;
工厂方法,根据某字符串创建出与之内容相同的新字符串。与创建空字符串所用的那个工厂方法一样,方法名的第一个单词也指明了返回类型。

3)+ (instancetype)localizedStringWithFormat:(NSString *)format, ...;
工厂方法,根据特定格式创建出新的“本地化字符串”(localized string)。返回值类型是方法名的第二个单词(string),因为其前面还有个修饰语(localized)用来描述其逻辑含义。此方法的返回值依然是“字符串”(string),只不过是一种经过本地化处理的特殊字符串。

4)- (NSUInteger)lengthOfBytesUsingEncoding:(NSStringEncoding)enc;
若字符串是以给定的编码格式(ASCII、UTF8、UTF16)来编码的,则返回其字节数组长度。此方法与length相似,但该方法还需一个参数,该参数紧跟着方法名中描述其类型的那个名词(encoding)。

因此,我们可以总结成几条方法命名规则:

1)如果方法的返回值是新创建的,那么方法名的首个词应该是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象。即便有时返回内部对象的一份拷贝,我们也认为那相当于原有对象。这些存取方法应该按照其所对应的属性来命名。

2)应该把表示参数类型的名词放在参数前面。

3)如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。

4)不要使用str这种简称,应该使用string这样的全称。

5)boolean属性应加is前缀。如果某方法返回非属性的boolean值,那么应该根据其功能,选用has或is当前缀。

6)将get这个前缀留给那些借由“输出参数”来保存返回值的方法,比如说,把返回值填充到“C语言式数组”(C-style array)里的那种方法就可以使用这个词做前缀。

类与协议命名

不仅仅是方法,类和协议也应该加上前缀,避免命名空间冲突。例如:

  • UIView
  • UIViewController
  • UITableViewDelegate

错误模型

目前有很多编程语言都有“异常”(exception)机制,Objective-C也不例外。

“自动引用计数”(ARC, Automatic Reference Counting)在默认情况下不是“异常安全的”。这意味着:如果抛出异常,那么本应该在作用域末尾释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exception

Objective-C现在所采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的“异常安全”代码了。

异常只应该用于极其严重的错误,比如,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。在这种情况下,如果有人直接使用了这个抽象基类,那么可以考虑抛出异常。与其他语言不同,Objective-C中没办法将某个类标识为“抽象类”。要想达成类似效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。

异常只用于处理严重错误(fatal error),对于其他错误,Objective-C语言所用的编程范式为:令方法返回nil/0,或使用NSError,以表明有错误发生。

NSError对象里封装了三条信息:

  • Error domain (错误范围,其类型为字符串)

错误发生的范围,也就是产生错误的根源,通常用一个特有的全局变量来定义。例如,URL-handling-subsystem,在从URL中解析或获取数据时如果出错了,那么就使用NSURLErrorDomain来表示错误范围。

  • Error code (错误码,其类型为整数)

独有的错误码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误情况通常采用enum来定义。

  • User info (用户信息,其类型为字典)

有关此错误的额外信息,其中或许包含一段“本地化描述”,或许还包含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”。

使用不可变对象

设计类的时候,应充分使用属性来封装数据。而在使用属性时,则可将其声明为readonly。默认情况下,属性是readwrite

因为如果把可变对象(mutable object)放入collection之后又修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。故此,我们应该尽量减少对象中的可变内容。具体到编程实践中,则应该尽量把对外公布出来的属性设为readonly,而且只在有必要时才将属性对外公布。

定义类的公共API时,需要注意,对象里表示各种collection的那些属性究竟应该设成可变的,还是不可变的。如果某个属性可以为外界所增删,那么这个属性就需要用可变的set来实现。在这种情况下,通常应该提供一个readonly属性供外界使用,该属性将返回不可变的set,而此set则是内部那个可变set的一份拷贝。

//  ZKRPointOfInterest.h

#import <UIKit/UIKit.h>
 ZKRPointOfInterest : NSObject
 (nonatomic, copy, readonly) NSString *identifier; (nonatomic, copy, readonly) NSString *title; (nonatomic, assign, readonly) CGFloat latitude; (nonatomic, assign, readonly) CGFloat longitude; (nonatomic, strong, readonly) NSSet *locations;

- (instancetype)initWithIdentifier:(NSString *)identifier
                             title:(NSString *)title
                          latitude:(CGFloat)latitude
                         longitude:(CGFloat)longitude;

- (void)addLocation:(ZKRPointOfInterest *)location;
- (void)removeLocation:(ZKRPointOfInterest *)location;


//  ZKRPointOfInterest.m

#import "ZKRPointOfInterest.h"
 ZKRPointOfInterest
{
    NSMutableSet *_internalLocations;
}

- (instancetype)initWithIdentifier:(NSString *)identifier
                             title:(NSString *)title
                          latitude:(CGFloat)latitude
                         longitude:(CGFloat)longitude
{
    self = [super init];
    if (self) {

    }
    return self;
}

- (NSSet *)locations
{
    return [_internalLocations copy];
}

- (void)addLocation:(ZKRPointOfInterest *)location
{
    if (location) {
        [_internalLocations addObject:location];
    }
}

- (void)removeLocation:(ZKRPointOfInterest *)location
{
    [_internalLocations removeObject:location];
}

注意:不要在返回的对象上查询类型以确定其是否可变。(即使不用isKindOfClass:方法来判断返回值类型是否可变)

description方法

在调试程序时,经常需要打印并查看对象信息。一种办法是编写代码把对象的全部属性都log到日志中。NSLog(@"object=%@", object);

在构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”(format string)里的“%@”。

top Created with Sketch.