GrabDuck

Организация Objective C класса

:

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

Кому-то статьи про Obj C могут показаться архаизмом, но пока мы не планируем повсеместный переезд на Swift. Это скорее плавное замещение в новых проектах. Все еще остается огромная кодовая база на Objective C которую необходимо поддерживать.
К тому же, на Swift еще не накоплено достаточно опыта в больших проектах.

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

В заголовочном файле 279 строк, в файле реализации 2948 строк.
Привет, ⌘F, я не скучал.

Поддержка такого огромного файла может принести много сложностей.
Я стараюсь держать все .m файлы до 100 строк. Максимум 150.
Это помогает быстро находить нужную логику без необходимости держать в голове карту.


После подключения библиотек из SDK, объявляется протокол и класс:
@protocol WYPopoverControllerDelegate;
@class WYPopoverTheme;

Это отличное решение чтобы не добавлять хедеры для класса WYPopoverTheme.
Не всем классам использующим WYPopoverController нужно менять тему.

Но сам класс WYPopoverTheme объявлен в этом же файле.

На этот счет есть простое правило:
один .h файл — один

@interface

один .m файл — один
@implementation

Обратите внимание на строчку "////…" перед интерфейсом.
Для чего она? Очевидно чтобы не путаться где закончился один интерфейс, а где начинается другой.

Ведь так просто добавить свойство или метод не туда.

У каждого программиста порой возникает чувство что файл стал слишком большой и в нем сложно разобраться. Симптомами этой болезни можно назвать частое использование выпадающего списка методов или поиска по файлу (⌘F).
Другой симптом это желание вставить #pragma mark.

Я уже переболел, вот вам лекарство.

Никогда не используйте #pragma mark или строчки вида "////...", это ничего не меняет.Только вместо метода вы станете сначала искать метку.

Первое что нужно сделать это вынести классы в отдельные файлы. Потом объявить через @class те что необходимы.
#import минимизировать чтобы избежать глобальной перекомпиляции при каждом изменении хедеров.

Константы

#define WY_POPOVER_DEFAULT_ANIMATION_DURATION    .25f
#define WY_POPOVER_MIN_SIZE

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

К тому же, константы нужно определять так через const: const CGFloat kAbc = 0.25;.
Если нужно объявить константу то так: FOUNDATION_EXPORT const CGFloat kAbc;

С использованием NS_OPTIONS согласен.
Нам нужно знать этот тип чтобы вызывать контроллер. И контроллер должен предоставлять эти знания. Возможно, стоит вынести их в отдельный хедер. Ну да ладно, это уже придирки.

@interface WYPopoverController"


Поддерживает UIAppearanceContainer, это круто. Сможем легко настроить его в одном месте. Плюсик. Подробней тут.

Свойства


Не понимаю почему по какому принципу составлен список свойств. Похоже что они были добавлены в хедер по мере реализации.

Я бы вынес делегат к теме, а остальные настройки сгруппировал по категориям либо просто по алфавиту (У меня настроено на ⌃⌘A).

@property (nonatomic, assign) BOOL                              		   dismissOnPassthroughViewTap;
@property (nonatomic, assign) BOOL                              		   dismissOnTap;
@property (nonatomic, assign) BOOL                              		   implicitAnimationsDisabled;
@property (nonatomic, assign) BOOL                              		   wantsDefaultContentAppearance;
@property (nonatomic, assign) CGSize                            		   popoverContentSize;
@property (nonatomic, assign) float                             		           animationDuration;
@property (nonatomic, assign) UIEdgeInsets                      		   popoverLayoutMargins;
@property (nonatomic, copy) NSArray                            	          *passthroughViews;
@property (nonatomic, readonly, getter=isPopoverVisible) BOOL   popoverVisible;

@property (nonatomic, strong) WYPopoverTheme                   *theme;
@property (nonatomic, strong, readonly) UIViewController       *contentViewController;
@property (nonatomic, weak) id <WYPopoverControllerDelegate> delegate;

@property (nonatomic, copy) void (^dismissCompletionBlock)(WYPopoverController *dimissedController);

Кстати, assign писать необязательно:
@property (nonatomic) BOOL dismissOnPassthroughViewTap;

Хорошо что геттер для popoverVisible определен как isPopoverVisible, эта проперти отвечает на вопрос о состоянии объекта поэтому начинается с is:
@property (nonatomic, readonly, getter=isPopoverVisible) BOOL   popoverVisible;

Но для dismissOnTap и других тоже нужно сделать кастомные геттеры.
@property (nonatomic, assign) BOOL dismissOnTap;

Это свойство отвечает на вопрос что будет с объектом при определенных событиях.
А конкретно что нужно сделать по тапу.
Для таких вариантов я определяю геттер с префиксом will (поправьте если неправильно использую английский):
@property (nonatomic, getter=willDismissOnTap) BOOL dismissOnTap;

Дополнительная ответственность

+ (void)setDefaultTheme:(WYPopoverTheme *)theme;
+ (WYPopoverTheme *)defaultTheme;

Если есть объект темы, то он сам должен контролировать свое состояние по умолчанию.
Зачем нагржуать дополнительной ответственностью наш класс?
Переношу в WYPopoverTheme.

Но согласен с тем что методы класса должны быть в начале списка методов.

