Несколько лет назад ... Wehkamp, ​​известный в Нидерландах на протяжении десятилетий розничный торговец, уже успешно превратился из компании, занимающейся каталогами, в 100% интернет-магазин. Пришло время изучить вселенную приложений на Wehkamp.

Мы начали с команды SCRUM, в которую входили 1 разработчик Android, 1 разработчик iOS, 1 тестировщик, несколько бэкендеров, мастер Scrum и владелец продукта. Мы заполнили наш бэклог и начали создавать приложения с нуля. Приложения поддерживались .NET API, который работал на локальных серверах IIS. Среда .NET работала уже значительное количество лет и превратилась в большой монолит.

Микросервисы и микросайт

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

Первые дни

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

На данный момент… в нашей команде 3 разработчика iOS и 2 разработчика Android, и мы все еще ищем больше.

История повторяется

Но… история повторяется. Там, где был Интернет несколько лет назад, приложение развивалось в том же направлении; монолит с большой задницей.

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

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

Цель

Нашей целью было уйти от этого:

К этому:

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

Внедрение зависимостей FTW

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

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

Функции

Другой подход, который помог, заключался в разделении кода на отдельные «функциональные группы». Я помещаю весь код, связанный с определенной функцией, в отдельную группу. Как вы можете видеть здесь:

В сочетании с внедрением зависимостей это привело к созданию довольно независимых частей кода. Каждая функция имеет свою собственную раскадровку (или иногда несколько, чтобы предотвратить адский конфликт-раскадровку-команду-слияние-конфликт) и все другие вещи, связанные с этой функцией. Как вы можете видеть на картинке, есть, например, функции «Покупки» и «Избранное». Функция «Избранное» показывает нам список продуктов, и вы можете представить, что пользователь хочет видеть подробную информацию о продукте, нажимая на продукт в этом списке. Контроллеры просмотра сведений о продукте находятся в функции «Покупки». Для этого я добавил ссылку раскадровки на раскадровку Покупки в раскадровку Избранное. И… ушла наша разлука!

Первый шаг; Core и CoreUI против независимости

Важной частью реализации микропрограмм является то, что они должны жить сами по себе. Иногда это может противоречить принципу DRY (не повторяйся). Бывают ситуации, когда вам придется копировать и вставлять код из одной функции в другую. Раньше я использовал как можно больше кода. Например; представьте себе список товаров для определенной категории, например «Женская одежда». Это показывает нам список таких продуктов:

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

Теперь представьте, что мы хотим добавить в наше приложение новую замечательную функцию; список желаний! Дизайн показывает нам, что это в основном тот же дизайн, что и у «обычного» списка продуктов. Итак, моя первая мысль; давайте использовать это повторно! Для этого мы создаем класс ProductListCollectionView. Нам нужно определить источник данных, поэтому давайте создадим протокол ProductListCollectionViewDatasource. В существующем списке продуктов у нас есть значок сердца, чтобы добавить продукт в список желаний, но мы не хотим этого, если вы есть в списке желаний. Итак, давайте добавим протокол ProductListCollectionViewDelegate с методом shouldShowHeartIcon.

Ух ты! Это работает как шарм. Через несколько спринтов мы хотим добавить значок в список желаний, чтобы добавить продукт в корзину. Это A / B-тест, поэтому пока мы не хотим, чтобы он фигурировал в обзоре продуктов. Мы должны добавить дополнительный метод делегата shouldShowAddToCart. Вы можете себе представить, что он превратится в неприступного, уродливого и испорченного гиганта. В конце концов, у вас не останется много общего пользовательского интерфейса. Надо было начать создавать его как самостоятельный элемент, не делясь друг с другом. Не расстраивайтесь, если иногда копируете одну функцию в другую.

Но есть компромисс. Если вы обнаружите, что копируете определенную часть кода во все функции, это может быть сигналом о том, что можно создать что-то для совместного использования этого кода. Здесь в игру вступают модули «core» и «coreUI».

