Настройка существующих классов

Объекты должны были ясно определить задачи, такие как моделирование определенной информации, отображение визуального содержания или управление потоком информации. Как Вы уже видели, интерфейс класса определяет пути, которыми другие, как ожидают, будут взаимодействовать с объектом помочь ему выполнить те задачи.

Иногда, можно найти, что Вы хотите расширить существующий класс путем добавления поведения, которое полезно только в определенных ситуациях. Как пример, Вы могли бы найти, что Ваше приложение часто должно отображать строку символов в визуальном интерфейсе. Вместо того, чтобы создавать некоторый строковый рисованного объект для использования каждый раз, когда необходимо вывести на экран строку, она имела бы больше смысла, если бы было возможно дать NSString сам класс возможность нарисовать ее собственные символы на экране.

В ситуациях как это это не всегда целесообразно добавлять служебное поведение к исходному, основному интерфейсу класса. Рисующие возможности вряд ли будут необходимы большинство времен, в случае которых любой строковый объект используется в приложении, например, и NSString, Вы не можете изменить исходный интерфейс или реализацию, потому что это - класс платформы.

Кроме того, это не могло бы быть целесообразно разделять существующий класс на подклассы, потому что можно хотеть поведение получения, доступное не только к оригиналу NSString класс, но также и любые подклассы того класса, как NSMutableString. И, несмотря на то, что NSString доступно и на OS X и на iOS, код для прорисовки должен был бы отличаться для каждой платформы, таким образом, необходимо будет использовать различный подкласс на каждой платформе.

Вместо этого Objective C позволяет Вам добавлять свои собственные методы к существующим классам посредством расширений класса и категорий.

Категории добавляют методы к существующим классам

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

Синтаксис для объявления категории использует @interface ключевое слово, точно так же, как стандартное описание класса Objective C, но не указывает наследования от подкласса. Вместо этого это указывает имя категории в круглых скобках, как это:

@interface ClassName (CategoryName)
 
@end

Даже если у Вас нет исходного исходного кода реализации (такой что касается стандартных Сенсорных классов Какао или Какао), категория может быть объявлена для любого класса. Любые методы, которые Вы объявляете в категории, будут доступны всем экземплярам исходного класса, а также любым подклассам исходного класса. Во время выполнения нет никакого различия между методом, добавленным категорией и тем, реализованным исходным классом.

Рассмотрите XYZPerson класс из предыдущих глав, имеющий свойства для имени и фамилии лица. Если Вы пишете приложение ведения записей, можно найти, что часто необходимо отображать список людей фамилией, как это:

Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate

Вместо того, чтобы иметь необходимость записать код для генерации подходящего lastName, firstName представьте в виде строки каждый раз, когда Вы хотели вывести на экран его, Вы могли добавить категорию к XYZPerson класс, как это:

#import "XYZPerson.h"
 
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end

В этом примере, XYZPersonNameDisplayAdditions категория объявляет один дополнительный метод возвратить необходимую строку.

Категория обычно объявляется в отдельном заголовочном файле и реализуется в отдельном файле исходного кода. В случае XYZPerson, Вы могли бы объявить категорию в вызванном заголовочном файле XYZPerson+XYZPersonNameDisplayAdditions.h.

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

Реализация категории могла бы быть похожей на это:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
 
@implementation XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
    return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end

Как только Вы объявили категорию и реализовали методы, можно использовать те методы от любого экземпляра класса, как будто они были частью исходного интерфейса класса:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation SomeObject
- (void)someMethod {
    XYZPerson *person = [[XYZPerson alloc] initWithFirstName:@"John"
                                                    lastName:@"Doe"];
    XYZShoutingPerson *shoutingPerson =
                        [[XYZShoutingPerson alloc] initWithFirstName:@"Monica"
                                                            lastName:@"Robinson"];
 
    NSLog(@"The two people are %@ and %@",
         [person lastNameFirstNameString], [shoutingPerson lastNameFirstNameString]);
}
@end

А также просто добавив методы к существующим классам, можно также использовать категории для разделения реализации сложного класса через многократные файлы исходного кода. Если геометрические вычисления, цвета, и градиенты, и т.д., являются особенно сложными, Вы могли бы, например, поместить код для прорисовки для элемента настроенного пользовательского интерфейса в отдельном файле к остальной части реализации. Также Вы могли обеспечить различные реализации для методов категории, в зависимости от того, писали ли Вы приложение для OS X или iOS.

Категории могут использоваться для объявления или методов экземпляра или методов класса, но обычно не подходят для объявления дополнительных свойств. Это - допустимый синтаксис для включения объявления свойства в интерфейс категории, но не возможно объявить дополнительную переменную экземпляра в категории. Это означает, что компилятор не синтезирует переменной экземпляра, и при этом это не синтезирует методов доступа свойства. Можно записать собственные методы доступа в реализации категории, но Вы не будете в состоянии отслеживать значение для того свойства, если это не будет уже сохранено исходным классом.

