NTJsonModel 为 JSON 对象提供了一款易于使用且性能高强的包装类。它具有直观的属性声明模型,并会保留原始 JSON。
NTJsonModel 的独特之处在于它支持对象的不可变和可变状态,并为您提供了将这种特性应用于模型对象的工具。如果您利用这些特性,可以拥有不可变模型对象,仅在特定的上下文中进行“修改”,类似于您在函数式语言中看到的方法。
@property
声明和宏 NTJsonProperty
声明。mutableCopy
创建可变对象)。通过一些额外的工作,您可以使用不可变对象贯穿整个应用程序的生命周期,仅在受控的“修改”块中更改。@property
声明与每个属性的 @implementation
中的一个单行代码相结合(使用 NTJsonProperty
宏)进行声明。UIColor
),双向操作。属性通过结合标准 @property
(提供数据类型)和宏 NTJsonProperty
(添加元数据,例如 jsonPath)进行声明。
@interface User : NTJsonModel
@property (nonatomic) NSString *firstName;
@property (nonatomic) NSString *lastName;
@property (nonatomic) int age;
@end
@implementation User
NTJsonProperty(firstName)
NTJsonProperty(lastName)
NTJsonProperty(age)
@end
// usage
NSDictionary *json = ...
User *user = [[User alloc] initWithJson:json];
NSLog(@"Hello, %@", user.firstName);
NTJsonProperty 是一个需要将属性名作为第一个参数的宏。它还包含多个可选参数,形式为 key=value。
jsonPath="path"
- 为此属性设置 JSON 路径。默认情况下这与属性名匹配。允许嵌套的 jsonPaths(即 "a.b")用于只读属性。enumValues=NSArray
- 定义一个包含字符串的数组,这些字符串定义此属性的合法值。有关详细信息,请参阅 基于字符串的枚举。cachedObject=YES
- 指定这应被视为一个缓存对象,这是一个特殊的转换案例。有关更多信息,请参阅 对象缓存。elementType=class
- 允许您为数组设置元素类型。还可以使用协议语法,这更为优雅。请参见下文的 声明类型数组NTJsonModel 支持声明类型数组,首次访问时它们将自动转换为子 NTJsonModel 或其他原生类型。您可以使用 NTJsonProperty
的 elementType=
参数或使用协议来实现。要使用协议,只需声明一个与类同名的协议,并使数组符合该协议。(感谢 JsonModel 颠先采用这种做法。)以下是一个示例
@protocol Address // empty
@end
@interface Address
@property (nonatomic) NSString *street;
@property (nonatomic) NSString *city;
@end
@interface User : NTJsonModel
...
@property (nonatomic) NSArray<Address> *addresses;
@end
@implementation User
NTJsonProperty(address)
-- or, without protocols --
NTJsonProperty(address, elementType=[Address class])
@end
NTJsonModel 支持使用 enumValues=
NTJsonProperty 参数进行基于字符串的枚举。请参见下文的示例来了解如何使用它。如果 JSON 值与任何一个枚举值匹配,则将返回该确切值,允许您使用 ==
而不是 isEqualToString:
,这可能会非常方便。
typedef NSString *UserType;
extern UserType UserTypePrimary;
extern UserType UserTypeSecondary;
@interface User : NTJsonModel
...
@property (nontatomic) UserType type;
+(NSArray *)types;
@end
UserType UserTypePrimary = @"primary";
UserType UserTypeSecondary = @"secondary";
@implementation
NTJsonProperty(type, enumValues=[User types])
@end
...
User *user = ...
if ( user.type == UserTypePrimary )
...
每个模型对象都可以被创建为可变或不可变的。不可变对象是线程安全的,并且非常高效。调用 -initWithJson:
或 +mutableModelWithJson:
将创建一个不可变模型。此外,您还可以通过在任意对象上调用 copy
获取不可变版本。(在不可变对象上调用 copy
仅返回发送者。)您可以使用 isMutable
属性检查模型是否不可变。尝试在不可变实例上设置属性值将引发异常。
这与我们看到的类似于 NSMutableArray
和 NSArray
的对象的处理方式相似,但我们只需一个可以在可变或不可变状态下创建的对象。 (通过做更多的工作,有方法可以让编译器强制不可变和可变属性,请参见 可变协议
可变对象不是线程安全的,您必须自行执行强制。您可以使用 initMutable
(创建空的可变实例)、initMutableWithJson
或 mutableModelWithJson:
创建可变实例。此外,您还可以通过在任何模型上调用 mutableCopy
获取可变版本。
当设置属性时,系统会尽量保持 JSON 与当前数据的一致性。例如,如果您有一个您已经公开为 int 的属性,但在 JSON 中它以字符串的形式存储,设置它将导致系统将字符串设置为 JSON(它始终公开为 int 属性。)如果底层 JSON 已经是 int 类型,则它会存储一个 int。
不可变对象设计用于高性能和线程安全。尽可能使用不可变对象,并在需要更改时创建可变副本,这是一个好的实践。
由于只有一个对象,当您具有可变属性并使用不可变对象时,可能会产生意外的结果。您可能期望以下操作能够工作,但会引发异常
User *user = [User modelWithJson:userJson];
user.age = 21; // Fails with an exception, user is immutable!
您始终可以创建可变对象,这样就会工作
User *user = [User mutableModelWithJson:userJson];
user.age = 21; // succeeds
如果您的代码中混合使用了太多不可变和可变对象,事情可能会变得更加复杂。理想情况下,您应该使用不可变对象,并在非常受控的情况下仅“更改”它们。您可以通过创建可变副本,更新它,然后存储不可变版本来达到上述效果。
User *user = [User modelWithJson:userJson];
User *mutableUser = [user mutableCopy];
mutableUser.age = 21; // allowed, this is a mutable object
user = [mutableUser copy];
当然,更改单个属性会有很多样板代码;mutate:
方法是为了简化这些操作而提供的——请参见下一节。
mutate:
方法mutate 方法封装了创建对象可变拷贝的常见模式,修改后获取不可变结果
-(id)mutate:(void (^)(id mutable))mutationBlock;
此方法创建发送者的可变拷贝,将其传递给修改块并返回结果的不可变拷贝。如您所见,上面的代码使用 mutate 后要简洁得多
User *user = [User modelWithJson:userJson];
user = [user mutate:^(User *mutable) {
mutable.age = 21;
}];
这使得代码更安全、更清晰。
不幸的是,编译器不会在您尝试在不可变对象上设置属性时发出警告(但是在运行时你将会得到异常。)下一节将展示您如何通过略微更多的努力,让编译器为您模型对象强制执行可变性。
通过对您的模型声明进行一些额外的工作,您可以让编译器帮助强制对象的可变性。这涉及到在您的类中将所有属性声明为只读,并创建一个“配对”协议,其中有可更新属性的读写版本。您可以在正常(不可变)条件下使用该类,并在您显式创建对象的可变实例(或拷贝)时应用该协议。这在代码中最能清楚地看出 - 这里是 User
模型的更新版本
@interface User : NTJsonModel
@property (nonatomic,readonly) NSString *firstName;
@property (nonatomic,readonly) NSString *lastName;
@property (nonatomic,readonly) int age;
@end
@protocol MutableUser <NTJsonMutableModel>
@property (nonatomic) NSString *firstName;
@property (nonatomic) NSString *lastName;
@property (nonatomic) int age;
@end
typedef User<MutableUser> MutableUser;
...
@implementation User
NTJsonMutable(MutableUser) // this is required!
NTJsonProperty(firstName)
NTJsonProperty(lastName)
NTJsonProperty(age)
@end
这里是一个例子
User *user = [User modelWithJson:userJson];
这将导致编译器错误
user.age = 21; // compiler error
但这将恰到好处地工作
user = [user mutate:^(MutableUser *mutable) {
mutable.age = 21; // all good!
}];
在声明属性时需要做更多的工作(并且在创建修改属性的函数时有一些额外的工作),但是通过与编译器合作避免可变性的错误非常有帮助,尤其是在大型应用程序中。
以下是一些附加细节
NTJsonMutable
宏将协议绑定到类并允许创建设置器的原因。如果您省略此步骤,则在尝试设置属性时将获得“方法未找到”异常。AdminUser
,我们可以创建一个协议 MutableAdminUser
,它将实现 MutableUser
。typedef
声明为实现可变协议的类的实例是可选的,但将在以后使用可变协议时简化语法。对于类 'XXX' 的基本格式是 typedef XXX<MutableXXX> *MutableXXX;
这相当于创建一个名为 MutableXXX
的类型,它是一个实现协议 MutableXXX
的类 XXX
。NTJsonMutable
宏声明),类中的所有 NTJson 属性必须声明为 readonly
。任何 readwrite
NTJson 属性在第一次访问类型时将导致异常。readonly
,因此尝试修改属性 - 即使在您打算使其可变的函数中 - 都会导致编译器错误。特殊属性 mutableSelf
将允许您显式访问属性的设置器。(这实际上是将您的 self
指针转换为可变协议。)虽然JSON易于解析且非常通用,但它确实缺乏丰富性。NTJsonModel使得定义转换器(或变换器)变得简单,这些转换器在自动将底层JSON转换为丰富值时会自动被调用。系统将通过三个地方进行检查,以寻找一个满足转换条件的类方法
+(id)convert<propertyName>ToJson:(id)json
和+(id)convertJsonTo<propertyName>(id)json。
此外,还可以选择使用+(id)validateCached<propertyName>:(id)value forJson:(id)json
进行缓存的值验证+(id)convert<className>ToJson:(id)json
和+(id)convertJsonTo<className>:(id)value。
此外,还可以选择使用+(id)validateCached<className>:(id)value forJson:(id)json
进行缓存的值验证+(id)convertValueToJson:(id)value
和+(id)convertJsonToValue:(id)value。
此外,还可以选择使用+(id)validateCachedValue:(id)value forJson:(id)json
进行缓存的值验证。这些方法符合“NTJsonPropertyConversion”协议。以下示例
@interface User : NTJsonModel
@property (nonatomic) NSDate *dateCreated;
@end
系统会搜索以下选择器
+(id)convertDateCreatedToJson:(id)json
,+(id)convertJsonToDateCreated(id)json
或+(id)validateCachedDateCreated:(id)value forJson:(id)json
在类User
中+(id)convertNSDateToJson:(id)json
,+(id)convertJsonToNSDate:(id)value。
或+(id)validateCachedNSDate:(id)value forJson:(id)json
在类User
中+(id)convertValueToJson:(id)value
,+(id)convertJsonToValue:(id)value。
或+(id)validateCachedValue:(id)value forJson:(id)json
在类NSDate
中首次读取值时,系统会执行转换并将结果缓存起来,因此重复调用将是高效的。如果有可能该值会过时,您可以实施-(id)validateCachedValue:(id)value forJson:(id)json
。如果实现了,这将在每次访问值时被调用;返回NO
将使系统获取最新的值。验证在处理从数据存储缓存对象时特别有用。
允许将基本类型,如NSDate
,UIColor
或NSURL
从JSON转换到JSON的相同机制,也可以用于从数据存储中缓存对象查找。
@interface FeedItem : NTJsonModel
@property (nonatomic) User *user;
@end
@implementation FeedItem
NTJsonProperty(user, jsonPath='user_id', cachedObject=YES)
@end
...
@implementation User
+(id)convertJsonToValue:(id )json
{
NSString *userId = json;
NSDictionary *userJson = (get json for User from data store with userId)
return [User modelWithJson:userJson];
}
+(id)convertValueToJson:(id)value
{
User *user = value;
return user.userId;
}
在JSON中具有“多态”对象并不少见,其中一个基础类有几个基于某些字段(对象类型)的子类。
+(Class)modelClassForJson:(NSDictionary *)json;
此方法应该检查JSON并返回要创建实例的子类。
假设我们有一个可能是矩形或正方形的“形状”数组。
[
{"type": "rectangle", "x": 8, "y": 16, "width": 64, "height": 32},
{"type": "circle", "x": 32, "y": 32, "radius": 16}
]
这将产生以下接口(为简明起见,这里不包括可变协议)
@interface Shape : NTJsonModel
@property (nonatomic,readonly) NSString *type;
@property (nonatomic,readonly) double x;
@property (nonatomic,readonly) double y;
@end
@interface Rectangle: Shape
@property (nonatomic,readonly) double width;
@property (nonatomic,readonly) double height;
@end
@interface Circle: Shape
@property (nonatomic,readonly) double radius;
@end
Shape实现会声明以下内容
+(Class)modelClassForJson:(NSDictionary *)json
{
if ( [json[@"type"] isEqualToString:@"rectangle"] )
return [Rectangle class];
else if ( []json[@"type"] isEqualToString:@"circle"] )
return [Circle class];
return [Shape class]; // default
}
现在,创建Shape实例实际上会根据JSON创建正确的类型(矩形或圆形)。
通过重写+modelClassForJson:
可以基于JSON内容创建对象
NTJsonModel包含辅助方法(和类),可以将整个JSON对象数组转换为模型对象。这些方法返回一个懒加载的特殊实现version的NSArray,NTJsonModel对象仅在其被引用时创建。以我们上面的shapes示例来说,你可以写一些类似的东西:
NSArray *shapesJson = (get array of JSON objects from somewhere)
NSArray *shapes = [Shape arrayWithJsonArray:shapesJson];
for (Shape *shape in shapes)
{
// do something cool
}
在上面的示例中,每个Shape对象将在循环中引用时实例化。
isEqual:
和hash
按预期工作。description
将输出非默认属性,并尽可能输出有用的信息。此外,fullDescription
将输出更详细版本,递归进入嵌套对象,并显示数组的正文。