Это модули, в которые вы помещаете код, который используется всеми функциями. Иногда бывает сложно решить, должен ли конкретный фрагмент кода находиться в «ядре» или ему нужен собственный модуль. Я решил положить в ядро ​​такие вещи:

  • Сети
  • Безопасность
  • AB тестирование
  • Аналитика
  • Конфигурация
  • Утилиты
  • Расширения классов Foundation
  • и т.п.

То же самое количество элементов пользовательского интерфейса, вот что вы найдете здесь:

  • Элементы пользовательского интерфейса
  • Макеты коллекций
  • Базовый ViewController
  • Проверка ввода
  • Состояние полного ViewController
  • Расширения классов UIKit
  • и т.п.

Я считаю, что мы должны помещать как можно меньше кода в эти модули, время от времени копировать / вставлять не так уж и плохо ...

Функции

Следующим шагом является создание нового модуля только с кодом для этой конкретной функции. Я объясню, как это сделать для вашего проекта iOS в Xcode, шаг за шагом в другом посте. (Изменить; из-за проблем со временем я создал пример приложения, возможно, позже появится еще один пост. Источник: https://github.com/martijnschoemaker/modularizesampleapp). Пока что речь идет только о концепции. В нашем случае при открытии фич-проекта вы увидите следующие группы:

  • UI; со всеми файлами, относящимися к пользовательскому интерфейсу; ViewControllers, представления, раскадровки и NIB.
  • AB Tests; здесь мы определяем тесты AB, которые мы используем в этой функции.
  • Обработчики; вот реализации обработчиков, которые мы можем определить. Например, можно зарегистрировать обработчик для делегата ApplicationContinueActivity таким образом, чтобы модуль мог сам решить, должен ли он обрабатывать это, и если да, то что делать.
  • Модель; для всех классов моделей
  • Сетевые операции; мы определяем наш сетевой запрос как HttpOperations. HttpOperation - это протокол, для конкретного запроса мы реализуем HttpOperation, здесь вы можете найти эти операции.

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

Зависимости

Помните тот пример, в котором мы хотели отображать экран с подробными сведениями о продукте при нажатии на продукт в списке желаний? Раньше мы просто разделяли функции на разные группы в рамках одного проекта. Таким образом, это дало нам возможность использовать код из других функций.

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

Но как? Вся эта магия зависимости достигается путем добавления модуля «человек посередине»; зависимости. Этот модуль позаботится о зависимостях между функциями. Функции не ссылаются друг на друга, а только ссылаются на этот модуль зависимости. Модуль зависимостей не должен содержать никаких классов реализации. Это всего лишь связующее звено между функциями.

Если мы хотим открыть подробное представление продукта при нажатии на список желаний продукта, нам понадобится что-то вроде ProductViewControllerProvider

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

Так что здесь нам нужна некоторая абстракция ...

Только протоколы

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

Для нашей детали продукта из примера списка желаний это приводит к примерно следующему:

  1. В функции Wishlist мы реализуем список продуктов, которые находятся в списке желаний.
  2. В зависимостях у нас есть протокол ProductViewControllerProvider с методом getProductViewController () - ›UIViewController.
  3. В функции покупок у нас есть реализация ProductViewControllerProvider, которая знает, как создать контроллер представления и возвращает его.
  4. Посредством внедрения зависимостей мы определяем в модуле покупок, что для протокола ProductViewControllerProvider должна использоваться реализация в модуле покупок.

Что теперь?

С того момента, как мы создали базовую реализацию, введя core, coreUI и зависимости, мы решили разбить функциональность на модули. Выбор одной или двух функций за спринт. На данный момент мы сделали около 80% существующих функций. Для новых функций мы создадим новый модуль.

Удачной модульности!

Пример приложения

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

Https://github.com/martijnschoemaker/modularizesampleapp

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