基于 message forwarding 的轻量依赖注入容器实现

371 阅读2分钟

TL.DR

将多个 protocol 的 implementation 封装到一个类中,运行时通过 message forwarding 将消息转发给内部持有的 implementation。

问题描述:init 越来越长了

在 MVVM 中,VM 通过 init 接收底层的功能模块

- (instancetype)initWithSimpleManager:(id<SimpleManager>)simpleManager
                          otherParams:(WhatEverType)otherParams
    NS_DESIGNATED_INITIALIZER;

随着时间的推移,VM 依赖的模块越来越多,渐渐变成这样

- (instancetype)initWithThisManager:(id<ThisManager>)thisManager
                        thatManager:(id<ThatManager>)thatManager
                    evenMoreManager:(id<EvenMoreManager>)evenMoreManager
                       //...
                        otherParams:(WhatEverType)otherParams
    NS_DESIGNATED_INITIALIZER;

看着难受,用着难受,重构起来更难受。

理想中的效果

// ViewModel.h
- (instancetype)initWithManager:(id<ThisManager, ThatManager, EvenMoreManager, ...>)manager
                    otherParams:(WhatEverType)otherParams
    NS_DESIGNATED_INITIALIZER;

传入的 manager 支持指定任意个 protocol

// ManagerFactory.h
@interface ManagerFactory: NSObject

+ (CompoundManager *)managerConform2:(NSArray<Protocol *> *)protocols;

@end

然后

id<ThisManager, ThatManager, EvenMoreManager, ...> manager = [ManagerFactory managerConform2:@[@protocol(ThisManager), @protocol(ThatManager), @protocol(EvenMoreManager), ...]];
ViewModel *vm = [[ViewModel alloc] initWithManager:manager
                                       otherParams:...];

One manager to rule them all。

也就是说需要两个部件

  • ManagerFactory,根据传入的 protocol 初始化并持有对应的 manager 实例,最终返回 CompoundManager
  • CompoundManager,接收到 vm 的方法调用消息后,转发给实现了该方法的对应 manager

一步步来看。

解决方案

第一步:ManagerFactory

比如有

@protocol ThisManager<NSObject>
// ...

@end

@interface ThisManager<XXXThisManager>
//...

@end

如果应用内 class 量级较小,可以通过 runtime 查找

NSArray<Class> *NSFClassesThatConformsToProtocol(Protocol *protocol)
{
    Class *classes = NULL;
    NSMutableArray *collection = [NSMutableArray array];
    int numClasses = objc_getClassList(NULL, 0);
    if (numClasses == 0)
    {
        return @[];
    }
    
    classes = (__unsafe_unretained Class*)malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);
    for (int index = 0; index < numClasses; index++)
    {
        Class aClass = classes[index];
        if (class_conformsToProtocol(aClass, protocol))
        {
            [collection addObject:aClass];
        }
    }
    free(classes);
    
    return collection.copy;
}

于是有

// ManagerFactory.m
+ (id)managerConform2:(NSArray<Protocol *> *)protocols
{
    // ...
    for (Protocol *protocol in protocols)
    {
        id manager = NSFClassesThatConformsToProtocol(protocol);
        // ...
    }
    // ...
}

如果 class 很多,可以简单地采用注册制

// ManagerFactory.h
+ (void)registerManager:(Protocol)managerProtocl withClass:(Class)managerClass;

这里就不细说了。

第二步:CompoundManager

先要持有这些初始化了的 manager 实例

// CompoundManager.h
@interface CompoundManager : NSObject

- (instancetype)initWithManagers:(NSArray<id<NSObject>> *)managers 
                        NS_DESIGNATED_INITIALIZER;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

@end

// CompoundManager.m
@implementation CompoundManager

- (instancetype)initWithManagers:(NSArray<id<NSObject>> *)managers
{
    if (self = [super init])
    {
        self.managers = managers;
    }
    
    return self;
}

// ...

@end

收到一个 selector 之后,要从 self.managers 中找出合适的一个

// CompoundManager.m
- (id<NSObject>)rules:(SEL)selector
{
    // 多个 manager 实现了相同的 selector,排前面的优先响应 
    // 可以继承后 override 此方法来定制特定场景的 rules
    for (id<NSObject> manager in self.managers)
    {
        if ([manager respondsToSelector:selector])
        {
            return manager;
        }
    }
    
    return nil;
}

消息转发很简单

#pragma mark - Forwarding
- (BOOL)respondsToSelector:(SEL)selector
{
    return [self rules:selector] != nil;
}

- (id)forwardingTargetForSelector:(SEL)selector
{
    return [self rules:selector];
}

实现 - NSFPrioritizedDelegateContainer

这实际上是第二步 CompoundManager 的实现,可以和不同类型的 "ManagerFactory" 组合使用。

一个比较有趣的使用场景是 MVVM 与 tableView,避免胶水代码

代码见 NSFPrioritizedDelegateContainer,单元测试见 NSFPrioritizedDelegateContainerSpec

实际的实现中,还支持全部或部分地弱引用传入的 delegates

@interface NSFPrioritizedDelegate : NSObject
@property (readonly) id<NSObject> content;

/// 是否需要在 container 中弱引用 delegate
@property (readonly) BOOL weakRef;

@end

@interface NSFPrioritizedDelegateContainer : NSObject

- (instancetype)initWithPrioritizedDelegate:(NSArray<NSFPrioritizedDelegate *> *)delegates NS_DESIGNATED_INITIALIZER;

@end

这适用于一些特定场景,比如某个 Manager 仅在用户登录状态下存活,登出时即销毁。

NSFPrioritizedDelegateContainer 不应该影响到这样的逻辑。