Вы когда-нибудь писали адаптеры для Keychain или NSUserDefaults? Они полностью состоят из однотипных сеттеров и геттеров. Предлагаю написать логику один раз, предоставив остальное рантайму. За реализацией прошу под кат.


клавиатура с кнопками copy и paste


Привет. С вами вновь vdugnist из FunCorp. Недавно, при добавлении нового поля в адаптер Keychain, я поймал ошибку при копировании кода из соседнего метода.


Как выглядела реализация до этого:


- (Credentials *)credentials {
    // implementation details
}

- (void)setCredentials:(Credentials *)credentials {
    // implementation details
}

- (NSDate *)firstLaunchDate {
    // implementation details
}

- (void)setFirstLaunchDate:(NSDate *)date {
    // implementation details
}

Код попросту копировался из ближайшего метода и в нём менялись константы. Понятно, что при таком подходе можно легко допустить ошибку. После небольшого рефакторинга получилось вынести всю реализацию логики класса в два метода:


- (Credentials *)credentials {
    return [self objectFromKeychainForKey:@"credentials"];
}

- (void)setCredentials:(Credentials *)credentials {
    [self setObject:credentials toKeychainForKey:@"credentials"];
}

- (NSDate *)firstLaunchDate {
    return [self objectFromKeychainForKey:@"firstLaunchDate"];
}

- (void)setFirstLaunchDate:(NSDate *)firstLaunchDate {
    [self setObject:firstLaunchDate toKeychainForKey:@"firstLaunchDate"];
}

- (void)setObject:(id)obj toKeychainForKey:(NSString *)key {
    // implementation details
}

- (id)objectFromKeychainForKey:(NSString *)key {
    // implementation details
}

Уже лучше. Но остаётся ещё две проблемы:


  • всё ещё можно опечататься в строковой константе, которую мы передаём в метод;
  • весь класс будет состоять из методов с одинаковой реализацией, отличающихся только аргументом у вызываемого метода.

И тут нам на помощь приходит рантайм. В Objective-C при добавлении @property в интерфейс класса автоматически генерируется сеттер, геттер и ivar. В стандартной реализации сеттера значение записывается в ivar, а для геттера — читается из ivar. Для того чтобы эти методы не генерировались, в реализации класса вам нужно написать dynamic <имя поля>. Тогда при обращению к полю мы получим исключение unrecognized selector sent to instance.


Перед отправкой исключения у класса будет вызван метод +(BOOL)resolveInstanceMethod:(SEL)sel в случае instance property или +(BOOL)resolveClassMethod:(SEL)sel в случае class property.
В них можно добавить реализацию метода по селектору с помощью class_addMethod и вернуть YES, если всё прошло гладко. После этого для текущего и последующих вызовов будет вызвана реализация вновь добавленного метода.


Для добавления метода в рантайме вам понадобится указатель на класс, к которому будет добавлен метод, селектор, блок с реализацией и сигнатура метода. Документацию по последнему аргументу можно почитать вот тут.


Я сразу решил выносить решение моей проблемы в библиотеку, поэтому в примере обработаны и class property, и instance property. В примере используются вспомогательные функции, реализацию можно посмотреть тут.


