NTJsonModel 1.00

NTJsonModel 1.00

测试测试
语言语言 Obj-CObjective C
许可协议 MIT
发布最新版本2015 年 3 月

Ethan Nagel 维护。



  • 作者:
  • Ethan Nagel

NTJsonModel 为 JSON 对象提供了一款易于使用且性能高强的包装类。它具有直观的属性声明模型,并会保留原始 JSON。

NTJsonModel 的独特之处在于它支持对象的不可变和可变状态,并为您提供了将这种特性应用于模型对象的工具。如果您利用这些特性,可以拥有不可变模型对象,仅在特定的上下文中进行“修改”,类似于您在函数式语言中看到的方法。

概述

  • 一个轻量级的现有 JSON 对象包装器。对象的创建非常高效。所有属性和数组首次请求时才会加载数据。
  • 属性使用结合 @property 声明和宏 NTJsonProperty 声明。
  • 对象可以创建为不可变或可变的(不可变是默认值;可以使用可变初始化器或 mutableCopy 创建可变对象)。通过一些额外的工作,您可以使用不可变对象贯穿整个应用程序的生命周期,仅在受控的“修改”块中更改。
  • 保留了原始 JSON,包括所有未映射到模型中的 JSON 值。
  • 属性通过将 @property 声明与每个属性的 @implementation 中的一个单行代码相结合(使用 NTJsonProperty 宏)进行声明。
  • 支持转换属性(例如,透明地将字符串转换为 UIColor),双向操作。
  • 支持对象缓存,允许 JSON 中的对象 ID 返回为从数据存储加载的对象,例如。

声明属性


属性通过结合标准 @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 宏

NTJsonProperty 是一个需要将属性名作为第一个参数的宏。它还包含多个可选参数,形式为 key=value。

  • jsonPath="path" - 为此属性设置 JSON 路径。默认情况下这与属性名匹配。允许嵌套的 jsonPaths(即 "a.b")用于只读属性。
  • enumValues=NSArray - 定义一个包含字符串的数组,这些字符串定义此属性的合法值。有关详细信息,请参阅 基于字符串的枚举
  • cachedObject=YES - 指定这应被视为一个缓存对象,这是一个特殊的转换案例。有关更多信息,请参阅 对象缓存
  • elementType=class - 允许您为数组设置元素类型。还可以使用协议语法,这更为优雅。请参见下文的 声明类型数组

声明类型数组

NTJsonModel 支持声明类型数组,首次访问时它们将自动转换为子 NTJsonModel 或其他原生类型。您可以使用 NTJsonPropertyelementType= 参数或使用协议来实现。要使用协议,只需声明一个与类同名的协议,并使数组符合该协议。(感谢 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 属性检查模型是否不可变。尝试在不可变实例上设置属性值将引发异常。

这与我们看到的类似于 NSMutableArrayNSArray 的对象的处理方式相似,但我们只需一个可以在可变或不可变状态下创建的对象。 (通过做更多的工作,有方法可以让编译器强制不可变和可变属性,请参见 可变协议

可变对象不是线程安全的,您必须自行执行强制。您可以使用 initMutable(创建空的可变实例)、initMutableWithJsonmutableModelWithJson: 创建可变实例。此外,您还可以通过在任何模型上调用 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
  • 您可能会发现直接在类中实现协议非常有吸引力 - 不要这样!虽然这似乎完全合理,但它将更改属性的元数据,并创建与 NTJson 属性的动态性质相关的问题。在这里,让我用我的话来说,它可能看起来工作正常,但您会遇到问题。
  • typedef 声明为实现可变协议的类的实例是可选的,但将在以后使用可变协议时简化语法。对于类 'XXX' 的基本格式是 typedef XXX<MutableXXX> *MutableXXX; 这相当于创建一个名为 MutableXXX 的类型,它是一个实现协议 MutableXXX 的类 XXX
  • 如果您有可变协议(使用 NTJsonMutable 宏声明),类中的所有 NTJson 属性必须声明为 readonly。任何 readwrite NTJson 属性在第一次访问类型时将导致异常。
  • 如果您创建修改对象(可变)的函数,请将函数声明在可变协议中。由于属性在类中声明为 readonly,因此尝试修改属性 - 即使在您打算使其可变的函数中 - 都会导致编译器错误。特殊属性 mutableSelf 将允许您显式访问属性的设置器。(这实际上是将您的 self 指针转换为可变协议。)

属性转换


虽然JSON易于解析且非常通用,但它确实缺乏丰富性。NTJsonModel使得定义转换器(或变换器)变得简单,这些转换器在自动将底层JSON转换为丰富值时会自动被调用。系统将通过三个地方进行检查,以寻找一个满足转换条件的类方法

  1. 在模型类中查找属性名重写。签名约定是+(id)convert<propertyName>ToJson:(id)json+(id)convertJsonTo<propertyName>(id)json。此外,还可以选择使用+(id)validateCached<propertyName>:(id)value forJson:(id)json进行缓存的值验证
  2. 在模型类中查找类名重写。签名约定是+(id)convert<className>ToJson:(id)json+(id)convertJsonTo<className>:(id)value。此外,还可以选择使用+(id)validateCached<className>:(id)value forJson:(id)json进行缓存的值验证
  3. 在值类中查找“NTJsonPropertyConversion”协议的实现。签名约定是+(id)convertValueToJson:(id)value+(id)convertJsonToValue:(id)value。此外,还可以选择使用+(id)validateCachedValue:(id)value forJson:(id)json进行缓存的值验证。这些方法符合“NTJsonPropertyConversion”协议。

以下示例

@interface User : NTJsonModel

@property (nonatomic) NSDate *dateCreated;

@end

系统会搜索以下选择器

  1. +(id)convertDateCreatedToJson:(id)json+(id)convertJsonToDateCreated(id)json+(id)validateCachedDateCreated:(id)value forJson:(id)json在类User
  2. +(id)convertNSDateToJson:(id)json+(id)convertJsonToNSDate:(id)value。+(id)validateCachedNSDate:(id)value forJson:(id)json在类User
  3. +(id)convertValueToJson:(id)value+(id)convertJsonToValue:(id)value。+(id)validateCachedValue:(id)value forJson:(id)json在类NSDate

首次读取值时,系统会执行转换并将结果缓存起来,因此重复调用将是高效的。如果有可能该值会过时,您可以实施-(id)validateCachedValue:(id)value forJson:(id)json。如果实现了,这将在每次访问值时被调用;返回NO将使系统获取最新的值。验证在处理从数据存储缓存对象时特别有用。

对象缓存

允许将基本类型,如NSDateUIColorNSURL从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内容创建对象

转换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将输出更详细版本,递归进入嵌套对象,并显示数组的正文。