Единственный способ добавить традиционное свойство — поддержанный новой переменной экземпляра — к существующему классу состоит в том, чтобы использовать расширение класса, как описано в Классе, Расширения Расширяют Внутреннюю Реализацию.

Избегите столкновений имени метода категории

Поскольку методы, объявленные в категории, добавляются к существующему классу, необходимо быть очень осторожны относительно имен методов.

Если имя метода, объявленного в категории, совпадает с методом в исходном классе или методом в другой категории на том же классе (или даже суперкласс), поведение не определено, относительно которого реализация метода используется во время выполнения. Если Вы будете использовать категории со своими собственными классами, но сможете заставить проблемы при использовании категорий добавлять методы к стандартным Сенсорным классам Какао или Какао, это, менее вероятно, будет проблемой.

Приложению, работающему с удаленным веб-сервисом, например, возможно, понадобился бы простой способ закодировать строку символов с помощью кодирования Base64. Это было бы целесообразно определять категорию на NSString добавить метод экземпляра возвратить Base64-закодированную версию строки, таким образом, Вы могли бы добавить вызванный удобный метод base64EncodedString.

Проблема возникает, если Вы соединяетесь с другой платформой, которая также, оказывается, определяет ее собственную категорию на NSString, включая его собственный вызванный метод base64EncodedString. Во время выполнения только одна из реализаций метода «победит» и будет добавлена к NSString, но какой не определен.

Другая проблема может возникнуть, если Вы добавляете удобные методы для Сенсорных классов Какао или Какао, тогда добавляющихся к исходным классам в более поздних выпусках. NSSortDescriptor класс, например, который описывает, как набор объектов должен быть упорядочен, всегда имел initWithKey:ascending: метод инициализации, но не предлагал соответствующий метод фабрики классов под ранним OS X и версиями iOS.

Условно, метод фабрики классов нужно вызвать sortDescriptorWithKey:ascending:, таким образом, Вы, возможно, приняли решение включить категорию NSSortDescriptor обеспечить этот метод для удобства. Это работало бы, как Вы будете ожидать под более старыми версиями OS X и iOS, но с выпуском версии 10.6 Mac OS X и iOS 4.0, a sortDescriptorWithKey:ascending: метод был добавлен к оригиналу NSSortDescriptor когда Ваше приложение было запущено на этих или более поздних платформах, класс, имея в виду Вы теперь закончили бы со столкновением именования.

Во избежание неопределенного поведения это - наиболее успешная практика для добавления префикса к именам методов в категориях на классах платформы, точно так же, как необходимо добавить префикс к именам собственных классов. Вы могли бы принять решение использовать те же три буквы, которые Вы используете для своих префиксов класса, но нижнего регистра для следования обычному соглашению для имен методов, затем подчеркивание, перед остальной частью имени метода. Для NSSortDescriptor пример, Ваша собственная категория могла бы быть похожей на это:

@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end

Это означает, что можно быть уверены, что метод будет использоваться во время выполнения. Неоднозначность удалена, потому что Ваш код теперь похож на это:

    NSSortDescriptor *descriptor =
               [NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];

Расширения класса расширяют внутреннюю реализацию

Расширение класса есть некоторое сходство к категории, но это может только быть добавлено к классу, для которого у Вас есть исходный код во время компиляции (класс компилируется в то же время, что и расширение класса). Методы, объявленные расширением класса, реализованы в @implementation блок для исходного класса, таким образом, Вы не можете, например, объявить расширение класса на классе платформы, таком как Сенсорный класс Какао или Какао как NSString.

Синтаксис для объявления расширения класса подобен синтаксису для категории и похож на это:

@interface ClassName ()
 
@end

Поскольку никакое имя не дано в круглых скобках, расширения класса часто упоминаются как анонимные категории.

В отличие от регулярных категорий, расширение класса может добавить свои собственные свойства и переменные экземпляра к классу. Если Вы объявляете свойство в расширении класса, как это:

@interface XYZPerson ()
@property NSObject *extraProperty;
@end

компилятор автоматически синтезирует соответствующие методы доступа, а также переменную экземпляра, в основной реализации класса.

Если Вы добавляете какие-либо методы в расширении класса, они должны быть реализованы в основной реализации для класса.

Также возможно использовать расширение класса для добавления пользовательских переменных экземпляра. Они объявляются внутренними фигурными скобками в интерфейсе расширения класса:

@interface XYZPerson () {
    id _someCustomInstanceVariable;
}
...
@end

Используйте расширения класса для сокрытия частной информации

