Objective-C(三)接口与API设计

这是Objective-C系列的第3篇。编程

1、最佳实践

  • 为全部类名都加上适当的前缀数组

  • 提供"全能初始化方法"安全

    • 在类中提供一个全能初始化方法,并与文档中指明。其余初始化方法均应调用此方法;
    • 若全能初始化方法与超类方法不一样,则须要覆写超类中对应的方法;
    • 若是超类方法的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
  • description方法bash

    • 实现description方法返回一个有意义的字符串,用于描述该实例;
    • 若想在调试时,打印出更详尽的对象描述信息,则应实现debugDescription方法。
    • 能够去寻找实现这样的插件来快速生成该方法。
  • 尽可能建立不可变对象;session

    • 若属性仅可用于对象内部修改,则在“class-continuation分类”中将其有readonly属性扩展为readwrite
    • 不要把可变的collection做为属性公开,而应提供相关方法,以此修改对象中可变的collection。
  • 命名框架

    参考使用清晰而协调的命名方式dom

  • copy函数

    • 若想令本身的所写的对象具备拷贝功能,则需实现NSCopying协议。
    • 若是自定义对象分为可变版本和不可变版本,那么就要同时实现NSCopying与NSMutableCopying。
    • 复制对象时需决定采用浅拷贝仍是深拷贝,通常状况下应该尽可能执行浅拷贝。
    • 若是你所写的对象须要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。

2、实践详解

2.1 用前缀避免命名空间的冲突

赶上以下错误吗?post

duplicate symbol _OBJC_METACLASS_$DuplicateTheClass in 
 	build /somethings.o
 	build /something_else.o
复制代码

以上错误是因为在应用程序中两份代码都各类实现了DuplicateTheClass的类,致使该类符号和元类各定义了两次。ui

避免此问题的惟一办法就是变相实现命名空间:为全部名称都加上适当前缀

前缀能够是公司、应用程序或者功能模块,甚至是我的标识等关联的词汇。但要注意的是,Apple宣称其保留全部使用“两字母前缀”的权利,因此你本身选用的前缀应该是三个字母的。

还有一个要注意的问题是,类的实现文件中所用的纯C函数以及全局变量,在编译好的目标文件中,这些都是“顶级符号(top-level symbol)”。

同时,也要注意在引入第三方库的时候,注意别使得本身的命名与其余库命名重合。

2.2 提供“全能初始化方法”

  • 在类中提供一个全能初始化方法,并与文档中指明。其余初始化方法均应调用此方法;
  • 若全能初始化方法与超类方法不一样,则须要覆写超类中对应的方法;
  • 若是超类方法的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

2.3 实现description方法

  • 实现description方法返回一个有意义的字符串,用于描述该实例;
  • 若想在调试时,打印出更详尽的对象描述信息,则应实现debugDescription方法。
  • 能够去寻找实现这样的插件来快速生成该方法。

2.4 尽可能使用不可变对象

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

在设计类的时候,将某些不须要改变的属性设置为readonly,以防止被改动。

@interface HOReadOnlyProperty : NSObject
@property (nonatomic ,readonly ,copy)NSString *name;
@end
复制代码

在这里,虽然指定了readonly,就没有设置方法,但仍是指定了内存管理语义,是为了之后方便将其改动为可读写属性。

有时可能想修改封装在对象内部的数据,可是却不想令这些数据为外人所改动。一般是在对象内部将readonly属性从新声明为readwrite。以下:

@interface HOReadOnlyProperty()
@property (nonatomic ,readwrite ,copy)NSString *name;
@end

@implementation HOReadOnlyProperty
@end
复制代码

这样就能够实现只在内部改变属性值了。

!可是!并不能保证外部不会改变属性值,由于仍然能够经过KVC实现改变。经过setValue:forKey:来修改。

另外,在定义表示各类collection的那些属性时,通常会提供一个readonly属性供外界使用,该属性返回一个不可变的set,而此set是内部那个可变set的一个拷贝。好比下面:

