什么是 POSLens?
POSLens 是一个用于使用函数式透镜存储和更新 持久化数据结构 的 Objective-C 库。
POSLens 在应用程序中的角色与通用数据库相同。同时,当数据结构相对较小时,它是一个更好的选择,而使用大型持久化框架可能就像是一种过度设计。接下来的部分将解释在 iOS 项目中何时以及如何使用透镜。
透镜的使用案例
该库的主要职责如下:
-
数据同步
所有主流的 iOS 数据库都能在多线程环境中工作。这是一个关键特性,因为有许多情况需要应用逻辑在多个线程上同时读取和更新对象。在一个操作不同线程上的数据系统中,我们必须考虑竞争条件。当对象的图很大时,我们将其保存在数据库中。数据库隐藏了客户端大量关于多线程读写同步的繁杂工作。但是我们如何处理内存对象上的竞争条件呢?不可变数据结构是对此问题的回答,而 POSLens 是更新它们的途径。
-
数据持久化
该库以 ACID 兼容的方式将数据结构序列化和反序列化到不同的存储中。与数据库不同,POSLens 在内存中加载整个对象的图,这就是为什么该库不能在数据需要大量 RAM 的情况下使用。POSLens 发挥作用的良好案例是应用程序设置和远程配置的持久管理。POSLens 提供了最常用 iOS 数据存储的统一接口
- 密钥链
- 文件
- NSUserDefaults
- 内存中的
如果它们适用于您的数据,那么 POSLens 很可能是一个管理它们的合适工具。
图书馆的结构
图书馆的结构相当简单。
POSLens
提供只读访问管理对象的能力,并发射有关对象更新的通知。POSMutableLens
为POSLens类添加了额外的用于修改管理对象的方法。POSValueStore
实例实现了对象持久化的特定存储逻辑。POSLensValue
是由透镜管理的对象。它应该至少符合NSCopying协议,因为POSMutableLens通过“写时复制”习语来更新它。当POSValueStore需要持久化对象时,也需要实现NSCoding协议。
图书馆完全将数据访问器与数据持久化分离。只有根透镜有存储服务的引用,具体实现由POSLensStore协议隐藏。这使得将特定应用的存储扩展到库中成为可能。
使用透镜
创建透镜
让我们为某些需要处理认证数据和启动保护设置的应用声明数据模型和服务。
// Authenticator.h
@interface AccountCredentials : NSObject <NSCopying, NSCoding>
@property (nonatomic, readonly) NSString *email;
@property (nonatomic, readonly) NSString *password;
- (instancetype)initWithEmail:(NSString *)email password:(NSString *)password;
@end
@interface Authenticator : NSObject
@property (nonatomic, readonly) POSLens<AccountCredentials *> *credentials;
- (instancetype)initWithCredentials:(POSMutableLens<AccountCredentials *> *)credentials;
@end
// LaunchProtector.h
@interface AccountProtectionsSettings : NSObject <NSCopying, NSCoding>
@property (nonatomic, readonly) BOOL enabled;
@property (nonatomic, readonly, nullable) NSString *passcode;
- (instancetype)initWithEnabled:(BOOL)enabled passcode:(nullable NSString *)passcode;
@end
@interface LaunchProtector : NSObject
@property (nonatomic, readonly) POSLens<AccountProtectionsSettings *> *settings;
- (instancetype)initWithSettings:(POSMutableLens<AccountProtectionsSettings *> *)settings;
@end
// App.m
@interface AccountInfo : NSObject <NSCopying, NSCoding>
@property (nonatomic, readonly) AccountCredentials *credentials;
@property (nonatomic, readonly, nullable) AccountProtectionsSettings *protectionSettings;
- (instancetype)initWithCredentials:(AccountCredentials *)credentials
protectionSettings:(nullable AccountProtectionsSettings *)protectionSettings;
@end
@interface TheApp : NSObject
@property (nonatomic, readonly) POSMutableLens<AccountInfo *> *accountInfo;
@property (nonatomic, readonly) Authenticator *authenticator;
@property (nonatomic, readonly) LaunchProtector *launchProtector;
@end
@implementation TheApp
// ...
- (void)bootstrap {
_accountInfo = [POSMutableLens
lensWithDefaultValue:nil
keychainService:@"my.app"
valueKey:@"accountInfo"
error:nil];
_authenticator = [[POSAuthenticator alloc]
initWithAccountCredentials:_accountInfo[@"credentials"]];
_launchProtector = [[POSLaunchProtector alloc]
initWithSettings:_accountInfo[@"protectionSettings"]];
}
// ...
@end
根透镜是使用基于安全密钥库的初始化器创建的。对象的整个图在每次修改后都会从和安全存储加载并保存。还有一个更通用的透镜初始化器,其中显式提供了持久数据存储。它可以用来创建具有自定义存储或更高级选项的内置存储的透镜。
id<POSValueStore> store = [[POSKeychainValueStore alloc]
initWithValueKey:@"accountInfo"
service:@"my.app"
accessGroup:@"my.app.access_group"];
_accountInfo = [POSMutableLens lensWithDefaultValue:nil store:store error:nil];
请注意,应用将服务仅绑定到应用程序状态的具体部分。与Brandon Williams引入并由Elviro Rocca在其精彩的连载文章中描述的Swift透镜不同,POSLens
类只提到了底层对象的类型,而没有提到对象所有者的类型。通过这种方式,POSLens
客户端与整个数据结构解耦,因此可以在完全不同的上下文中重用。在上面的代码中,Authenticator已与AccountCredentials
相连,而LaunchProtector与ProtectionsSettings
相连。每个服务只了解其状态,而对整个应用程序内容了一无所知。
读取值
可以通过value
属性访问一个管理对象。
AccountInfo *accountInfoValue = _accountInfo.value;
只有根透镜会保持对基础对象的强引用。其部分的透镜会在需要时懒加载它们的值。
POSLens
通过使用字符串键在诸如NSDictionary和NSObject等父数据结构中提取实际值。
- NSDictionary有一个内置的键的概念,不需要任何暗黑魔法就可以用它来查找对象。
- 基于NSObject的实例的属性通过键值编码机制进行排队。
POSLens
统一了访问基于NSDictionary、基于NSObject和混合对象层次中具有相同键路径的对象的接口。
_accountInfo = [POSMutableLens lensWithValue:@{
@"credentials": @{
@"email": @"[email protected]",
@"password": @"123"
},
@"protectionSettings": @{
@"enabled": @YES,
@"passcode": @"123"
}
}];
_authenticator = [[POSAuthenticator alloc]
initWithAccountCredentials:_accountInfo[@"credentials"]];
_launchProtector = [[POSLaunchProtector alloc]
initWithSettings:_accountInfo[@"protectionSettings"]];
对ProtectionsSettings
实例的"enabled"属性的透镜在两种AccountInfo
实现中具有相同的键路径。下面是如何使用基于索引的API获取POSLens
对象的方法。
POSLens<NSNumber *> *enabled = _accountInfo[@"protectionSettings"][@"enabled"];
动态对象查找打开了创建可选对象透镜的可能性。例如,如果AccountInfo
对象中不存在ProtectionsSettings
实例,仍然可以为其或其属性创建透镜。此外,这些透镜将在底层对象可用时发出更新通知并解决实际值。
POSMutableLens<AccountInfo *> *accountInfo = [POSMutableLens lensWithValue:
[[AccountInfo alloc]
initWithCredentials:[[AccountCredentials alloc]
initWithEmail:@"[email protected]"
password:@"123"]
protectionSettings:nil]
];
POSLens<NSNumber *> *enabled = _accountInfo[@"protectionSettings"][@"enabled"];
[enabled.valueUpdates subscribeNext:^(NSNumber * _Nullable x) {
// Process new enabled value.
}];
[accountInfo[@"protectionSettings"]
updateValue:[[AccountProtectionsSettings alloc] initWithEnabled:YES passcode:@"123"]
error:nil];
POSLens
支持可选对象的默认值。有一个专门的工厂方法,可以指定它们。
AccountProtectionsSettings *defaultSettings = [[AccountProtectionsSettings alloc]
initWithEnabled:NO
passcode:nil];
POSLens<AccountProtectionsSettings *> *settings = [_accountInfo
lensForKey:@"protectionSettings"
defaultValue:defaultSettings];
当客户端代码在不同级别的透镜层次结构上为同一对象提供一个默认值时,则更高级的实例具有更高的优先级。默认值不是对象图的组成部分,透镜不会将其保存在存储中。指定新默认值是应用程序的责任,用于下一个版本的相同可选对象。
更新值
更新管理对象最直接的方式是使用更新方法。
POSMutableLens<NSNumber *> *enabled = _accountInfo[@"protectionSettings"][@"enabled"];
[enabled updateValue:@NO error:nil];
当更新逻辑由多个步骤组成或依赖于管理对象的当前状态时,则基于块的更新方法更合适。这些情况的最简单例子是并发属性递增和根据另一个属性的值修改某些属性。POSLens类使用多读/单写锁来控制对管理对象的访问,因此所有更新块都是串行执行的。换句话说,同一时间内只能有一个客户端修改对象的状态。
typedef POSAccountProtectionsSettings Settings;
POSMutableLens<Settings *> *settings = _accountInfo[@"protectionSettings"];
[settings updateValueWithBlock:^Settings *(Settings *actual, NSError **error) {
// Neither thread can update passcode value while this block is executing.
if (actual.passcode.length > 0) {
// Enabling protection only if passcode is valid...
return [[Settings alloc] initWithEnabled:YES passcode:actual.passcode];
} else {
// Return an error otherwise...
*error = [NSError
errorWithDomain:@"my.app.error"
code:0
userInfo:@{NSLocalizedDescriptionKey: @"Passcode is invalid."}];
return nil;
}
} error:nil]; // <- Error may be received here.
POSLens
永远不会修改管理对象的实际实例。客户端代码可以自由操作提取的对象,而无需任何锁。透镜使用“写时复制”惯用法根据以下递归步骤修改底层值
- 解决管理对象的实际实例。
- 创建解决值的副本。
- 修改解决值的副本。
- 要求父透镜更新对象的拥有者以管理对象的新值
这种方式,每次修改都会创建修改对象的实例以及所有直接和间接的父对象的实例。这就是为什么兼容透镜的对象应该至少遵循NSCopying协议。NSDictionary默认支持NSCopying功能,但更精细的基于NSObject的状态类应显式实现克隆。
以下图表说明了在B2实例更新后,新对象图的样子。注意,B2、B和R的先前行版本仍然可以访问并保留它们的过时但有效的状态。
POSLens
保证每次更新都会以一致的状态修改并持久化底层存储中的整个数据结构,或者在发生错误时保持数据结构的原始状态。要启用持久化功能和使用POSLens
对象结合如下支持的数据存储,如密钥链、文件和NSUserDefaults,管理对象也应实现NSCoding协议。
更新可选值
在某些情况下,如果该值的拥有者也是可选的,更新可选值可能会很棘手。上一节中提到,当可用的管理对象的新版本出现时,透镜会克隆父对象。如果父对象不存在,那么透镜更新其子对象唯一的方法就是使用父对象的默认值。在这种情况下,默认值晋升为真实值,并且更新过程结束时,该值将作为对象图的一部分被持久化。如果某个直接或间接的父对象没有真实值或默认值,那么更新方法将以错误结束。
接收值更新通知
POSLens
类包含valueUpdates
信号,它发射管理对象的实例。客户端代码在订阅时接收此类通知,并在管理对象或其部分每个更新时接收。以下图表说明了B2对象更新时哪些透镜将发出新的实际实例。
扩展性
POSLens库可以通过自定义数据存储进行扩展。它们应遵循POSValueStore
协议。自定义存储可以选择任何方式保存和加载对象图。所有内置存储使用NSKeyedArchive
持久化其值,因此POSLensValue
应遵循NSCoding
协议。如果一个自定义存储同样依赖于管理对象的NSCoding兼容性,那么它可以派生自POSPersistentValueStore
类,该类实现了对象序列化和反序列化的大部分工作。