+ (BOOL)resolveClassMethod:(SEL)sel {
    return [self resolveMethodFor:object_getClass(self) selector:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return [self resolveMethodFor:self selector:sel];
}

+ (BOOL)resolveMethodFor:(id)target selector:(SEL)sel {
    if (!sel_isGetterOrSetter(target, sel)) {
        return NO;
    }

    objc_property_t property = propertyForSelector(target, sel);

    if (sel_isSetter(target, sel)) {
        SEL getterSel = sel_getterFromSetter(sel);
        dvPropertySetterBlock setterBlock = [self setterBlockForTarget:target getterSelector:getterSel];
        IMP blockImplementation = imp_implementationWithBlock(setterBlock);
        char *methodTypes = copySetterMethodTypesForProperty(property);
        assert(class_addMethod(target, sel, blockImplementation, methodTypes));
        free(methodTypes);
    }
    else {
        dvPropertyGetterBlock getterBlock = [self getterBlockForTarget:target getterSelector:sel];
        IMP blockImplementation = imp_implementationWithBlock(getterBlock);
        char *methodTypes = copyGetterMethodTypesForProperty(property);
        assert(class_addMethod(target, sel, blockImplementation, methodTypes));
        free(methodTypes);
    }

    return YES;
}

+ (dvPropertySetterBlock)setterBlockForTarget:(id)target getterSelector:(SEL)getterSelector {
    @throw @"Override this method in subclass";
}

+ (dvPropertyGetterBlock)getterBlockForTarget:(id)target getterSelector:(SEL)getterSelector {
    @throw @"Override this method in subclass";
}

В наследниках достаточно переопределить два метода (блок геттера и блок сеттера), добавить @property в интерфейс и dynamic в реализацию. Вот, например, реализация адаптера к NSUserDefaults:


+ (dvPropertySetterBlock)setterBlockForTarget:(id)target getterSelector:(SEL)getterSelector {
    return ^(id blockSelf, id value) {
        [[NSUserDefaults standardUserDefaults] setObject:value forKey:NSStringFromSelector(getterSelector)];
    };
}

+ (dvPropertyGetterBlock)getterBlockForTarget:(id)target getterSelector:(SEL)getterSelector {
    return ^id(id blockSelf) {
        return [[NSUserDefaults standardUserDefaults] objectForKey:NSStringFromSelector(getterSelector)];
    };
}

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

Комментарии (8)


  1. Krypt
    30.05.2018 10:49

    Я сейчас предложу еретическое, по общему мнению, решение, но препроцессор в ObjC делает тоже самое


    1. Krypt
      30.05.2018 11:06

      В частности, вы можете заменить +(BOOL)resolveMethodFor:(id)target selector:(SEL)sel на вызов препроцессорной директивы.
      Плюсы такого решения:


      • нет оверхеда на использование рантайма
      • директива может быть составлена так, чтобы ошибки в именах методов проверялись во время компиляции


      1. vdugnist Автор
        30.05.2018 11:29

        Вы предлагаете написать #define generate_accessor(property_name) ... и добавлять его для каждого поля?


        Решение рабочее, но:


        • нужно будет проделать чуть больше действий для добавления нового поля
        • подсветка синтаксиса при составлении и редактировании макросов осутствует

        Оверхедом при использовании рантайма можно пренебречь, так как resolveMethodFor:selector: вызывается только при первом обращении к полю. Ошибки в именах методов в решении с рантаймом не возникают так как селектор передаётся в аргументе.


        1. Krypt
          30.05.2018 16:58

          Не сильно больше: вы можете определить что-нибудь типа


          #define PROPERTY_DEF(type, name) <...>
          #define PROPERTY_IMP(type, name) <...>

          И использовать как


          @interface Storage : NSObject 
          
          PROPERTY_DEF((NSString*), prop1)
          PROPERTY_DEF((NSString*), prop2)
          
          @end

          @implementation Storage 
          
          PROPERTY_IMP((NSString*), prop1)
          PROPERTY_IMP((NSString*), prop2)
          
          @end

          В качестве обхода проблемы с подсветкой синтаксиса я обычно реализовывал один экземпляр свойства "в коде", а потом переделывал его в препроцессорную директиву. Так же у XCode есть инструмент для просмотра результата работы препроцессора — там подсветка синтаксиса работает корректно.


          1. vdugnist Автор
            30.05.2018 19:32

            В реализации PROPERTY_IMP придётся на макросах приводить propertyName к setPropertyName, что не очень приятно. Зато в вашем решении проще поддержать скалярные типы.


            А зачем макрос PROPERTY_DEF? Разве в нём не будет @property (attributes) type name?


            1. Krypt
              30.05.2018 19:57

              Да, по поводу PROPERTY_IMP вы правы, в С'шном препроцессоре нет нативного способа сделать upper case. Когда я использовал что-то подобное, я передавал два параметра для имени, PROPERTY_IMP(type, camelName, CapsName). Без PROPERTY_DEF в данном случае можно обойтись, хотя он тут скорее для однообразия.


  1. iCpu
    30.05.2018 11:15
    -3

    Извините за грубость и за несанкционированное использование машины времени, но разве IDE в ваше время (Кстати, который год?) не способно автоматически генерировать такой тривиальный код из, ну, не знаю, списка полей класса?


  1. omaksim
    30.05.2018 16:49

    Вы когда-нибудь писали адаптеры для Keychain или NSUserDefaults?

    Предлагаю это не делать вообще…