@interface HOPerson : NSObject
@property (nonatomic ,copy)NSString *name;
@property (nonatomic ,strong ,readonly)NSSet *friends;
@end
复制代码

实现文件:

@interface HOPerson()
{
    NSMutableSet *_internalFriends;
}

@end

@implementation HOPerson

- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init]) {
        _name = name;
        _internalFriends = [NSMutableSet set];
    }
    return self;
}

- (NSSet *)friends
{
    return [_internalFriends copy];
}

- (void)addFriend:(HOPerson *)person
{
    [_internalFriends addObject:person];
}

- (void)removeFriend:(HOPerson *)person
{
    [_internalFriends removeObject:person];
}

@end
复制代码
  • 尽可能建立不可变对象;
  • 若属性仅可用于对象内部修改,则在“class-continuation分类”中将其有readonly属性扩展为readwrite
  • 不要把可变的collection做为属性公开,而应提供相关方法,以此修改对象中可变的collection。

2.5 使用清晰而协调的命名方式

2.5.1 方法命名规则

  • 若是方法的返回值是新建立的,那么方法名的首个词应是返回值的类型,好比stringWithString。除非前面有修饰语,好比localizedString。属性的存取方法不遵循这种命名方式,由于通常认为这些方法不会建立新对象,即使有时返回内部对象的一份拷贝,咱们也认为那至关于原有的对象。这些存取方法应该按照其所对应的属性来命名;
  • 应该把表述参数类型的名词放在参数前面。好比-(void)getCharacters:(unichar*)buffer range:(NSRange)aRange中的range表述的参数类型;
  • 若是方法要在当前对象执行操做,那么久应该包含动词;若执行操做还须要参数,则应该在动词后面再加上一个或多个名词,好比:lowercaseString
  • 不要使用str这种简称,应该使用string这样的全称;
  • Boolean属性应该加上is前缀。若是某方法返回非属性的Boolean值,那么应该根据其功能,选用hasis为前缀;

2.5.2 类与协议的命名

应该为类与协议的名称加上前缀,以免命名空间冲突。同时添加ViewViewControllerDelegateProtocol等这样标识类与协议的名词。

  • 方法名要言简意赅,从左到右读起来要像个平常用语中的句子才好;
  • 方法名里不要用缩略后的类型名称;
  • 给方法起名的第一要务就是要确保其风格与你本身的代码或所要集成的框架相符。

2.5.3 为私有方法名加前缀

一个类所作的事情一般都要比从外面看到的更多。编写类的实现代码时,常常要编写一些只在内部使用的方法。应该为这中方法加上某些前缀,这有助于调试,由于据此很容易就能把公共方法和私有方法区分开。

另外,前缀的存在便于修改方法名或方法签名,即这些有前缀的方法能够随时修改,而不用担忧会影响面向外界的那些API。

具体使用何种前缀可根据我的喜爱而定,可是最好包括下划线与字母p。p即private,下划线是为了与真正的方法名区分开。因此私有方法名会写成:

- (void)p_privateMethod {
		/.../	
}
复制代码
  • 给私有方法的名称加上前缀,这样能够很容易地将其同公共方法区分开;

  • 不要单用一个下划线前缀,由于这种方法是预留给苹果公司用的。

2.6 理解Objective-C错误模型

​ Objective-C 也有异常机制,可是在默认状况下不是“异常安全的(exception safe)”。这意味着:若是抛出异常,那么本应在做用于末尾释放的对象选择却不会自动释放了。

​ 若是想生成“异常安全”的代码,能够经过设置编译标志来实现,不过这将引入一些额外的代码,在不抛出异常时,也照样要执行这部分代码。须要打开的编译器标志是:-fobjc-arc-exceptions。

​ 上面说的是ARC的情形,那么在MRC呢?

​ 即便MRC时,也很难写出抛出异常不会致使内存泄漏的代码。好比,先建立好某资源,使用完以后再将其释放,可是在释放以前若是抛出异常,这部分资源就不会被释放了。这种问题的解决方案之一,就是在抛出异常前先释放资源。这样的确能解决问题,不过万一资源过多,执行路径复杂,那么在抛出什么异常时该释放哪些资源就会杂乱无章。

