В предыдущей статье я затронул тему структуры проекта. На мой взгляд, это первый шаг с которого начинается красивый код.
Второй шаг это правильная организация файлов самого класса.
Кому-то статьи про Obj C могут показаться архаизмом, но пока мы не планируем повсеместный переезд на Swift. Это скорее плавное замещение в новых проектах. Все еще остается огромная кодовая база на Objective C которую необходимо поддерживать.
К тому же, на Swift еще не накоплено достаточно опыта в больших проектах.
В качестве антипримера предлагаю рассмотреть WYPopoverController.
Представим, что он пришел к нам не из пода, а это наш собственный класс написанный в эпоху буйной юности.
В заголовочном файле 279 строк, в файле реализации 2948 строк.
Привет, ?F, я не скучал.
Поддержка такого огромного файла может принести много сложностей.
Я стараюсь держать все .m файлы до 100 строк. Максимум 150.
Это помогает быстро находить нужную логику без необходимости держать в голове карту.
После подключения библиотек из SDK, объявляется протокол и класс:
Это отличное решение чтобы не добавлять хедеры для класса WYPopoverTheme.
Не всем классам использующим WYPopoverController нужно менять тему.
Но сам класс WYPopoverTheme объявлен в этом же файле.
На этот счет есть простое правило:
один .h файл — один
один .m файл — один
Обратите внимание на строчку "////…" перед интерфейсом.
Для чего она? Очевидно чтобы не путаться где закончился один интерфейс, а где начинается другой.
Ведь так просто добавить свойство или метод не туда.
У каждого программиста порой возникает чувство что файл стал слишком большой и в нем сложно разобраться. Симптомами этой болезни можно назвать частое использование выпадающего списка методов или поиска по файлу (?F).
Другой симптом это желание вставить #pragma mark.
Я уже переболел, вот вам лекарство.
Никогда не используйте #pragma mark или строчки вида "////...", это ничего не меняет.Только вместо метода вы станете сначала искать метку.
Первое что нужно сделать это вынести классы в отдельные файлы. Потом объявить через
Дефайны нужно убрать в приватный файл заголовка. Это значения по умолчанию.
Если я захочу их узнать, то зайду в реализацию.
К тому же, константы нужно определять так через const:
Если нужно объявить константу то так:
С использованием NS_OPTIONS согласен.
Нам нужно знать этот тип чтобы вызывать контроллер. И контроллер должен предоставлять эти знания. Возможно, стоит вынести их в отдельный хедер. Ну да ладно, это уже придирки.
Поддерживает UIAppearanceContainer, это круто. Сможем легко настроить его в одном месте. Плюсик. Подробней тут.
Не понимаю почему по какому принципу составлен список свойств. Похоже что они были добавлены в хедер по мере реализации.
Я бы вынес делегат к теме, а остальные настройки сгруппировал по категориям либо просто по алфавиту (У меня настроено на ??A).
Кстати, assign писать необязательно:
Хорошо что геттер для popoverVisible определен как isPopoverVisible, эта проперти отвечает на вопрос о состоянии объекта поэтому начинается с is:
Но для dismissOnTap и других тоже нужно сделать кастомные геттеры.
Это свойство отвечает на вопрос что будет с объектом при определенных событиях.
А конкретно что нужно сделать по тапу.
Для таких вариантов я определяю геттер с префиксом will (поправьте если неправильно использую английский):
Если есть объект темы, то он сам должен контролировать свое состояние по умолчанию.
Зачем нагржуать дополнительной ответственностью наш класс?
Переношу в WYPopoverTheme.
Но согласен с тем что методы класса должны быть в начале списка методов.
Предложенный метод вызывает [self init] значит можно быть уверенным что объект инициализирует все необходимые данные перед использованием.
Но чтобы узнать об этом мне пришлось смотреть исходники.
А вот если вызвать просто init, не будет возможности установить contentViewController. Т.к. он readonly.
Значит нужно пометить int как метод который не нужно использовать, а initWithContentViewController как желаемый метод инициализации.
Иначе можно долго тупить сделав alloc init.
Есть такой способ.
Используйте NS_DESIGNATED_INITIALIZER чтобы указать клиенту желаемый метод инициализации.
Можно пометить таким образом несколько методов.
Можно даже пометить метод суперкласса:
Например так
Тогда при вызове неправильного инициализатора Xcode покажет ворнинг (у меня ошибку).
Метод возващает id.
Лучше использовать instancetype. Тогда метод всегда будет возвращать экземпляр класса у которого он был вызван. Даже если мы отнаследуемся от него.
Обратили внимание на комментарий? Это опять симптом- заголовочный файл слишком большой.
Решим это проблему дальше.
Секция "// 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.
Выйдет так:
Эти методы публичные поэтому их объявления в WYPopoverController.h.
Не будемпоказывать подштанники нарушать инкапсуляцию и приватные методы будем объявлять в расширении (extension) Private. Оно создается там же где категория.
Все .m файлы должны импортировать именно его:
#import «WYPopoverController_Private.h»
В нем же должны быть импортированы файлы необходимые для работы разных категорий.
Файлы необходимые только для одной категории нужно импортировать в .m файле конкретной категории.
Там же объявляем все приватные свойства и методы.
Если нам нужен заголовочный файл который будут использовать наследники, его можно назвать Protected.
Так же поступаем со всеми методами которые можно отнести к одной логической группе.
Если файл .m стал слишком большой- выделяйте еще одну категорию.
Но не стоит перебарщивать: 4-5 категорий это максимум, иначе можно опять начать путаться.
Если вам нужно больше, возможно, вы пытаетесь наделить класс слишком большим количеством обязанностей (God object). В таком случае, вынесите эти обязанности в отдельный класс. Если уж сильно нужно сделать такой супер класс, сделайте фасад.
Лучше, конечно, проектировать классы до реализации, но не всегда получается.
Категория отлично помогают изолировать функционал. Его потом можно будет без большой боли вынести в отдельный класс.
Если вы решили делить класс на несколько отдельных классов, избегайте большого количества вызовов между двумя классами.
Иначе будет высокая связность кода и не будет никакого смысла в разделении. Объект должен быть скорее черным ящиком у которого вызывается один метод, а он после обработки данных вызывает один метод в обратную сторону. Никаких швейцарских ножей.
Категория может быть хорошим местом чтобы вынести методы протокола.
Делаем так:
Или вынести 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" использую в местах которые нужно исправить, но чуть позже.
У меня есть еще много замечаний к качеству этого кода, но эта тема заслуживает отдельной статьи.
В следущей статье я планирую описать устройство класса который я использую для работы с апи.
Я постарался спроектировать его максимально гибким и с максимальной степенью автоматизации.
Так чтобы добавление нового вызова метода апи и парсинга ответа можно было сделать в несколько строк.
Любые предложения и замечания приветствуются.
Спасибо что дочитали до конца, всем добра.
Второй шаг это правильная организация файлов самого класса.
Кому-то статьи про Obj C могут показаться архаизмом, но пока мы не планируем повсеместный переезд на Swift. Это скорее плавное замещение в новых проектах. Все еще остается огромная кодовая база на Objective C которую необходимо поддерживать.
К тому же, на Swift еще не накоплено достаточно опыта в больших проектах.
В качестве антипримера предлагаю рассмотреть WYPopoverController.
Представим, что он пришел к нам не из пода, а это наш собственный класс написанный в эпоху буйной юности.
В заголовочном файле 279 строк, в файле реализации 2948 строк.
Привет, ?F, я не скучал.
Поддержка такого огромного файла может принести много сложностей.
Я стараюсь держать все .m файлы до 100 строк. Максимум 150.
Это помогает быстро находить нужную логику без необходимости держать в голове карту.
Заголовочный файл WYPopoverController.h
После подключения библиотек из 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"
@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.
Не будем
Все .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" использую в местах которые нужно исправить, но чуть позже.
У меня есть еще много замечаний к качеству этого кода, но эта тема заслуживает отдельной статьи.
В следущей статье я планирую описать устройство класса который я использую для работы с апи.
Я постарался спроектировать его максимально гибким и с максимальной степенью автоматизации.
Так чтобы добавление нового вызова метода апи и парсинга ответа можно было сделать в несколько строк.
Любые предложения и замечания приветствуются.
Спасибо что дочитали до конца, всем добра.
IgorFedorchuk
Хотел бы посмотреть как организовываете контроллеры. Сильно маленькие классы — это уже другая крайность. Я стараюсь, чтобы в классе было не больше 600 строк. А #pragma mark — полезная вещь.
NikolayJuly
При классах в 100 строк — класс делает ничего. Большая группа классов, которые делают ничего и только друг другу передают вызовы. Для меня 450 — пока все в норме, 600 — надо рефакторить, 1000 — блин, кажется я что то пропустил… Так что в среднем классы по 400 — 500 строк и все отлично.
Ну и да #pragma mark отлично разделяет методы в списке методов в классе. Это который выпадающий список в пути файла под Tool Bar'ом
Holms
Мне бы ваши проблемы.
На новой работе приходиться работать с файлами где по 70тысяч строк.
NikolayJuly
Сочувствую. Правильный подход — пометить файл как Legacy… и постепенно пилить новый функционал. Потому то рефакторить такое… и не внести багов… нереально.
Holms
так и поступаем, по крайней мере стараюсь толкать такой подход
Goodkat
А теперь представьте, что эти 70000 строк — на коболе, а вместо IDE — окно терминала 20х80 символов :)
alkozin Автор
Вы меня не правильно поняли, я говорю о .m файлах, не о том что вся реализация класса должна быть 100 строк. Четыре-пять категорий по 100-150 строк, итого 400-750 строк реализации для одного класса. Разница в том чтобы вместо #pragma mark использовать категории. Опять же, посмотрите в заголовки файлов из SDK: в них много категорий.
greenkaktus
Мне #pragma mark – нравится тем, что помогает при последующей автогенерации документации по коду. + дополнительно помогают при навигации через строку пути доступа xcode (но это уже вкусовщина)
#warning хороши для использования в командной работе при code review (как и //FIXME).
alkozin Автор
Попробуйте разбить реализацию на несколько файлов. Тогда в выпадающем списке будет что-то около пяти методов.
Про ворнинги писал раньше. Считаю что в проекте не должно быть ворнингов. Плюс в наших проектах ворнинги всегда трактуются как ошибки.
Makaveli
а вам не кажется, что разбив на файлы вы будете искать глазами не методы и метки, а файлы?
alkozin Автор
Я пользуюсь этими правилами уже около трех лет. Пока это максимально удобный вариант из всех что я пробовал.
Опять же, для меня и моей команды.
Попробуйте.
greenkaktus
Ну так когда код ревью проведен, появляются ворнинги: человек сразу смотрит и все видит что сделал не так, м?
alkozin Автор
Интересная идея. Думаю, даже попробую.
Сейчас я делаю ревью так:
1) Новая ветка
2) Мои комментарии в формате //TODO
3) Звонок с программистом
4) Его поправки в ветке
5) Я смотрю и мерджу ветку в дев
Ворнинги будет проще увидеть и не забыть поправить.
Makaveli
Не согласен с тем, что это болезнь. Это смотря кому как удобнее добраться до нужного метода. И скидывать со счетов #pragra mark я бы тоже не стал. По-моему это очень удобно — разделять методы протокола
Хотя бы визуальное разделение методов на такие вот блоки в списке это уже удобно, по-моему. Лично мне помогает гораздо быстрее найти нужный метод, чем в классах, где такой организации нет.
Ну и если уж про организацию говорить, то, по-моему, стоит упомянуть о документировании кода.
alkozin Автор
Посмотрите мои комментарии выше. Но это только мое мнение- попробуйте, вдруг понравится )
Документирование заслуживает отдельной статьи. Не стал затрагивать эту тему намеренно.
AnthonyBY
спасибо за статьи, пожалуйста продолжайте
alkozin Автор
Пожалуйста! Буду рад предложениям по темам статей.
kostyl
Дефайны это зло, надо писать типизированные константы
mChief
Как вы после такой разбивки на категории выполняете навигацию по файлам? Пользуетесь ли вы AppCode?
alkozin Автор
Когда работаю с одной фичей открыты только нужные папки с классами. Иногда пользуюсь фильтрами внизу панели дерева проекта.
Если нужно открыть класс использую ??O, потом делаю ??J. Открывается папка с классом. Вынос логики в категории это как добавление еще одного уровня вместо поиска по одному уровню. AppCode не использую, привык к Xcode. Там есть похожая фича?
mChief
>>Вынос логики в категории это как добавление еще одного уровня вместо поиска по одному уровню.
Вот это меня и смутило. Поиск нужного метода несколько замедляется, но возможно это не так и критично.
>>Там есть похожая фича?
Если вы о навигации, то скорее нет. В AppCode тоже можно перейти на нужный файл (??O) и найти его в дереве проекта (?F1, 1), но получается еще больше нажатий. Зато там есть удобный(и быстро работающий) рефакторинг и возможность переносить методы в категорию.