Основной интерфейс для класса используется для определения способа, которым другие классы, как ожидают, будут взаимодействовать с ним. Другими словами, это - открытый интерфейс к классу.

Расширения класса часто используются для расширения открытого интерфейса с помощью дополнительных закрытых методов или свойств для использования в реализации самого класса. Распространено, например, определить свойство как readonly в интерфейсе, но как readwrite в расширении класса, объявленном выше реализации, чтобы внутренние методы класса могли изменить значение свойства непосредственно.

Как пример, XYZPerson класс мог бы добавить вызванное свойство uniqueIdentifier, разработанный для отслеживания информацию как Номер социального страхования в США.

Это обычно требует, чтобы большая сумма документов присвоила уникальный идентификатор частному лицу в реальном мире, таким образом, XYZPerson интерфейс класса мог бы объявить это свойство как readonly, и обеспечьте некоторый метод, запрашивающий, чтобы идентификатор был присвоен, как это:

@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end

Это означает, что это не возможно для uniqueIdentifier быть установленным непосредственно другим объектом. Если лицо уже не имеет один, запрос должен быть выполнен для присвоения идентификатора путем вызова assignUniqueIdentifier метод.

Для XYZPerson класс, чтобы быть в состоянии изменить свойство внутренне, это целесообразно повторно объявлять свойство в расширении класса, это определяется наверху файла реализации для класса:

@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
 
@implementation XYZPerson
...
@end

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

Путем объявления расширения класса в файле исходного кода для XYZPerson реализация, информация остается частной к XYZPerson класс. Если другой тип объектных попыток установить свойство, компилятор генерирует ошибку.

Рассмотрите другие альтернативы для настройки класса

Категории и расширения класса упрощают добавлять поведение непосредственно к существующему классу, но иногда это не наилучший вариант.

Одна из основных целей объектно-ориентированного программирования состоит в том, чтобы записать повторно используемый код, что означает, что классы должны быть допускающими повторное использование во множестве ситуаций, по мере возможности. Если Вы создаете класс представления для описания объекта, выводящего на экран информацию об экране, например, это - хорошая идея думать, мог ли бы класс быть применимым в многократных ситуациях.

Вместо решений жесткого кодирования о расположении или содержании, одна альтернатива должна эффективно использовать наследование и оставить те решения в методах, специально предназначенных, чтобы быть переопределенной подклассами. Несмотря на то, что это действительно делает относительно простым снова использовать класс, все еще необходимо создать новый подкласс каждый раз, когда Вы хотите использовать тот исходный класс.

Другая альтернатива для класса для использования объекта делегата. Любые решения, которые могли бы ограничить возможность многократного использования, могут быть делегированы к другому объекту, который оставляют принять те решения во время выполнения. Один типичный пример является стандартным классом табличного представления (NSTableView для OS X и UITableView для iOS). Для универсального табличного представления (объект, выводящий на экран информацию с помощью одного или более столбцов и строк), чтобы быть полезной, она оставляет решения о своем содержании, которое будет решено другим объектом во время выполнения. Делегация покрыта подробно в следующей главе, Работающей с Протоколами.

Взаимодействуйте непосредственно со временем выполнения Objective C

Objective C предлагает свое динамическое поведение через систему времени выполнения Objective C.

Много решений, такой как, какие методы вызывают, когда сообщения отправляются, не приняты во время компиляции, но вместо этого определяются, когда запущено приложение. Objective C является больше, чем просто язык, компилирующийся вниз в машинный код. Вместо этого это требует, чтобы система во время выполнения на месте выполнила тот код.

Возможно взаимодействовать непосредственно с этой системой во время выполнения, такой как путем добавления ассоциативных ссылок на объект. В отличие от расширений класса, связанные ссылки не влияют на исходное объявление класса и реализацию, что означает, что можно использовать их с классами платформы, для которых у Вас нет доступа к коду первоисточника.

Ассоциативные ссылки один объект с другим, похожим способом к свойству или переменной экземпляра. Для получения дополнительной информации посмотрите Ассоциативные Ссылки. Для узнавания больше о Времени выполнения Objective C в целом см. Руководство по программированию Времени выполнения Objective C.

Упражнения

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

  2. Добавьте категорию к NSString для добавления метода для рисования прописной версии строки в данной точке, вызывающей через к одному из существующих NSStringDrawing методы категории для выполнения фактического получения. Эти методы документируются в Нсстринга УИКИТА Аддайшнса Референса для iOS и Нсстринга Аппликэйшна Кита Аддайшнса Референса для OS X.

  3. Добавьте два readonly свойства к оригиналу XYZPerson реализация класса для представления высоты и веса лица, вместе с методами к measureWeight и measureHeight.

    Используйте расширение класса, чтобы повторно объявить свойства как readwrite, и реализуйте методы для установки свойств в подходящие значения.