​ 针对上面的问题,Objective-C的解决办法是:只在极其罕见的状况下抛出异常,异常抛出以后,无须考虑恢复问题,并且应用程序也应该退出。也就是说,无须编写复杂的“异常安全”的代码了。

​ 异常只应该应用于极其严重的错误。

​ 对于那些“不那么严重的错误(nonfatal error,非致命错误)”,Objective-C所用的编程范式:令方法返回nil/0,或是使用NSError,以代表有错误发生。

​ 好比,在init时,返回nil,意指初始化失败。

​ NSError的用法灵活,经由此对象,能够把致使错误的缘由回报给调用者。

-(instancetype)errorWithDomain:(NSErrorDomain)domain code:(NSInteger)code userInfo:(nullable NSDictionary *)dict;
复制代码
  • Error domain(错误范围,其类型为字符串)

    即错误产生的根源,一般用一个特有的全局变量来定义。系统提供了一些,参考NSError.h头文件。

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

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

​ 那么如何将错误告知调用者,即经过什么途径传递出去?

​ 第一种常见用法是经过委托协议来传递此错误。

- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error 
复制代码

​ 另一种常见用法是:经由方法的“输出参数”返回给调用者。

- (BOOL)doSomething:(NSError **)error 
{
  if(/*此处是错误逻辑*/){
	if(error) {
		//必须先判断error,假如nil时,解引用会致使“段错误(segmentation fault)”,致使程序崩溃
		*error = [NSError errorWithDomain:domain code:code userinfo:userinfo];
	}	
	return NO;
  } else {
    return YES;
  }
}

//调用
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if(error){
	//处理错误
}
复制代码

下面示例了一个domain以及code的代码:

//HOErrors.h
extern NSString *const HOErrorDomain;

typedef NS_ENUM(NSUInteger, HOError) {
  HOErrorUnknown = -1,
  HOErrorInternalInconsistency = 100,
}

//HOErrors.m
NSString *const HOErrorDomain = @"HOErrorDomain"
复制代码

2.7 理解NSCopying协议

​ 使用对象时,常常须要拷贝。在Objective-C中,此操做经过copy方法完成。若是要想令某个类支持copy,须要实现NSCopying协议,该协议只有一个方法:

- (id)copyWithZone:(NSZone*)zone;
复制代码

​ 其中,zone在以前开发中会据此把内存分红不一样的“区(zone)”,而对象建立在某个区里面。如今不用了,每一个程序只有一个区:默认区(default zone)。因此,不用管zone这个参数。

​ 有时候,须要实现可变版本的拷贝,遵循NSMutableCopying协议,实现:

- (id)mutableCopyWithZone:(NSZone*)zone;
复制代码

​ 在编写拷贝方法是,还要以为是执行“深拷贝(deep copy)”仍是“浅拷贝(shallow copy)”。深拷贝时,在拷贝对象自身是,将其底层数据也一并复制过去。Foundation框架中全部的collection类在默认状况下都执行浅拷贝,即只拷贝对象自己,而不复制其中数据。

​ 通常状况下,咱们在实现拷贝时,遵循系统所使用的那种样式,即以浅拷贝实现“copyWithZone”方法。可是若是有必要的话,增长一个执行深拷贝的方法。以NSSet为例,该类实现深拷贝的方法:

- (id)initWithSet:(NSArray*)array copyItems:(BOOL)copyItems;
复制代码

​ 若copyItems为YES,则该方法会向数组中的每一个元素都发送copy消息,用拷贝好的元素建立新的set,并返回给调用者。

  • 若想令本身的所写的对象具备拷贝功能,则需实现NSCopying协议。
  • 若是自定义对象分为可变版本和不可变版本,那么就要同时实现NSCopying与NSMutableCopying。
  • 复制对象时需决定采用浅拷贝仍是深拷贝,通常状况下应该尽可能执行浅拷贝。
  • 若是你所写的对象须要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
相关文章
相关标签/搜索