Aug 27

从NSArray的实现看一个友好的类簇的实现

Lrdcq , 2018/08/27 21:39 , 程序 , 閱讀(2726) , Via 本站原創
在OC-Foundation与UIKit中,苹果使用类簇来实现具体工厂子类用得出神入化。有意思得是,作为工厂设计模式的一种实践,似乎只有苹果自己大量使用类簇,在别的语言下类簇似乎并不是一个优雅的最佳实践。那么仿照苹果的类簇类型的设计,我们如何才能实现一个API友好的类簇类型呢。

首先我们自己的代码中既有常见的类簇类型的实践方式是这样的:

@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"] 这样更友好的方式使用,也会大大降低使用者后续迁移和库开发者内部重构的风险。

- 另外,整个写法中,虽然多存在了一个单例的工厂类,代码语法上不规范处很多理解成本较高。但是类簇完成之后,使用收益(业务代码可维护性,类安全,重构成本)肯定是大于开发成本的,所以还是可以放心的去写和推广。
关键词:oc
logo