Pull to refresh

Избавляемся от строковых констант в Objective-C

Reading time5 min
Views14K
Магические константы в коде — зло. Строковые константы в коде — еще большее зло.
И вроде бы от них никуда не денешься, они повсюду:

1) При загрузке объектов из xib-ов:
MyView* view = [[[NSBundle mainBundle] loadNibNamed:@"MyView" owner:self options:nil] lastObject];

MyViewController* controller = [MyViewController initWithNibName:@"MyViewController" bundle:nil];

2) При работе с CoreData:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:@"MyCoreDataClass" inManagedObjectContext:moc]];
[request setSortDescriptors:@[ [[NSSortDescriptor alloc] initWithKey:@"someProperty" ascending:NO] ]];

3) Если вы используете KVO, то строки появляются и тут:
[self addObserver:someObservedObject 
       forKeyPath:@"someProperty"
          options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) 
          context:nil];

4) Ну и KVC:
NSInteger maxValue = [[arrayOfMyClassObjects valueForKeyPath:@"@max.someProperty"] intValue];

5) Но даже если CoreData вы предпочитаете работу с SQLite напраямую, xib-ами вы брезгуете, то вот такой код вам должен быть знаком:
[self.tableView dequeueReusableCellWithIdentifier:@"MyTableViewCell"];

6) Ну и когда Apple представила миру Storyboard — это было замечательно, если-бы не одно но:
[self performSegueWithIdentifier:@"MySegue" sender:nil]

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:( id )sender {
   if ( [segue.identifier isEqual:@"MySegue"] );
}

Вы видите проблему? Она состоит в том, что компилятор никак не проверяет содержимое строк, поскольку не знает (да и не может в принципе знать), что в них содержится. И если вы опечатаетесь или измените значение соответствующих полей в xcdatamodel / xib / storyboard / переименуете property, то ошибка вылезет не на стадии компиляции, а в рантайме, и отловить и исправить ее будет дольше и дороже.
Так что-же можно сделать?
С некоторыми строками можно справиться административными мерами, с некоторыми — при помощи специального инструментария.

Загрузка из xib-ов

Например, если начать с правила, что имя xib-а должно совпадать с именем класса, который он содержит, то код из первого примера можно переписать так:
MyView* view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([MyView class]) owner:self options:nil] lastObject];

MyViewController* controller = [MyViewController initWithNibName:NSStringFromClass([MyViewController class] bundle:nil];

Плюсом такого решения будет то, что если мы надумаем переименовать наш класс ( Xcode -> Edit -> Refactor -> Rename оставив галочку «Rename related files» выбранной ), то его при переименоввании xib будет так-же переименован, и, соответственно, загрузка вью/контроллера никак не пострадает.

CoreData

С примером 2 решение чуть сложнее и комплекснее.
Для начала нам понадобятся MagicalRecord и mogenerator

Если вы работаете с CoreData и все еще не используете эти замечательные инструмменты, то самое время начать.
Добавим MagicalRecord в наш podfile (ну или по старинке скопировав файлы в проект — за подробностями в гихаб)
pod MagicalRecord

И установим mogenerator:
brew install mogenerator

Или скачаем установщик с сайта и поставим его вручную.

mogenerator создает на основе файла модели CoreData исходные файлы, которые иначе пришлось бы писать руками.
Теперь нам нужно настроить проект так, чтобы запускать утилиту при каждой сборке.
Project -> Targets -> Add Build Phase -> Add Run Script:
Идем в настройки таргета, в закладку Build Phases и добавляем новую фазу в виде скрипта:
image
Ну и сам скрипт:
image
На одном уровне с нашей моделью нужно создать две папки Human и Machine. Жмем CMD+B — после чего добавляем эти две папки в проект. Они будут содержать в себе сгенерированные файлы модели из CoreData.

KVO и KVC

Еще одна вещь в Objective-C, которая активно использует строковые константы — KeyPath для KVO и KVC. Можно использовать упомянутый выше mogenerator. Если у нас в модели есть класс MyCoreDataClass, то mogenerator создаст структуры MyCoreDataClassAttributes, MyCoreDataClassRelationships и MyCoreDataClassFetchedProperties. Так что теперь можно переписать пример № 2 так:
NSArray* arr = [MyCoreDataClass MR_findAllSortedBy:MyCoreDataClassAttributes.someProperty ascending:NO inContext:moc];

А пример № 3 так:
[self addObserver:myCoreDataClass
       forKeyPath:MyCoreDataClassAttributes.someProperty
          options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) 
          context:nil];

Но такое решение подойдет только для CoreData. Нам же нужно что-то более общее.

Для этого вполне подойдет библиотека Valid-KeyPath:
#import "EXTKeyPathCoding.h"

[self addObserver:myClass
       forKeyPath:KEY.__(MyClass, someProperty)
          options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) 
          context:nil];

Либо EXTKeyPathCoding библотеки libextobjc:
pod libextobjc

#import "MTKValidKeyPath.h"

[self addObserver:myClass
       forKeyPath:@keypath(MyClass.new, someProperty)
          options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) 
          context:nil];

Плюсами обоих решений будет autocompletion из IDE во время написания самого кода, а также проверка самого KeyPath на стадии компиляции.

Если вы используете KVC, как в примере 4, то для генерации KeyPath вы можете воспользоваться любой из библиотек, представленных выше, а можете — любой из LINQ-like библиотек для Objective-C, например LinqToObjectiveC
pod LinqToObjectiveC

NSInteger maxValue = [arrayOfMyClassObjects aggregate:^(MyClass* myClass, NSInteger aggregate){
   return MAX( [myClass.someProperty intValue],  aggregate);
}];


Storyboard

Что касается UI части, то тут нам поможет утилита ssgenerator Скачиваем ее отсюда
Создаем для нее еще один скрипт в проекте:
image
После чего добавляем в проект сгенерированные файлы StoryboardSegue.h и StoryboardSegue.m. В этих файлах содержатся категории для тех контроллеров в сториборде, которые содержат в себе UIStoryboardSegue или UITableViewCell, для которых определены идентификаторы. Теперь можно использовать:
[self.tableView dequeueReusableCellWithIdentifier:self.cell.MyTableViewCell];

[self performSegueWithIdentifier:self.segue.MySegue sender:nil]

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:( id )sender {
   if ( [segue.identifier isEqual:self.segue.MySegue] );
}


Заключение


Избавление от строковых констант в коде — не самоцель, а способ существенно сэкономить на написании и поддержке кода за счет проверок во время компиляции. Часть описанных способов требует сторонних утилит, часть — сторонних библиотек. Для некоторых программистов описанные методики покажутся «сложными» и «плохочитаемыми», но все они направлены на как можно раннее выявление ошибок в коде. Так что если вы пишете код, который не изменяется, код, в котором нет ошибок — эти способы не для вас.

Если у вас есть на примете утилиты и библиотеки, которые помогают лично вам избавиться от констант в коде и сделать его более гибким и поддерживаемым — пишите в комментариях, я с удовольствием добавлю их в этот обзор.
Tags:
Hubs:
Total votes 30: ↑26 and ↓4+22
Comments41

Articles