Aug
27
在OC-Foundation与UIKit中,苹果使用类簇来实现具体工厂子类用得出神入化。有意思得是,作为工厂设计模式的一种实践,似乎只有苹果自己大量使用类簇,在别的语言下类簇似乎并不是一个优雅的最佳实践。那么仿照苹果的类簇类型的设计,我们如何才能实现一个API友好的类簇类型呢。
首先我们自己的代码中既有常见的类簇类型的实践方式是这样的:
这种实践方式虽然直接方便,但是问题明显:
- 其实XGFEAppleView的作用是一个抽象类abstract class,但是oc里面并没有这样的东西,只能用一个真实的类。那么我们就很难能阻止用户直接初始化XGFEAppleView类本身了。
- 这里的doSomething方法作为目标抽象方法,采用了常见的直接抛异常,并且子类实现方法不对父类进行继承实现的。这样一个只能在运行时才能发现问题,或者发生了漏测的话,还会引入线上crash,同时如果父类不是纯抽象方法,希望有逻辑复用的话,就很难写了。
- 使用子类,如果作为类簇,子类是不会暴露给用户的,用户初始化子类只能用限定的类方法+ (instancetype)redApple,+ (instancetype)appleWithType:(NSString *)type之类的进行构造,api学习成本和api迁移重构成本高。
总的来说以上类簇类代码虽然实现起来很规整,但是api暴露对于一个库的设计来说是高风险低质量的,需要寻找更友好的实现方法。
因此,回到Foundation中,苹果的类簇类是怎么实现既通过特定构造类方法,也能通过alloc-init就初始化出实现类的呢?我们拿NSArray看看:
单从NSArray来看,至少出现了__NSArray0,__NSSingleObjectArrayI,__NSArrayI三个类簇子类,并且均可以通过[NSArray alloc]初始化出来。
这时候就注意到了,[NSArray alloc]居然得到了它的之类,那这个alloc方法一定是假的,至少是类似于
这样的骚操作才能做到,那么实际上[NSArray alloc]到底返回的什么呢,我们做了如下验证:
- [NSArray alloc]得到了一个类型为__NSPlaceholderArray的NSArray子类,和实际初始化出来的类完全无关
- 并且__NSPlaceholderArray还是一个单例(多次alloc出来的地址相同)
同时考虑到类簇的设计初衷是工厂设计模式的实现,很明显,__NSPlaceholderArray就是那个工厂类了。
那么就可以梳理出流程:
父类的alloc方法始终返回的工厂对象,工厂对象根据init方法传入的参数来生产实际需要的子类型。
- 这样写,不通过子类alloc,XGFEAppleView本身绝对不可能被直接alloc出来的,这样XGFEAppleView的内部公共方法就完全可以像抽象类那样自由实现,而不在意被抽象类直接调用。从内部安全的角度甚至可以在XGFEAppleView的alloc对子类做限定,否则一律走工厂,这样也可以阻止用户继承(不过一般没必要)。
- 另外,就可以像用NSArray一样,通过[[XGFEAppleView alloc] init],[[XGFEAppleView alloc] initWithType:@"red"] 这样更友好的方式使用,也会大大降低使用者后续迁移和库开发者内部重构的风险。
- 另外,整个写法中,虽然多存在了一个单例的工厂类,代码语法上不规范处很多理解成本较高。但是类簇完成之后,使用收益(业务代码可维护性,类安全,重构成本)肯定是大于开发成本的,所以还是可以放心的去写和推广。
首先我们自己的代码中既有常见的类簇类型的实践方式是这样的:
@interface XGFEAppleView : NSView
@end
@interface XGFERedAppleView : XGFEAppleView
@end
@interface XGFEBlueAppleView : XGFEAppleView
@end
@implementation XGFEAppleView
+ (instancetype)redApple { // public
return [XGFERedAppleView new];
}
+ (instancetype)blueApple { // public
return [XGFERedAppleView new];
}
+ (instancetype)appleWithType:(NSString *)type { // public
if ([type isEqualToString:@"red"]) {
return [XGFEAppleView redApple];
} else if ([type isEqualToString:@"blue"]) {
return [XGFEAppleView blueApple];
} else {
return nil;
}
}
- (void)doSomething {
@throw [NSException exceptionWithName:@"XGFEAppleView" reason:@"不要直接使用XGFEAppleView的方法" userInfo:nil];}
@end
@implementation XGFERedAppleView
- (void)doSomething {
//doSomething red
}
@end
@implementation XGFEBlueAppleView
- (void)doSomething {
//doSomething blue
}
@end
这种实践方式虽然直接方便,但是问题明显:
- 其实XGFEAppleView的作用是一个抽象类abstract class,但是oc里面并没有这样的东西,只能用一个真实的类。那么我们就很难能阻止用户直接初始化XGFEAppleView类本身了。
- 这里的doSomething方法作为目标抽象方法,采用了常见的直接抛异常,并且子类实现方法不对父类进行继承实现的。这样一个只能在运行时才能发现问题,或者发生了漏测的话,还会引入线上crash,同时如果父类不是纯抽象方法,希望有逻辑复用的话,就很难写了。
- 使用子类,如果作为类簇,子类是不会暴露给用户的,用户初始化子类只能用限定的类方法+ (instancetype)redApple,+ (instancetype)appleWithType:(NSString *)type之类的进行构造,api学习成本和api迁移重构成本高。
总的来说以上类簇类代码虽然实现起来很规整,但是api暴露对于一个库的设计来说是高风险低质量的,需要寻找更友好的实现方法。
因此,回到Foundation中,苹果的类簇类是怎么实现既通过特定构造类方法,也能通过alloc-init就初始化出实现类的呢?我们拿NSArray看看:
(lldb) po [[NSArray alloc] init]
<__NSArray0 0x6000000080a0>(
)
(lldb) po @[]
<__NSArray0 0x6000000080a0>(
)
(lldb) po @[@1]
<__NSSingleObjectArrayI 0x600000015f50>(
1
)
(lldb) po @[@1, @2]
<__NSArrayI 0x6000003acee0>(
1,
2
)
(lldb) po [[NSArray alloc] initWithObjects:@1, @2, nil]
<__NSArrayI 0x600000352aa0>(
1,
2
)
单从NSArray来看,至少出现了__NSArray0,__NSSingleObjectArrayI,__NSArrayI三个类簇子类,并且均可以通过[NSArray alloc]初始化出来。
这时候就注意到了,[NSArray alloc]居然得到了它的之类,那这个alloc方法一定是假的,至少是类似于
+ (instancetype)alloc {
return [__NSArrayI alloc];
}
这样的骚操作才能做到,那么实际上[NSArray alloc]到底返回的什么呢,我们做了如下验证:
(lldb) po [NSArray alloc]
0x00007fe1546011c0
(lldb) po [NSArray alloc].class
__NSPlaceholderArray
(lldb) po [[NSArray alloc] isKindOfClass:NSArray.class]
YES
(lldb) po [NSArray alloc]
0x00007fe1546011c0
- [NSArray alloc]得到了一个类型为__NSPlaceholderArray的NSArray子类,和实际初始化出来的类完全无关
- 并且__NSPlaceholderArray还是一个单例(多次alloc出来的地址相同)
同时考虑到类簇的设计初衷是工厂设计模式的实现,很明显,__NSPlaceholderArray就是那个工厂类了。
那么就可以梳理出流程:
父类的alloc方法始终返回的工厂对象,工厂对象根据init方法传入的参数来生产实际需要的子类型。
@interface XGFEAppleView : NSView
//XGFEAppleView暴露出init风格的初始化方法
- (instancetype)init;
- (instancetype)initWithType:(NSString *)type;
- (instancetype)initWithTypeRed;
@end
@interface XGFERedAppleView : XGFEAppleView
@end
@interface XGFEBlueAppleView : XGFEAppleView
@end
//新增工厂类__XGFEAppleViewFactory
@interface __XGFEAppleViewFactory : XGFEAppleView
+ (instancetype)instance;//它是一个单例
//实际由工厂方法来实现XGFEAppleView的那些init方法,这里声明不声明就无所谓了
- (instancetype)init;
- (instancetype)initWithType:(NSString *)type;
- (instancetype)initWithTypeRed;
@end
@implementation __XGFEAppleViewFactory
+ (instancetype)instance {
static __XGFEAppleViewFactory *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!instance) {
instance = [__XGFEAppleViewFactory alloc];
}
});
return instance;
}
- (instancetype)init {
return (id)[[XGFERedAppleView alloc] init];
}
- (instancetype)initWithType:(NSString *)type {
if ([type isEqualToString:@"red"]) {
return (id)[[XGFERedAppleView alloc] init];
} else if ([type isEqualToString:@"blue"]) {
return (id)[[XGFEBlueAppleView alloc] init];
} else {
return nil;
}
}
- (instancetype)initWithTypeRed {
return (id)[[XGFERedAppleView alloc] init];
}
@end
@implementation XGFEAppleView
+ (instancetype)alloc {
if (self == [XGFEAppleView class]) {//如果是直接调用[XGFEAppleView alloc],就走工厂
return (id)[__XGFEAppleViewFactory instance];
} else {
return [super alloc];//留给子类super用
}
}
+ (instancetype)new {
if (self == [XGFEAppleView class]) {//如果是直接调用[XGFEAppleView new],就走alloc到工厂
return [[XGFEAppleView alloc] init];
} else {
return [super new];//留给子类super用
}
}
- (instancetype)init {
//...省略,正常实现即可,不可能有人直接调用的,爱实现实现不实现拉倒
}
- (instancetype)initWithType:(NSString *)type {
//...省略,正常实现即可,不可能有人直接调用的,爱实现实现不实现拉倒
}
- (instancetype)initWithTypeRed {
//...省略,正常实现即可,不可能有人直接调用的,爱实现实现不实现拉倒
}
- (void)doSomething {
//不可能直接调用,可以放心写复用的业务逻辑而不用担心误调
}
@end
@implementation XGFERedAppleView
- (void)doSomething {
[super doSomething];//可以super了
//doSomething red
}
@end
@implementation XGFEBlueAppleView
- (void)doSomething {
[super doSomething];//可以super了
//doSomething blue
}
@end
- 这样写,不通过子类alloc,XGFEAppleView本身绝对不可能被直接alloc出来的,这样XGFEAppleView的内部公共方法就完全可以像抽象类那样自由实现,而不在意被抽象类直接调用。从内部安全的角度甚至可以在XGFEAppleView的alloc对子类做限定,否则一律走工厂,这样也可以阻止用户继承(不过一般没必要)。
- 另外,就可以像用NSArray一样,通过[[XGFEAppleView alloc] init],[[XGFEAppleView alloc] initWithType:@"red"] 这样更友好的方式使用,也会大大降低使用者后续迁移和库开发者内部重构的风险。
- 另外,整个写法中,虽然多存在了一个单例的工厂类,代码语法上不规范处很多理解成本较高。但是类簇完成之后,使用收益(业务代码可维护性,类安全,重构成本)肯定是大于开发成本的,所以还是可以放心的去写和推广。