接口与API设计
文章目录
用前缀名避免命名空间冲突
OC没有其他语言所内置的那种命名空间的机制,所以我们命名的时候要设法避免潜在的一个命名冲突,在某些情况下如果在运行期载入了含重名的程序库,可能会让整个应用程序崩溃。
这里的内容比较少,其实就是我们采用三个字符前缀,apple采用两个字符前缀
- 选择与你的公司,应用程序或者是二者之间有关联的名字作为类名的一个前缀
- 如果用到了第三方库,那么应该给他的名称加上前缀
提供全能初始化方法
所有对象都要初始化,初始化时候有着不同的方法,创建类的实例方法不止一种,比方说NSDate这个类
这里有一个initWithTimeIntervalSinceReferenceDate:是一个全能初始化方法,我们设置一个类别来理解这个全能初始化方法的概念:
objc
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EOCRectangle : NSObject
@property (nonatomic, assign) float width;
@property (nonatomic, assign) float height;
- (id)initWithWidth:(float)width Height:(float)height;
@end
#import "EOCRectangle.h"
@implementation EOCRectangle
- (id)initWithWidth:(float)width Height:(float)height {
if (self = [super init]) {
_width = width;
_height = height;
}
return self;
}
- (instancetype)init {
return [self initWithWidth:6 Height:5];
}
@end
我们这里采用自定义的方法覆盖了init方法。
这里就可以保证我们调用任意一种init方法的时候不会出现一个报错,倘如我们又出现了一个新的一个子类,就像正方形这个类,
objc
#import "EOCRectangle.h"
NS_ASSUME_NONNULL_BEGIN
@interface EOCSquare : EOCRectangle
- (instancetype)initWithDimension:(float)dimension;
@end
NS_ASSUME_NONNULL_END
@implementation EOCSquare
- (instancetype)initWithDimension:(float)dimension {
return [super initWithWidth:dimension Height:dimension];
}
@end
这里他的子类也要使用这个全能初始化方法,但是这里会出现一个问题,也就是我们这里的代码,如果调用他父类的方法就会出现一个问题,创建出了一个长和宽不同的正方形,所以我们需要重写父类的一个方法。
objc
- (id)initWithWidth:(float)width Height:(float)height {
return [self initWithDimension:MAX(width, height)];
}
有些时候可能要实现多个全能初始化方法,比方说这里的一个
objc
NSCoding这个协议
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
_width = [coder decodeFloatForKey:@"width"];
_height = [coder decodeFloatForKey:@"height"];
}
return self;
}
如果子类要实现的话,我们要先调用父类的超类对应的方法,逐层向上,实现initWithCoder也是这样
小结
- 给一个类中提供一个全能初始化方法
- 全能初始化方法与超类不同,则要重写父类的方法
- 超类的初始化方法不适用于子类的话,就要重写这个方法并且抛出异常
实现description方法
我们如果想直接打印对象的话如果不重写description方法的话会看到下面的内容:
objc
<Person: 0x60000022b180>
我们如果想要输出更有用的信息也很简单,只用重写description方法就好了
objc
- (NSString *)description
{
return [NSString stringWithFormat:@"%@: %p, \%@ %@\"", [self class], self, _firstName, _name];
}
//输出结果
Person: 0x60000023a5e0, 123 nan"
Type: Notice | Timestamp: 2025-01-21 15:56:34.193344+08:00 | Process: fragment | Library: fragment | TID: 0x21153bf
其实我们还可以借助NSDictionary来更方便我们看到对象内部的一个属性
objc
- (NSString *)description
{
return [NSString stringWithFormat:@"%@: %p, %@", [self class], self, @{@"firstName":_firstName, @"name":_name}];
}
//输出结果
Person: 0x6000002043c0, {
firstName = 123;
name = nan;
}
这里还有一个\- (NSString *)debugDescription
这个方法其实是在我们的一个调试控制台里面输入命令来让他打印的。
小结
- 实现description来返回一个有意义的字符串,用以描述这个实例
- 若想在调试的时候打印更详尽的对象描述信息,则需实现debugDescription方法
尽量使用不可变方法
在具体编程实践的时候,尽量使用不可变对象,也就是我们尽量吧对外公开的属性设为只读,
objc
@interface Person : NSObject
@property (nonatomic, copy, readonly) NSString* name;
@property (nonatomic, copy, readonly) NSString* firstName;
-(id) initWithName:(NSString*)firstName lastName:(NSString*)name;
@end
在有些时候我们想重新修改封装在对象的数据,但是却不想数据在外部被别人修改,那么可以在extension中重新声明这个属性。
objc
@interface Person ()
@property (nonatomic, copy, readwrite) NSString* firstName;
@end
当我们返回一个对象的要求是collection的时候,我们还是尽量返回一个不可变的对象,假设他可以进行一个添加和删除,但是我们要设置成一个不可变的collection,这个不可变collection是内部可变的一个collection的复制
objc
@interface Person : NSObject
@property (nonatomic, copy, readonly) NSString* name;
@property (nonatomic, copy, readonly) NSString* firstName;
@property (nonatomic, copy, readonly) NSSet* firend;
-(id) initWithName:(NSString*)firstName lastName:(NSString*)name;
@end
@implementation Person {
NSMutableSet* internalSet;
}
- (NSSet *)firend {
return [internalSet copy];
}
-(void)addFirend:(Person*)person {
[internalSet addObject:person];
}
这样可以避免外部直接修改set里面的内容。
小结
- 创建不可变对象
- 在对象内可以重新修改,可以在extesion中重新修改成readwrite
- 不要把可变的colleciton作为属性公开,而是提供对应方法来处理对象中的collection
使用清晰且协调的命名方法
这里其实就是在强调我们命名要清晰协调。
- 方法的返回值如果是新创建的,那么方法名的首个词应该是返回值的类型
- 吧表示参数类型的名次放在参数前面
- 使用全称而不是使用简称
- boolean前加is前缀
- get这个前缀留给输出参数来保存返回值的方法
要点
- 起名遵循OC的命名规范,创建出来的接口更容易被开发者使用
- 方法名要言简意赅
- 方法名不要有简称
- 风格和你自己的代码或者继承的框架类似
为私有方法加前缀
使用什么前缀和个人喜好而定,其中最好包括下划线和字母p作为前缀。
比方说:
objc
- (void)p_privateMethod;
不要用单个下划线作为私有方法的前缀,这种方式会和苹果公司冲突
- 给私有方法加上前缀,这样更容易区分公共方法
- 不要用一个下划线作为私有放的前缀
理解OC错误模型
OC只在极罕见的请况下才抛出异常
异常只用于极其严重的问题,也就是NSException的使用场景并没有那么多,而是NSError比较多,这里我们先认识一下NSError
封装了三个信息:
- Error domain(错误范围,类型为字符串)
错误发生的范围
-
Error code (错误吗,整数)
独有的错误代码,用以指明某个范围内具体发生哪些错误
-
User info (用户类型, 字典)
有关此错误的额外信息,其中包含一段本地化的描述
常见的是通过error在委托协议中传递此错误,我们在网络协议中经常看到这种内容
我们可以创建属于自己的程序库中所发生的错误指定一个专用的错误范围的字符串
- 只有发生了严重错误才使用异常
- 可以通过委托方法来处理错误
- 可以把错误信息放到NSError中
理解NSCopying协议
为什么会出现zone,zone是不同的区
我们要重写copyWithZone:这个方法
这里先看一个例子:
objc
-(id)copyWithZone:(NSZone *)zone {
return [[[self class] allocWithZone:zone] initWithName:_firstName lastName:_name];
}
这个实现主要是创建一个新的对象,然后进行一个全能初始化的操作,让他执行所有的初始化工作。
但在某些情况中我们会遇到一些容器类的数据结构,我们有些时候要采用浅拷贝,有些时候采用深拷贝。
objc
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
如果copyItems参数为yes,这个方法会给数组中的每个对象发生copy,这个方法才是容器的一个深拷贝,而采用普通的copy方式只是一个浅拷贝,不会拷贝每一个set中的元素
小结
- 实现NSCpoying来实现让自己的对象有拷贝功能
- 如果自定义对象有两种可变和不可变两种类型那么就要同时实现NSCopying和NSMutableCopying
- 复制对象一般情况下采用一个浅拷贝
- 如果需要深拷贝,可以写一个专门执行深拷贝的方法