Инициализация

- (id)initWithContentViewController:(UIViewController *)viewController;

Предложенный метод вызывает [self init] значит можно быть уверенным что объект инициализирует все необходимые данные перед использованием.
Но чтобы узнать об этом мне пришлось смотреть исходники.

А вот если вызвать просто init, не будет возможности установить contentViewController. Т.к. он readonly.
Значит нужно пометить int как метод который не нужно использовать, а initWithContentViewController как желаемый метод инициализации.

Иначе можно долго тупить сделав alloc init.

Есть такой способ.

Используйте NS_DESIGNATED_INITIALIZER чтобы указать клиенту желаемый метод инициализации.
Можно пометить таким образом несколько методов.

Можно даже пометить метод суперкласса:
Например так

- (id)init NS_DESIGNATED_INITIALIZER; 

Тогда при вызове неправильного инициализатора Xcode покажет ворнинг (у меня ошибку).

Метод возващает id.
Лучше использовать instancetype. Тогда метод всегда будет возвращать экземпляр класса у которого он был вызван. Даже если мы отнаследуемся от него.

Вспомогательные методы

// theme

- (void)beginThemeUpdates;
(void)endThemeUpdates;

Обратили внимание на комментарий? Это опять симптом- заголовочный файл слишком большой.
Решим это проблему дальше.

Секция "// Present popover from classic views methods"
Это уже настоящая болезнь. Файл стал слишком большим- нужно делить хедер.

Для этого воспользуемся категорией.
Судя по .h файлам от Apple (например UIView.h) ОНИ тоже делают так.

Возьмем методы этой секции до "// Present popover from bar button items methods" и создадим категорию PresentationFromView.

⌘N -> iOS -> Source -> Objective-C File

Перенесем реализацию этих методов в файл WYPopoverController+PresentationFromView.m.
Скопируем интерфейс из WYPopoverController+PresentationFromView.h в WYPopoverController.h после интерфейса WYPopoverController и перенесем объяления методов в него.

Файл WYPopoverController+PresentationFromView.h удаляем.

Вообще, проще иметь специальный темплейт для таких категорий где не нужен файл .h.

Выйдет так:

@interface WYPopoverController (PresentationFromView)

- (void)presentPopoverFromRect:(CGRect)rect
                        inView:(UIView *)view
      permittedArrowDirections:(WYPopoverArrowDirection)arrowDirections
                      animated:(BOOL)animated;
………………………………………………………………………
@end

Эти методы публичные поэтому их объявления в WYPopoverController.h.
Не будем показывать подштанники нарушать инкапсуляцию и приватные методы будем объявлять в расширении (extension) Private. Оно создается там же где категория.

Все .m файлы должны импортировать именно его:

#import «WYPopoverController_Private.h»

В нем же должны быть импортированы файлы необходимые для работы разных категорий.
Файлы необходимые только для одной категории нужно импортировать в .m файле конкретной категории.

Там же объявляем все приватные свойства и методы.

Если нам нужен заголовочный файл который будут использовать наследники, его можно назвать Protected.

Так же поступаем со всеми методами которые можно отнести к одной логической группе.

Если файл .m стал слишком большой- выделяйте еще одну категорию.
Но не стоит перебарщивать: 4-5 категорий это максимум, иначе можно опять начать путаться.

Если вам нужно больше, возможно, вы пытаетесь наделить класс слишком большим количеством обязанностей ( God object). В таком случае, вынесите эти обязанности в отдельный класс. Если уж сильно нужно сделать такой супер класс, сделайте фасад.

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

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

Категория может быть хорошим местом чтобы вынести методы протокола.
Делаем так:

@interface WYPopoverController (TableView) <UITableViewDataSource, UITableViewDelegate>

Или вынести IBAction в категорию UserActions.
Не так давно Xcode научился определять IBAction в файлах реализации.
Но нужно все равно объявлять их в заголовке, тогда будет легко понять где находятся их реализации.

Иногда бывает необходимо в одной категории определить публичные и приватные методы.

Тогда в WYPopoverController.h мы добавим категорию TableView.
А в WYPopoverController_Private.h категорию TableView_Private, а файл реализации будет один- WYPopoverController+TableView.m.

Класс в проекте будет выглядеть так:

В комментариях к прошлой статье greenkaktus подсказал описать работу с "#pragma, #warning, //FIXME".

Я использую #pragma только когда мне нужно отключить ворнинги в определенных местах.
Использую очень аккуратно, только когда уверен что это единственный верный способ.
#pragma mark не использую никогда. Своих программистов бью за это линейкой по рукам.

#warning не использую, они у меня трактуются как ошибки. Некоторый подробности есть в прошлой статье.

"//TODO" использую как подсказки на будущее. Например, если вижу потенциально узкое место которое может потребовать оптимизации при росте нагрузок.
"//FIXME" использую в местах которые нужно исправить, но чуть позже.

У меня есть еще много замечаний к качеству этого кода, но эта тема заслуживает отдельной статьи.

В следущей статье я планирую описать устройство класса который я использую для работы с апи.
Я постарался спроектировать его максимально гибким и с максимальной степенью автоматизации.
Так чтобы добавление нового вызова метода апи и парсинга ответа можно было сделать в несколько строк.

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