Баланс между возможностью многократного использования и дублированием очень сложен. А что, если я скажу, что иногда этого делать не нужно?
В этой статье мы собираемся философски подойти к вопросу о назначении компонентов. Это позволит вам создавать функции, которые можно многократно использовать с минимальным риском.
В Интернете часто можно встретить банальности о возможности повторного использования (reusability) кода. Например:
Если вы используете компонент три раза, только тогда делайте его переиспользуемым.
Или
Дублированный код стоит дороже, чем неправильная абстракция.
Эти утверждения хороши в качестве общих советов. Но зачастую в написании многоразового кода есть гораздо больше нюансов.
Как мы рассмотрим в этой статье, часто можно сделать компонент более многоразовым с низким риском, что делает вас более быстрым.
Рассуждения об именовании
Для более наглядного объяснения рассмотрим некоторые представления.
Представьте, что мы разрабатываем приложение для управления деньгами, содержащее функцию, которая дает персонализированные финансовые советы в виде карточек с советами. Эта функция называется MoneyBuddy.
В частности, давайте рассмотрим компонент карточки, выделенный ниже.
Как бы вы назвали этот компонент? Возможно, MoneyBuddyCard
? Это хорошее название, но оно говорит о том, что его можно использовать только для фичи MoneyBuddy.
Здесь «неправильно» то, что название не позволяет использовать его в других местах. Как разработчики, мы теперь ограничены в использовании этой карты только для фичи MoneyBuddy.
Или, что тоже иногда встречается, мы используем «неправильный» компонент для других функций, но просто говорим своим коллегам: «Я знаю, что название неправильное, но мы должны использовать именно этот компонент».
Удалите сценарий использования из названия
Давайте попробуем убрать сценарий использования, дав тому же компоненту более общее имя. Тогда мы сможем использовать этот компонент в других функциях.
Что если назвать его AdviceView
? Тогда он будет отвязан от функции MoneyBuddy. Это больше говорит нам о том, для чего он используется.
Это лучше, теперь мы можем использовать этот компонент в других функциях. К сожалению, как следует из названия, мы можем использовать его только для советов. Потому что сценарий использования все равно просачивается в название.
Но этому компоненту «все равно», для чего мы его используем: для совета, рекламы, предупреждения или чего-то еще. С точки зрения этого компонента, у него нет конкретного use-case.
Давайте полностью уберем из названия сценарий использования, назвав его по тому, что представляет собой данный тип. В данном случае это карточка с некоторым стилем, назовем ее DecoratedCardView
.
Затем мы можем использовать его для функции MoneyBuddy, для советов в других местах, для многих других сценариев.
Это дает нам большую гибкость, и все, что нам нужно было сделать, — это убрать из названия сценарий использования!
Специфические или общие имена
Чем больше мы вписываем в название сценарий использования, тем больше мы ограничиваем компонент единственным применением. В результате компоненты со специфическими именами часто живут рядом с фичей.
И наоборот, когда мы убираем сценарий использования из названия, компонент становится более многоразовым. В этом случае он даже может жить в общей библиотеке пользовательского интерфейса.
Другой пример
Рассмотрим еще один пример, взятый из книги «Проектирование мобильных систем«.
Это экран, на котором студент связывается с репетитором — например, с преподавателем игры на гитаре. Этот преподаватель может создать для студента индивидуальный план обучения или учебную программу.
Обратите внимание, что этот пользовательский интерфейс создан с точки зрения ученика.
Здесь есть список TODO. Давайте рассмотрим этот компонент, с помощью которого студент может выполнять ежедневные задания.
В нашем случае мы видим список задач, поэтому можно подумать: «Давайте назовем его TODOListView
!».
Это логично. Ведь именно так мы его и называем, когда говорим об этом экране. Но попробуйте отделить техническое название от функции.
С таким названием, как TODOListView
, мы снова совмещаем то, как мы используем компонент, с названием его типа. Но этому компоненту «все равно», что он используется для списков дел.
Попробуйте рассуждать о функциональности, а не о сценарии использования компонента. Спросите себя: «Что делает этот компонент?».
Ответ заключается в том, что этот список — способ сделать выбор.
Сегодня мы используем этот компонент для TODO, но завтра мы можем использовать его для чего-то совершенно другого, например, для настройки push-уведомлений на экране настроек.
Давайте назовем его в честь того факта, что мы используем его для выбора, например SelectionView
.
Назвав его SelectionView
, мы сможем по-прежнему использовать этот компонент для списков TODO, а если позже нам понадобится использовать его для чего-то другого, у нас будет такая возможность.
Самое приятное. Мы не потратили никаких дополнительных усилий, чтобы сделать компонент многоразовым. Он просто такой есть. Все, что нам нужно было сделать, — это дать ему имя, не раскрывающее его назначения.
Обобщенные имена типов
Если более общие имена типов кажутся вам странными, имейте в виду, что вы, скорее всего, уже используете более общие компоненты для своих специфических функций.
Например, представим, что у вас есть приложение, в котором вы можете загружать фотографии своих блюд.
Для того чтобы пользователи могли выбрать фотографию, вы будете использовать специальный MealPhotoPicker
? Скорее всего, вы будете использовать более многоразовый PhotoPicker
, предлагаемый платформой. Возможно, в нем будет установлен фильтр для отображения фотографий блюд.
Но принцип тот же: в PhotoPicker
отсутствует сценарий выбора блюд. Это просто PhotoPicker
, который может быть использован для выбора блюд, а также для миллионов других случаев использования.
Добавляем контекст в код
Мы склонны использовать юзкейсы в именах типов, когда нам нужен дополнительный контекст.
Например, для отображения аватара и имени пользователя мы скорее используем тип UserProfileView
, чем ThumbnailView
. Это больше говорит нам о том, как использовать данный тип.
К счастью, мы можем получить лучшее из двух миров, когда мы предоставляем общие типы, но при этом сохраняем контекст.
При работе с более общими именами типов, такими как DecoratedCardView
, вы все равно можете поместить use-case в имя экземпляра типа, а не в его имя типа.
Ниже приведены два примера. В обеих парах use-case находится в имени экземпляра. В первой паре типы передают use-case в своем имени. Во второй паре типы имеют более общее имя.
// Before let moneyBuddyCard = MoneyBuddyCard(...) let adviceView = AdviceView(...) // After let moneyBuddyCard = DecoratedCardView(...) let adviceView = DecoratedCardView(...)
Добавление контекста с помощью фабрик
Существуют и другие способы добавления контекста. Один из них — использование фабрик.
Ниже мы представим фабрику MoneyBuddyFactory
, которая возвращает типы для функции. Мы можем вызвать makeAdviceCard()
, которая вернет тип DecoratedCardView
.
final class MoneyBuddyFactory { func makeAdviceCard() -> DecoratedCardView { return DecoratedCardView(...) } // ... rest omitted }
Теперь при вызове этого кода мы получаем вполне определенный контекст. Несмотря на то, что возвращаемый тип по-прежнему является многоразовым DecoratedCardView
, мы видим, что работаем с карточками советов в функции MoneyBuddy.
// We have a lot of context at creation let adviceCard = moneyBuddyFactory.makeAdviceCard() // The type is still DecoratedCardView type(of:adviceCard) // DecoratedCardView
Добавление контекста с помощью псевдонимов
Некоторые языки, например Swift, позволяют использовать псевдоним для присвоения конкретного имени общему компоненту. Таким образом, можно добавить больше контекста без необходимости вводить новый компонент.
Ниже мы вводим псевдоним MoneyBuddyCard
, который является новым именем для DecoratedCardView
. Затем, в рамках функции MoneyBuddy
, мы можем ссылаться на MoneyBuddyCard
вместо DecoratedCardView
.
// We add a specific name typealias MoneyBuddyCard = DecoratedCardView // We can use the specific name. let myCard = MoneyBuddyCard() // But its type is secretly still DecoratedCardView. // Below we explicitly define the type. let anotherCard: DecoratedCardView = MoneyBuddyCard()
При использовании псевдонима «старое» имя сохраняется. В этом случае компонент DecoratedCardView
остается доступным.
Теперь внутри функции мы можем использовать конкретное имя MoneyBuddyCard
, не делая новый компонент.
При этом мы сохраняем тип DecoratedCardView
, который можно использовать в других функциях. При желании мы можем даже перенести его в UI-библиотеку.
Эвристика для создания переиспользуемых компонентов на начальном этапе
Одна из опасностей при создании многоразовых компонентов — это создание чего-то для гипотетических, будущих случаев использования.
Мы не можем точно предсказать, как будут развиваться компоненты и требования — если бы мы могли предсказать будущее, мы бы, вероятно, наслаждались роскошным отдыхом в Сочи, а не читали эту статью.
Существует фактор риска, связанный с тем, чтобы заранее делать компоненты многоразовыми. Ведь если мы сделаем компонент более сложным для многократного использования, то это будет пустой тратой времени, если ни одна другая функция не будет использовать этот компонент. Мы будем поддерживать более сложный компонент без всякой причины.
Вместо того чтобы делать компонент многоразового использования «на всякий случай», спросите себя: «Создание моего компонента многоразовым».
- …увеличивает сложность моего компонента?
- …требует больших предварительных временных затрат?
Если на оба вопроса ответ отрицательный, то риск сделать компонент многократно используемым для одного случая использования невелик.
В примерах, приведенных в этой статье мы лишь изменили название. В частности, мы убрали из названия типа вариант использования.
Мы не увеличивали сложность и не тратили время на то, чтобы «сделать компонент многоразовым на всякий случай».
Таким образом, компоненты, которые мы создали, стали более многоразовыми в своей основе, без предварительных инвестиций.
Таким образом, риск остается низким. Представим, что нам никогда не понадобится повторное использование этих компонентов, тогда вреда практически не будет.
Самое замечательное, что эта концепция применима к любым компонентам, таким как потоки пользовательского интерфейса, бизнес-модели или даже названия целых модулей.
Заключение
Это может показаться простой концепцией, но уделить дополнительное время именованию очень важно. Это разница между тем, чтобы предоставить фичу, или предоставить фичу плюс здоровую библиотеку многократно используемых компонентов.
В следующий раз, когда вы будете создавать новый тип, посмотрите, можно ли убрать из его названия сценарий использования. Вы увидите, что тот же самый компонент становится более универсальным и пригодным для многократного использования, даже если вы только измените его название.
Если вы хотите узнать больше о компонентах многократного использования, то ознакомьтесь с главами 10 и 11 из книги «Проектирование мобильных систем«.