Computer programs are the most complex things that humans make. It is also the nature of software to be extensively modified over its productive life. If we can read and understand it, then we can hope to modify and improve it.
© Douglas Crockford, автор спецификации JSON
JSON — это мост между двумя мирами: миром веб-сервисов и миром клиентских приложений. Однако мост не настолько совершенен, чтобы данные существовали в одном формате. Пока что мы всегда вынуждены преобразовывать информацию в представление того языка, с которым работаем, для архитектуры того приложения, которое пишем. Для того чтобы такое преобразование было успешным, оно должно быть в первую очередь простым.
Есть много способов превращения JSON в Objective-C обьекты, однако многие из них имеют свои недостатки, которые мешают с ними работать. Есть известный и любимый многими RestKit, однако он, к сожалению, эффективно работает только при наличии идеального REST API. Шаг в сторону — и вы будете забивать гвозди микроскопом, не понимая, зачем нужно писать такие сложные конструкции для достаточно простых вещей. Есть решение от разработчиков GitHub — Mantle, однако с ним вы будете вынуждены наследоваться от базового класса Mantle и постоянно использовать NSValueTransformer — не самую популярную технологию в iOS/Mac OS разработке.
Я хочу рассказать о фреймворке, который недавно нашелся на просторах GitHub, и который позволяет достаточно просто и красиво преобразовывать JSON в Objective-C обьекты — EasyMapping.
Если заинтересовались, добро пожаловать под кат!
Задача
Возьмем для примера следующий JSON:
{
"name": "Lucas",
"email": "notexisting@gmail.com",
"gender" : "male",
"car": {
"model": "i30",
"year": "2013"
},
"phones": [
{
"ddi": "55",
"ddd": "85",
"number": "1111-1111"
},
{
"ddi": "55",
"ddd": "11",
"number": "2222-222"
}
]
}
В Objective-C будем использовать такие классы:
typedef enum {
GenderMale,
GenderFemale
} Gender;
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, assign) Gender gender;
@property (nonatomic, strong) Car *car;
@property (nonatomic, strong) NSArray *phones;
@end
@interface Car : NSObject
@property (nonatomic, copy) NSString *model;
@property (nonatomic, copy) NSString *year;
@end
@interface Phone : NSObject
@property (nonatomic, copy) NSString *DDI;
@property (nonatomic, copy) NSString *DDD;
@property (nonatomic, copy) NSString *number;
@end
Реализация
Для каждого класса необходимо создать маппинг (EKObjectMapping), например:
EKObjectMapping * mapping = [[EKObjectMapping alloc] initWithObjectClass:[Person class]];
После чего необходимо добавить соотношения ключ-значение для связи <ключ в JSON> -> <имя свойства обьекта>. Рассмотрим наиболее часто встречающиеся ситуации при маппинге и обработку этих ситуаций при помощи EasyMapping.
1. Ключи в JSON совпадают с именами свойств обьекта
Пример, класс Person:
[mapping mapFieldsFromArray:@[@"name",@"email"]];
2. Ключи в JSON отличаются от имен свойств обьекта
Пример, класс Phone:
[mapping mapFieldsFromDictionary:@{@"ddi":@"DDI", @"ddd":@"DDD"}];
3. Значение требует дополнительной обработки
Пример, класс Person
NSDictionary *genders = @{ @"male": @(GenderMale), @"female": @(GenderFemale) };
[mapping mapKey:@"gender" toField:@"gender" withValueBlock:^(NSString *key, id value) {
return genders[value];
}];
4. Обьект содержит обьект другого класса
Пример, класс Person
[mapping hasOneMapping:[Car mapping] forKey:@"car"];
5. Обьект содержит массив обьектов другого класса
Пример. Класс Person
[mapping hasManyMapping:[Phone mapping] forKey:@"phones"];
Результат
Полностью маппинги для наших классов:
+ (EKObjectMapping *)carMapping
{
EKObjectMapping * mapping = [[EKObjectMapping alloc] initWithObjectClass:[Car class]];
[mapping mapFieldsFromArray:@[@"model", @"year"]];
return mapping;
}
+ (EKObjectMapping *)phoneMapping
{
EKObjectMapping * mapping = [[EKObjectMapping alloc] initWithObjectClass:[Phone class]];
[mapping mapFieldsFromArray:@[@"number"]];
[mapping mapFieldsFromDictionary:@{
@"ddi" : @"DDI",
@"ddd" : @"DDD"
}];
return mapping;
}
+ (EKObjectMapping *)personMapping
{
EKObjectMapping * mapping = [[EKObjectMapping alloc] initWithObjectClass:[Person class]];
NSDictionary *genders = @{ @"male": @(GenderMale), @"female": @(GenderFemale) };
[mapping mapFieldsFromArray:@[@"name", @"email"]];
[mapping mapKey:@"gender" toField:@"gender" withValueBlock:^(NSString *key, id value) {
return genders[value];
}];
[mapping hasOneMapping:[self carMapping] forKey:@"car"];
[mapping hasManyMapping:[self phoneMapping] forKey:@"phones"];
return mapping;
}
Теперь, когда маппинг готов, создание обьекта Person выглядит следующим образом:
NSDictionary * personProperties = ...; // JSON обьекта Person, приведенный выше, распарсенный в словарь.
Person * person = [[Person alloc] init];
[EKMapper fillObject:person fromExternalRepresentation:personProperties withMapping:mapping];
Таким образом, при создании обьекта не используются никакие конструкции if-else, отсутствуют проверки на NSNull. Кроме того, существующие обьекты можно легко обновить новыми данными, если такая потребность возникнет.
Дополнительные плюшки EasyMapping
1. Поддержка CoreData
2. Встроенная проверка на NSNull — NSNull автоматически заменяется на nil.
3. Возможность сериализации обьектов в NSDictionary/NSArray
4. Поддержка установки через CocoaPods
5. Полностью покрыт тестами.
Удачные практики при реализации
Заполнение обьекта при помощи EKMapper лучше вынести в базовый класс, скажем, BaseModel, следующим образом:
- (id)initWithProperties:(NSDictionary *)properties
{
if (self = [super init])
{
[EKMapper fillObject:self fromExternalRepresentation:properties
withMapping:[[self class] objectMapping];
}
return self;
}
+ (EKObjectMapping *)objectMapping
{
//Do nothing. Implement in subclass if you want to initialize object
//via object mapping
return nil;
}
Таким образом, создание обьекта будет выглядеть так:
Person * person = [[Person alloc] initWithProperties:personProperties];
Заключение
Каждый качественный open-source продукт — это трамплин, который позволяет нам как программистам прыгать все выше и выше. Проекту EasyMapping всего лишь один месяц, если судить по первому коммиту, однако, на мой взгляд, он уже может соперничать с намного более старыми и продвинутыми решениями, по крайней мере, своей простотой. Хочется пожелать удачи автору этого интересного фреймворка, а всем, кто дочитал до конца, — спасибо за ваше время и удачи в путешествиях по JSON'у!
Ссылки
1. EasyMapping на github
2. CocoaPods
Альтернативные решения
Автор: DenHeadless