Зачем ViewController’ам пустые интерфейсы?

Заметка написана по мотивам пары крупномасштабных холиваров, имевших место в последние несколько недель в Rambler&Co. Именно из-за такого происхождения и был выбран несколько шизофренический стиль повествования.

vc
Как должен выглядеть интерфейс у ViewController’а?
@interface PrettyViewController
@end

А что с его outlet’ами и IBAction’ами?
Мы их, конечно, выносим в extension и держим в .m-файле.

А почему не в интерфейсе?
Эм… Во-первых, это говнокод, все знают. Во-вторых — чтобы никто другой не мог воспользоваться приватными свойствами и методами контроллера. Ведь это же отвратительно и небезопасно — любой человек может спокойно взять и установить свой outlet вместо исходного, или вообще сбросить его в nil.

В каких случаях работа с ViewController’ом ведется напрямую?
Ну как же — во-первых, при создании его вручную, из кода. Не очевидно, что свойства из его интерфейса — всякие там textView, проставятся из xib’а автоматически, а не потребуют явного создания.
Во-вторых — при наследовании контроллеров.

Вот тут немного притормозим. Наследование контроллеров? Зачем? Нет, не будем трогать эту тему — и отложим ее на следующий раз.
Ладно, забыли. Но что насчет первого случая?

Что мы делаем со свойствами, которые должны быть доступны только для чтения?
Выставляем им модификатор readonly. Ага, а в extension’е — readwrite объявление, и таким образом магия сумеет соединить его с элементом в xib! Неплохо, но остается открытым вопрос с IBAction’ами…

Мы еще и первый не закрыли, просто нашли решение поставленной проблемы. Что находится в твоем ViewController’е?
Ну в этом-то плане у меня все отлично! Еще недавно здесь было две тысячи строк, работа с сетью, базой и другие ритуальные песнопения — но я послушал ребят из Rambler&Co и попробовал VIPER — просто супер! Вся логика разбита по элементам модуля, бизнес-логике вообще отдельный слой выделен. А в контроллере остались View Lifecycle, обработка пользовательских действий, получение введенных данных — но, конечно, все разбито по простым маленьким методам, часть из которых — приватные.

Отлично. Как ты проверяешь правильность работы своего модуля?
Ха, я понял, к чему этот разговор! Я пишу юнит-тесты! Я даже пробовал разрабатывать модуль по TDD — и мне понравилось. Я проверяю, что интерактор в результате выполнения своих методов дергает соответствующие зависимости, то же для презентера и роутера. Конечно, тесты презентера получаются очень простыми и очевидными, так как зачастую его роль заключается лишь в пробрасывании действий от view к интерактору — но зато я уверен в своем модуле и в любом возможном потоке действий. Тесты — это круто!

Это все прекрасно, но ты забыл упомянуть о еще одном элементе, который мы совсем недавно обсуждали.
Серьезно? Кто вообще тестирует контроллер? Ну я понимаю, конечно, случаи, когда он содержит сложную логику, но у нас же VIPER — а все говорят, что в этом случае view — тупая. Что там тестировать?

Что, если ты забудешь вызвать всего один из методов презентера во viewDidLoad?
Да ничего, как я могу забыть! Это же всего пара строк!

А что насчет ситуаций со сложными формами и получением введенных пользователем данных?
Да тут тоже все тривиально — вряд ли я запутаюсь в паре десятков полей ввода… Хотя…

А вот еще пример, продуктовое требование — все контроллеры с текстовыми полями при своем открытии должны сразу открывать клавиатуру.
Ну, здесь, конечно, нам поможет базовый контр… А, ну да, мы же договорились пока об этом не вспоминать. Не суть важно, композиция мне поможет — я в одном месте реализую это базовое поведение — а все контроллеры будут его использовать. Хм, похоже, вот этот случай действительно стоит тестировать — при создании нового экрана об этом требовании легко забыть.

То есть ты согласен, что тестировать контроллер нужно, но только в редких случаях?
Похоже, что да.

А что тебе дадут эти тесты?
Ну понятно же — я буду уверен, что мой контроллер работает правильно. Ну, то есть, не всегда, а в некоторых случаях. Наверное. Черт.

Ты еще упоминал про TDD — не поможет ли тебе эта практика в текущей ситуации?
Ну давай посмотрим. Все как водится, я напишу тест того, что контроллер сообщает презентеру о своей загрузке. Добавлю эту логику. Вуаля — теперь я точно знаю, что модуль запустится, ведь я привязал его к lifecycle экрана!
Заполнение форм, пробрасывание нажатий… Да ведь и правда, я могу все это делать по TDD! Если бы я знал об этом раньше, я бы даже не сделал тот отвратительный switch с выбором отображения таблицы.

А теперь посмотри на интерфейс контроллера.
А что с ним не так? Ух ты, мне же придется в него добавить все outlet’ы и IBAction’ы. Как так то. Выходит, интерфейс нашего класса изменился только ради тестов? Я же читал, что так нельзя! Но как же так — ведь для правильного написания класса мне нужно это знание. Точно! Давай вынесем их в отдельный extension, о котором будет знать только тест!

И тут мы вернулись к самому началу разговора. Пряча IBOutlet’ы из внешнего интерфейса мы просто врем пользователю о том, что представляет собой класс. Интерфейс должен быть явным, отображать внутреннюю сложность класса, список его ответственностей и зависимостей. Контроллер в этом плане ничем не отличается от любого другого класса. Интерфейс, приведенный тобой в самом начале, не говорит нам вообще ничего о сущности твоего контроллера — только его название. Факт реализации им протоколов ViewInput и ViewOutput тоже не раскрывает перед нами полной сложности этого класса.
Возможность не объявлять outlet’ы и IBAction’ы в интерфейсе — это еще один шажок Apple в сторону от чистого кода. И ты сам только что доказал это — при написании класса по TDD знание публичного интерфейса было тебе просто необходимо.

Но что тогда делать с тем, что всегда нас пугало — интерфейсы контроллеров с десятками свойств и методов?

А может, это просто индикатор того, что у контроллера слишком много ответственностей и имеет смысл забить тревогу и разбить его на модули?
И правда. Но ведь получается, что теперь 100% code coverage — это далекая, но вполне осуществимая мечта?

Вполне.
Так, еще один вопрос — мы же можем добиться того же эффекта и в реалиях MVC, просто вынеся наружу приватные методы контроллера, и проверяя, исполнились они или нет?

Просто открой: http://shoulditestprivatemethods.com/