全域性資料同步(三)終極方案

weixin_34107955發表於2017-07-09

在全域性資料同步系列文章中(一)(二)分別解決了model和view的全域性同步,但是依然有一些問題,所以在這裡給一個終極解決方案DDKeyPathChannel

重新來說明下解決的問題

由於各種原因,目前有兩個表示同一種型別的model。

@interface UserModel1 : NSObject

@property (strong, nonatomic) NSString *id;
@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age;

@end

@interface UserModel2 : NSObject

@property (strong, nonatomic) NSString *id;
@property (strong, nonatomic) NSString *nickName;
@property (assign, nonatomic) NSInteger age;

@end

現在需要其中一個屬性修改了,或者第三方要求更新屬性,如何更好的同步各個不同model之間的屬性呢?

另一個問題,又如何把這個狀態更新到UI上呢?

以前方案的問題

首先,來看看之前解決方案的幾個問題。

  • 所有基類都需要實現特定介面協議。這對於model來說會比較簡單,但是對於UIView來說就比較麻煩。
  • 使用上,實現上比較麻煩,需要注意的地方比較多,容易犯錯誤。

那麼有沒有不影響到原來的類的方式呢?

新思路

既然現有的類去實現這個協議比較麻煩,那麼找一個第三方類,永久的實現這個介面,並且把訊息轉發到現有的類不就可以了嗎。

我們都知道有一個類不繼承於NSObject,功能就是代理,那麼我們利用這個類來做訊息轉發。

介面如下

@interface DDKeyPathChannelBaseProxy : NSProxy <DDKeyPathChannelProtocol>

// 以下兩個屬性確定物件唯一性
@property (readonly, nonatomic) NSInteger channelType;
@property (readonly, nonatomic) NSString *channelId;

// 原本的物件
@property (weak, readonly, nonatomic) __kindof NSObject *target;

- (instancetype)initWithChannelType:(NSInteger)channelType channelId:(NSString *)channelId target:(NSObject *)target;

@end

// 這是一個通過keyPath+白名單的實現,可以通過mapper來對映真正的keyPath
@interface DDKeyPathChannelProxy : DDKeyPathChannelBaseProxy

@property (strong, nonatomic) NSArray<NSString *> *whiteList;
@property (strong, nonatomic) NSDictionary<NSString *, NSString *> *keyPathMapper; // messageKeyPath : realKeyPath

@property (strong, nonatomic) void(^valueWillChangeBlock)(__kindof NSObject *target, NSString *keyPath, __kindof id newValue);
@property (strong, nonatomic) void(^valueDidChangeBlock)(__kindof NSObject *target, NSString *keyPath, __kindof id newValue);

@end

// 這是一個block的實現,可以在同步的時候自定義轉換與實現
@interface DDKeyPathBlockChannelProxy : DDKeyPathChannelBaseProxy

@property (strong, nonatomic) NSString *keyPath;

@property (strong, nonatomic) void(^valueChangedBlock)(__kindof NSObject *target, NSString *keyPath, __kindof id newValue);

@end

轉發的核心在於訊息的傳遞

// keyPath
- (void)setValue:(id)value forKey:(NSString *)key {
    if (self.valueWillChangeBlock) self.valueWillChangeBlock(self.target, key, value);
    [self.target setValue:value forKey:key];
    if (self.valueDidChangeBlock) self.valueDidChangeBlock(self.target, key, value);
}

// block
- (void)setValue:(id)value forKey:(NSString *)key {
    if ([key isEqualToString:self.keyPath]) {
        if (self.valueChangedBlock) {
            self.valueChangedBlock(self.target, key, value);
        }
    }
}

那麼我們怎麼去掛載這個代理物件呢,想到associate object,那麼我們也很容易的控制自己的生命週期了。

這樣,我們就不需要在現有類中實現方法來支援該功能了,而且這樣也更好的封裝遮蔽了這些比較特殊的功能。在實踐中感覺這種方式的使用成本是最低的,大家也比較容易接受。

[self.user1 bindChannelType:ChannelTypeUser
                  channelId:self.user1.id];
[self.user2 addChannelProxyWithChannelType:ChannelTypeUser
                                 channelId:self.user2.id
                                    config:^(DDKeyPathChannelProxy *proxy) {
                                        proxy.keyPathMapper = @{ @"name": @"nickName" };
}];
// 更新屬性
[[DDKeyPathChannelManager sharedChannel] emitChannelType:ChannelTypeUser
                                               channelId:@"1"
                                                   value:@"Tom"
                                              forKeyPath:@"name"];
[[DDKeyPathChannelManager sharedChannel] emitChannelType:ChannelTypeUser
                                               channelId:@"1"
                                                   value:@(30)
                                              forKeyPath:@"age"];

UI層更新也可以通過這種方式,也可以選擇使用KVO。

題外話

關於這個功能,很多人肯定想到了ReactiveX,關於這點,兩者的確有部分相似的場景,但也有很多不同的地方。

關於更新UI這點,兩者從效果上來看的確是一致的

object -> (signal, keyPath) -> UI

兩者最大的不同在於,ReactiveX是monad的思想,是有輸入輸出,擁有明確的輸入物件和觀察物件,行為流程是從上游到下游。而本套方案是一箇中間人模式,是一個星狀結構,更像通知一點。

但是兩者思想是類似的,ReactiveX是把各種行為封裝成Signal,而我們是把訊息使用keyPath來承載與轉發。

如果想要使用ReactiveX來實現這個功能也不是不可以,建立一個全域性的熱型號(subject),控制好回收(dispose),也是可以實現該功能,但總感覺和RX的概念有點偏差了。

總結

從第一篇方案,到現在最終比較完美的一套方案,也是因為我們的需求在一步一步的變化,要求我們使用更好、更靈活的方案才能滿足的結果。這個過程是一個不斷思考不斷反思的過程,從這個方案的演化中,我深有感悟,很多東西在創造出來的時候看似完美,但實際上還有很大的完善空間,同時別人的方案也會對自己的想法有很多的幫助。所以多瞭解別人的實現方案對自己的提升還是很有幫助的。

相關文章