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

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

    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] );
    }


    Заключение


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

    Если у вас есть на примете утилиты и библиотеки, которые помогают лично вам избавиться от констант в коде и сделать его более гибким и поддерживаемым — пишите в комментариях, я с удовольствием добавлю их в этот обзор.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 41

      +1
      Маленькое замечение

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


      в 99% случаев будет аналогично:

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


        +1
        И даже так:

        MyViewController* controller = [MyViewController new];
        
          0
          Да, там в реализации по умолчанию
          NSStringFromClass([self class])
          это только примеры, для иллюстрации основной идеи
          +2
          Для iOS — да, но на OS X нужно указывать валидное имя.
          –1
          Познавательно, но непрактично.

          С теми же ксибами, в большинстве проектов приходится делать так
          if (iPad) {
              [KingViewController initWithNibName:@"KingViewController"];
          } else {
              if (iPhone5) {
                  [KingViewController initWithNibName:@"KingViewController-568h"];
              } else {
                  [KingViewController initWithNibName:@"KingViewController-480h"];
              }
          }
          
          


          Как тут упростить диагностику до стадии компиляции без лишних усилий?
            +1
            Попробуйте называть ксибы KingViewController и KingViewController~ipad.xib — это избавит вас от одного сравнения в рантайме. А для решения проблемы 568/480 пикселей в высоту — используйте AutoresizingMask / Autolayout. Это избавит вас от необходимости поддерживать 2 интерфейса вместо одного для iPhone
            • UFO just landed and posted this here
                +3
                Если интерфейс так разительно отличается из-за половины дюйма, да так, что AutoresizingMask / Autolayout не спасают, то стОит подумать. Стоит сильно подумать что не так с интерфейсом. Или почитать про AutoresizingMask / Autolayout подробнее. Или вычислить положение некоторых элементов в рантайме. И только в крайнем случае заводить два ксиба для одного и того-же экрана под 3.5 / 4 дюйма. Так как поддержка двух вьюх для одного и того-же контроллера — занятие утомительное и неблагодарное. ИМХО опять-же
                • UFO just landed and posted this here
                    +3
                    Так серебряной пули не существует. Нужно, конечно, всегда смотреть от контента. Но, согласитесь, что такой UI — это не норма, а скорее наоборот. И для одного окна можно и 2 ксиба завести. Но в подавляющем большинстве случаев UI отлично растягивается/перемещается автоматически.
                    Смысл статьи немного не о именовании ксибов для исключительных случаев — а в том, чтобы помочь компилятору выявлять как можно больше ошибок еще до того, как приложение запустится и упадет
                  +4
                  Дать дизайнеру по почкам?
                  0
                  Ксиб не обязан иметь имя класса. Один ксиб может содержать внутри много разных шаблонов, например все виды ячеек таблиц, используемых в приложении. Эдакая фабрика объектов, чей дизайн создавался в IB. Иными словами, ксиб может быть вьюхой, возможно в большинстве приложений это так и есть, но он вовсе не обязан нею быть.
                    0
                    Не обязан, но желательно, чтобы один ксиб содержал один объект(вьюху, ячейку, контроллер).
                    Если ксиб содерджит 20 вьюх/селл, а нам нужна всего одна, то в память будут загружены и инстанциированы все 20 вьюх.
                    Если же одна вьюшка содержит аутлеты на остальные, то не проблема назвать сам ксиб но имени класса этой вьюшки.
                      0
                      Если нам нужна одна, остальные исчезнут после загрузки, ибо потеряют ссылки, только Dirty память пофрагментировав слегка. Связывать их между собой обычно смысла нет(тем более ячейки таблиц вообще мало с чем можно эффективно связать в IB, внешним по отношению к самой ячейке).
                      Зная эти особенности, можно вполне организовать кеш ячеек, запасая загруженные «вхолостую» впрок.
                      А так-то многие в ксибах хранят вьюху, внутри нее View Based NSTableView, внутри которой руками рисуют кастомную NSTableCellView, автосоздание которой отдают на откуп самому NSTableView, после чего даже не задумываются, что каждый раз, когда табличка создает очередную ячейку, инстанцируется не только ячейка, а весь ксиб.
                      Ну это все лирика. По теме — я согласен с PapaBubaDiop: это непрактично(впрочем, строки тоже не красиво, в этом я согласен с вами).
                      Если уж и надо, чтоб компилятор ругался, так константы объявить можно где-нибудь с нормальными названиями — и компилятор ругаться будет, и автокомплит подсказывать и дополнять, и если уж совсем туго с ориентированием в этом хозяйстве — для страховки тестами покрыть можно.
                    –3
                    Боже упаси меня от авто выравниваний. Все мои кнопки, заданные в ксибах, прыгают смеются дышат. Включение выравнивания их убивает, они не могут двигаться. Для неигровых приложений Ваше предложение разумно.
                      0
                      Игровые приложения на UIKit? Окей.
                        +1
                        На самом деле для большинства казуальных 2-d игр UIKit для меню и всего неигрового — отличный выбор. Для экрана самой игры — тот-же CCViewController (если вы используете cocos2d). Можно хоть xib-ы, хоть storyboard-ы использовать. (у меня так пару игровых приложений написана)
                        Другое дело, что даже красивая анимация, смех и дышание кнопок никак не противоречат авторесайзу
                          0
                          Ну я сейчас подумал что прекрасный Letterpress вроде бы на UIKit тоже, да.
                            0
                            Вот, кстати, замечательная статья о разработке cocos2d игры с использованием Storyboard
                            0
                            Научите как двигать программно кнопки, заданные через ксибы с авто выравниванием, интересно.
                              0
                              Если под авто выравниванием понимаете autoresizing mask, то все банально до неприличия:
                              someButton.center = newCenterCGPoint;

                              Ну и анимировано:
                              [UIView animateWithDuration:0.3f animations:^{
                                   someButton.center = newCenterCGPoint
                               }];

                              Если же под авто выравниванием вы понимаете Autolayout, то, к сожалению я не так плотно с ним работал. Но вот, нарыл гуглением:
                              — изменяются константы autolayout'а, после чего делается
                              [self.view layoutIfNeeded];
                    +2
                    Хорошая статья, от string typing надо избавляться любой ценой
                      –2
                      Мне кажется, что string typing — надуманная проблема. Objective-C — это динамический язык и превращать его в статический С++ — это неверный путь. Как по мне, нужно следовать «родным» шаблонам разработки. А от опечаток и прочих недостатков строк можно избавиться с помощью юнит-тестирования.
                        +1
                        Юнит-тестирование долно тестировать логику, а не опечатки.
                          +1
                          чаще всего при опечатках логика и не будет работать.
                          +1
                          Вы проверяете в unit-тестах что увас верно заргузился контроллер с ксибом?
                            +1
                            а вы пишете приложение и ни разу не запускаете хотя-бы в эмуляторе? Я думаю на этом этапе как раз опечатки в названиях ксибов и пропадут.
                              +3
                              Вы часто делаете рефакторинг? Вы часто изменяете reuseIdentifiers / storyboardID? Вы переименовуете имена пропертей? Вы плотно работаете с CoreData? Никто не спорит: Objective-C динамичен и прекрасен в своей гибкости. Но некоторые вещи я лучше доверю компилятору. Именно поэтому я не отключаю Static Analyser. Именно поэтому я буду использовать инструменты, позволяющие выявлять ошибки на стадии компиляции.
                              Я ценю свое время.
                                0
                                Рефакторинг делаю крайне редко при крайней необходимости. Если честно, в масштабах iOS приложений рефакторинг провожу только в чужом коде, в своем ни разу не было такого.

                                Вы плотно работаете с CoreData?

                                Плотнее некуда.

                                Именно поэтому я буду использовать инструменты, позволяющие выявлять ошибки на стадии компиляции.

                                Ваш выбор, но я еще думаю о тех людях, которые вероятно могут поддерживать мой код.

                                Я ценю свое время.

                                А нужно еще думать о времени других разработчиков, в том числе тех, которые потом будут разбираться в ваших «нестандартных» подходах.

                                Вы часто изменяете reuseIdentifiers / storyboardID? Вы переименовуете имена пропертей? Вы плотно работаете с CoreData?

                                Это надуманные проблемы, ошибки в этих примерах приводят к падению приложения при первом же обращения к «неизмененным» данным. Плюс инструменты рефакторинга переименовывают и iVar'ы (если их еще кто-то использует) и названия свойств и все места где они используются (так для примера о надуманности приведенных аргументов).
                                  +1
                                  Я что-то не совсем понимаю. Вы работаете с CoreData, но будете продолжать писать FetchRequest-ы оперируя строковыми константами, вместо того, чтобы использовать библиотеки, которые на порядки уменьшат количество кода (и соответственно ошибок), таких как ActiveRecord for Core Data. Вы предпочитаете не использовать инструменты автоматизирующие рутинные операции, а писать все руками? По поводу свойств — я говорил о KVO и KVC: рефакторинг в Xcode не настолько умен, чтобы переименовать и строковые константы вместе со свойствами.
                                  Наверное я вас неправильно понял.
                                  Все вышеописанное совсем не надуманные для меня проблемы. Это проблемы, с которыми я встречаюсь каждый день. И в своем, и в чужом коде. Я лишь описал один из способов решения этих (с моей точки зрения) проблем.
                                  Эти библиотеки и утилиты сэкономили мне кучу времени и нервов. И я буду рад если помогут и другим.
                                  Если для вас эти подходы сложны, неочевидны и нестандарты — не используйте их.
                                    0
                                    естественно, что нужно использовать и фреймворки и инструменты для облегчения труда.
                                    Но я лишь говорил, что те проблемы, о которых вы писали в статье — надуманные. Естественно и ваше и мое мнения — субьективны и каждый для себя сам выбирает путь. Для того комментарии и нужны, чтоб высказать альтернативную точку зрения, что я и сделал. И привел собственно аргументы.

                                    Вы работаете с CoreData, но будете продолжать писать FetchRequest-ы оперируя строковыми константами, вместо того, чтобы использовать библиотеки, которые на порядки уменьшат количество кода (и соответственно ошибок), таких как ActiveRecord for Core Data


                                    Библиотеки как правило увеличивают количество кода (я понимаю что вы имели ввиду, но просто озвучил другую сторону медали), и еще могут снизить производительность и увеличить количество ошибок. Любую библиотеку перед использованием нужно хорошо изучить, а не полагаться на чужую внимательность и компетентность.
                                    Как правило в 60 процентах я борюсь в ошибках и тормозах чужого кода, а не собственного, именно поэтому так и говорю.

                                    Рефакторинг в XCode работает не очень хорошо, но в AppCode все намного лучше.

                                    Я лишь пытаюсь сказать, что во всем должна быть мера и переносить шаблоны и подходы с других языков нужно осторожно и вдумчиво.
                                      +1
                                      AppCode умен, да. Но все равно, замена строкового литерала свойства ВЕЗДЕ где нужно — непростая задача.

                                      Если вы 60% времени боритесь с ошибками в зависимостях — значит что-то с процессом выбора библиотек не так.

                                      А по поводу переноса шаблонов из других языков — нельзя их бездумно переносить, если они лишают Вас каких-то возможностей динамичного рантайма ObjC. Что Вы теряете в описанных в статье случаях? Вместо @«someProperty» будет @keypath(object.someProperty)? Серьезно? Да в этом любой джуниор за полчаса разберется.
                                        0
                                        Если вы 60% времени боритесь с ошибками в зависимостях — значит что-то с процессом выбора библиотек не так.

                                        Вы опять переиначиваете. Я не 60 процентов времени борюсь с ошибками в зависимостях, а 60 процентов времени оптимизации провожу оптимизацию чужого кода а не собственного.
                                        Ну пока идеального ничего не встречал, те-же MagicalRecord и mogenerator входили в число этого кода.

                                        А по поводу переноса шаблонов из других языков — нельзя их бездумно переносить, если они лишают Вас каких-то возможностей динамичного рантайма ObjC.

                                        Ну возможностей динамического рантайма ни один шаблон избавить не может, так как приложение работает в этом рантайме.

                                        Что Вы теряете в описанных в статье случаях? Вместо @«someProperty» будет @keypath(object.someProperty)? Серьезно? Да в этом любой джуниор за полчаса разберется.

                                        Я не говорю о сложностях, я говорю о чюжеродности. Хранить указатели на Objective-C функции тоже можно, и разобраться в этом может любой джуниор, как вы выразились. Вопрос просто зачем, если есть блоки. А даже когда их небыло, то колл-бэки лучше было бы делать родными механизмами (центр уведомлений или шаблон делегирования например). Понятно что указатели бы работали быстрее и понятнее для любого С/С++ разработчика, но вопрос, зачем это делать, ради чего?

                                        Вот так и в вашем случае, я лично не вижу вообще никаких оснований и доказательств того, что эта статическая проверка экономит время.
                                          0
                                          т.н. Global blocks кстати как раз и являются обычными указателями на функцию. Это когда блок ничего из внешнего контекста не использует и аллоцируется статически при компиляции. Это я к тому что пример очень неудачный, где-то блоки могут работать также, как указатели на функцию. Там где нужно замыкание — в C++ тоже не обойдешься простым указателем.

                                          Если Вы действительно не видите преимуществ статических проверок — думаю нет смысла дальше что-то обсуждать.
                                            0
                                            И кстати даже сейчас некоторые вещи лучше делать делегатами, а не использовать везде блоки «потому что можем»
                                        0
                                        Ну видимо у человека бэкграунд из динамических языков, отсюда пренебрежение потенциальными compile-time проверками :(
                                          0
                                          неверные домыслы, мой бэкграунд: C/C++, и пренебрежения нет. О причинах я писал выше.
                                    0
                                    Я не утверждал обратного. Вы написали что эти ошибки можно выявить при помощи юнит-тестов.
                                      0
                                      про ксибы я не говорил ничего.
                                0
                                Большое спасибо!
                                  +1
                                  Это лучшее что я прочитал на хабре за месяц!

                                  Only users with full accounts can post comments. Log in, please.