паттерны разработки мобильных приложений
Шаблоны проектирования мобильных приложений. Command Processor
“Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живёте.”
Мартин Голдинг
При разработке одного из проектов, использующего GooglePlacesAPI, у меня возникла проблема организации сетевого взаимодействия между моим Android–приложением и API сервера. Интуиция и “лапша” из AsyncTask’ов подсказывала, что должны быть другие способы организации такого рода взаимодействия. Так я наткнулся на шаблон проектирования CommandProcessor. Об использовании этого паттерна проектирования в Android-приложениях я и хочу рассказать.
Для начала, опишу задачу, которую нужно было решить. Требовалось написать приложение, использующее Google Places API, показывающее превью любого места на карте, которое выбрал пользователь, а далее, если пользователь захочет получить больше информации (например просмотреть больше картинок), то подгружать картинки по заданному Id выбранного места, и показывать уже все картинки, относящиеся к выбранному месту. Самым очевидным на тот момент для меня способом было использование AsyncTask. Но после некоторых попыток стало ясно, что должны быть и другие способы, более удобные. Использование AsyncTask’ ов было неудобным потому что:
1) Чтобы получить превью какого-нибудь места, необходимо было сначала сделать запрос для получения информации о всех местах, которые находились рядом с выбранным пользователем местом.
2) По полученным Id сформировать и отправить запрос о получении фотографии-превью.
3) При клике на превью получить все картинки относящиеся к этому месту.
Таким образом, при использовании AsyncTask’ов получался некий «водопад» и пришлось бы использовать один AsyncTask внутри другого. И тогда, погуглив, я нашел информацию о паттерне Command Processor, который отлично справляется с задачами, описанными выше.
Паттерн проектирования CommandProcessor разделяет запросы к сервису от их выполнения. Главный компонент паттерна — CommandProcessor, управляет запросами, планирует их выполнение, а также предоставляет дополнительный сервис, например, хранение запросов для позднего выполнения или отмены запроса. Диаграмма, заимствованная из [1] показывает отношения между компонентами паттерна:
Области применения паттерна
Реализация
Теперь, рассмотрим, как этот паттерн можно применять при разработке мобильных приложений, а конкретно, Android–приложений. Способов реализации много, можно использовать IntentService или HaMeR-фрэймворк (Handler, Messages, Runnable) Давайте рассмотрим, как я имплементировал данный паттерн в тестовом приложении. Итак, тестовое приложение показывает маршруты и список мест, которые содержатся в том или ином маршруте. Соответственно, у нас есть два типа запросов (команд): TracksRequest и PlacesRequest. Оба класса являются наследниками базового класса CommonRequest, для того чтобы обрабатываться нашим процессором (CommandProcessor).
В методе sendAsyncPlaceRequest происходит вся работа: это может быть создание URL для запроса к API, создание нового Thread, парсинг ответа и передача результата работы контроллеру с помощью Handler.
Далее следует реализовать класс CommandProcessor, который будет управлять нашими запросами и выполнять их:
Теперь нам нужен котроллер, который, в зависимости от состояния будет отправлять разные команды процессору. Результат работы запроса отправляется из потока с помощью Handler:
Для того, чтобы теперь вернуть результат работы в активити, и вызвать какой-нибудь updateUI() для обновления пользовательского интерфейса (наполнения ListView, отрисовка маркеров на карте и т.д.) нужно определить интерфейс UpdateCallbackListener:
И реализовать его в нашей активити:
После того, как результат вернется в ответе на запрос (к примеру, запрос на получение всех мест по этому маршруту) нам необходимо обновить актвити и передать объекты Place в адаптер. Это мы можем сделать через метод processor_.updateActivity(places), который вызовет onUpdate() в активити, которая имплементировала данный метод. Следующая диаграмма, также взята из [1] показывает динамику поведения паттерна:
Чтобы инициировать запрос, нам нужно создать в активити объект TracksRequestи передать его в controller:
Реализация с помощью IntentService
Использование IntentService также отлично позволяет реализовать этот паттерн. Рассмотрим диаграмму:
В качестве объекта-команды можно использовтаь Intent и передавать его в наш процессор. Creator — это наша activity, которая создает объект-команду, и передает этот объект executor’у, то есть IntentService’у в нашем случае. Таким образом, роль CommandProcessor выполняет класс CustomIntentService, а именно метод onHandleIntent() который в зависимости от данных, содержащихся в Intent, может выполнять различные операции. Чтобы вернуть результат в активити, в данном случае можно использовать BroadcastReceiver.
Пошаговая инструкция
Итак, подведем итоги, чтобы реализовать данный паттерн необходимо выполнить следующее:
Достоинства и недостатки паттерна.
Заключение
Скорее всего, в том или ином виде вы уже видели реализацию паттерна. Но так как тема архитектуры мобильных приложений является достаточно актуальной, надеюсь, статья была полезной. В дальнейшем планируется рассмотреть еще несколько паттернов, применяемых в разработке мобильных приложений. Пишите вопросы в комментариях, до встречи.
Мобильные UX-паттерны, которые используются неправильно
(Мы продолжаем переводить цикл статей по UX/UI. Полную подборку можно найти в коллекции « Дизайн навигации»)
Если вы опытный дизайнер, то наверняка согласитесь: в UI-дизайне вдохновляться работами других — это не воровство. Это исследование передового опыта. Это использование дизайн-паттернов. Это следование руководствам. Это забота о пользователе: мы используем знакомые паттерны, чтобы создавать удобные интерфейсы.
Некоторые скажут, что если следовать руководствам и повторять за другими, пропадет фактор творчества, и все приложения в конце концов будут выглядеть одинаково. С точки зрения UX я вижу другую проблему. Когда привыкаешь подражать общепринятым паттернам, начинаешь верить, что Google / Facebook / Instagram / [ваше любимое приложение] всегда правы, в них такие же дизайн-цели как у нас, и нет смысла в них сомневаться. В этой статье я приведу несколько паттернов, которые считаются (или раньше считались) образцовыми, но на деле оказываются не столь прекрасными.
Интересуетесь свежими статьями по дизайну? Вступайте в группу на Facebook.
Ищите системное погружение в тему? Загляните в блог для дизайнеров.
1. Спрятанная навигация
На тему бургерного меню написано по меньшей мере полмиллиона постов: множество людей (в основном дизайнеров) высказывали свои аргументы против этого паттерна. Если вы не в курсе, прочитайте один или два из них. Вкратце, все дело не в самой иконке, а в идее спрятать навигацию за иконкой.
Решение очень заманчивое и удобное для дизайнера: не нужно париться из-за ограниченного экранного пространства, просто запихни всю навигацию в прокручиваемую накладку, которая по умолчанию спрятана.
Однако эксперименты показывают, что когда опции меню находятся на виду, растет и увлеченность, и удовлетворенность пользователей, и даже доход. Именно поэтому сейчас все крупные игроки переходят от бургерного меню к отображению ключевых опций навигации основном экране.
2. Иконки, иконки повсюду
Экранное пространство на мобильных устройствах ограничено, и решение, казалось бы, лежит на поверхности: по максимуму заменить текстовые лейблы иконками. Пиктограммы занимают меньше места, их не нужно переводить и людям они знакомы, правда же? И так делает каждое второе приложение.
Полагаясь на этот принцип, дизайнеры часто прячут функции под непонятными иконками, которые достаточно сложно истолковать правильно. К примеру, смогли бы вы догадаться, что за этой иконкой в Instagram скрывается отправка личных сообщений?
Или, предположим, вы никогда раньше не пользовались переводчиком Google. Какую функциональность вы ожидаете под такой пиктограммой?
Это очень распространенная ошибка — предполагать, что пользователи знакомы с вашими абстрактными пиктограммами или хотят тратить время на их изучение.
Если вы спроектировали иконку и вам кажется, что ее нужно объяснить при помощи всплывающей подсказки — значит вы что-то делаете неправильно. Даже если вы Foursquare и вашим пользователям так или иначе придется выучить эту иконку.
Это не значит, что вообще не нужно использовать иконки. Есть куча иконок, которые хорошо известны пользователям — в основном те, которыми обозначают популярные функции: поиск, воспроизведение видео, письмо, настройки и так далее. (Но пользователи все равно могут сомневаться. Например, что именно случится, если нажать на сердечко?)
Сложные и абстрактные функции всегда должны сопровождаться понятным текстовым лейблом. В подобных случаях иконки могут выступать отличным дополнением к лейблам: так пользователям будет проще ориентироваться в функциях, а само приложение приобретет индивидуальность.
Для базовой функциональность можно использовать иконки, но сложные функции нужно пояснять текстовыми лейблами. (И если вы используете иконки, всегда проверяйте их на юзабилити-тестах).
3. Навигация на основе жестов
Когда в 2007 году компания Apple представила рынку iPhone, мультитач-технология получила широкое распространение и пользователи узнали, что можно не только указывать и нажимать, но и приближать, отдалять и смахивать.
Жесты стали популярны у дизайнеров и многие приложения экспериментировали с навигацией на основе жестов.
Жесты — это заманчивая возможность сэкономить экранное пространство (так же как и спрятанная навигация, и иконки). “Не должно быть кнопки “удалить”, люди могут просто провести пальцем влево. Или вправо. Это решим”
Первое, что нужно знать о жестах — они всегда спрятаны. Людям приходится их запоминать. Это как с бургерным меню: если спрятать опцию, люди будут реже ею пользоваться.
К тому же, у жестов та же проблема, что и у иконок: есть общепринятые жесты, которые понятны всем (например, таппинг, приближение, прокрутка), а есть такие, которые приходится изучать и осваивать — причем в каждом приложении они разные.
К сожалению, большинство жестов можно отнести к нестандартным, да и используются они непоследовательно — все-таки это относительно новая сфера дизайна интерфейсов. Даже такой простой жест, как смахивание письма в почтовом ящике, может работать по-разному в разных приложениях.
Распространенные паттерны проектирования и архитектуры приложений на Android
Распространенные паттерны проектирования и архитектуры приложений на Android
В какой-то момент Будущий Вы станете наследовать код, который вы пишете, и, вероятно, у вас возникнут вопросы по поводу того, почему вы раньше этого не делали. Вы можете оставить в своем коде массу сбивающих с толку комментариев, но лучше использовать общие шаблоны проектирования и архитектуры приложений.
Представим, что вы планируете оставить свою текущую работу и хотите облегчить жизнь тем, кто будет делать вашу работу в дальнейшем. Вы же не хотите, чтобы они «нахваливали» ваш спагетти-код, который вы оставили.
В этом туториале вы не найдете исчерпывающий список шаблонов проектирования и архитектур приложений. Считайте это «практическим справочником» и отправной точкой для дальнейшего обучения.
Заметка
Предполагается, что вы знакомы с основами разработки под Android.
Приступим
Будущий Вы: «Будет ли в проекте место, где мне придется повторно использовать один и тот же фрагмент кода?».
Вам следует свести к минимуму время, затрачиваемое на «детективную» работу, на поиск сложных зависимостей проекта. Вам следует создать проект, который будет можно использовать повторно, который будет удобен и прост. Эти критерии могут быть применены и к одному объекту и к целому проекту и приводят к шаблонам, которые попадают под следующее:
Паттерны проектирования обычно имеют дело с объектами. Они представляют собой решение повторяющейся проблемы, которую показывает объект, и помогают устранить проблемы, связанные с дизайном. Другими словами, они содержат проблемы, с которыми уже столкнулись другие разработчики, и избавляют вас от необходимости изобретать велосипед, показывая вам проверенные способы их решения.
Вы можете использовать один или несколько из этих шаблонов. Главное, не оставляйте это только на вашу интуицию.
В следующих разделах вы рассмотрите эти шаблоны из каждой категории и увидите, как они применимы к Android:
Порождающие шаблоны:
Структурные шаблоны:
Поведенческие шаблоны:
Порождающие шаблоны
Будущий Вы: «Когда мне нужен особенно сложный объект, как мне получить его экземпляр?»
Явно худшим вариантом ответа будет: «Просто копируйте и вставляйте один и тот же код каждый раз, когда вам понадобится экземпляр этого объекта». Вместо этого порождающие шаблоны делают создание экземпляров объекта простым и повторяемым.
Builder
В Android примером шаблона Builder является AlertDialog.Builder :
Другой набор вариантов привел бы к совершенно другому сэндвичу, то есть предупреждению. :]
Dependency Injection
Dependency Injection (инъекция зависимостей) похоже на переезд в меблированную квартиру. Все необходимое уже есть. Вам не нужно ждать доставки мебели или следовать инструкциям IKEA, чтобы собрать книжную полку Borgsjö.
С точки зрения программного обеспечения, инъекция зависимостей позволяет вам предоставить все необходимые объекты для создания экземпляра нового объекта. Этому новому объекту не нужно создавать или настраивать сами объекты.
В Android вы можете обнаружить, что вам нужно получить доступ к одним и тем же сложным объектам из разных точек вашего приложения, таких как сетевой клиент, загрузчик изображений или SharedPreferences для локального хранилища. Вы можете внедрить эти объекты в свои действия и фрагменты и сразу же получить к ним доступ.
В настоящее время существует три основных библиотеки для внедрения зависимостей: Dagger «2», Dagger Hilt и Koin. Давайте посмотрим на пример с Dagger. В нем вы аннотируете класс с помощью @Module и заполняете его такими методами @Provides :
Модуль выше создает и настраивает все необходимые объекты. В качестве дополнительной практики в больших приложениях вы можете создать несколько модулей, разделенных по функциям.
Этот компонент связывает Модуль откуда эти зависимости идут с местом инъекций этих зависимостей.
Наконец, вы используете аннотацию @Inject для запроса зависимости там, где она вам нужна, вместе с lateinit для инициализации свойства, не допускающего значения NULL, после создания содержащего объекта.
В этом туториале мы рассматриваем упрощенный вариант, но вы можете прочитать документацию Dagger для получения дополнительных сведений о реализации. Вы также можете щелкнуть приведенные выше ссылки в упомянутых библиотеках для получения подробных туториалов по каждой теме.
Поначалу этот паттерн может показаться сложным, но он может упростить ваши действия и фрагменты.
Singleton
Шаблон Singleton указывает, что только один экземпляр класса должен существовать с глобальной точкой доступа. Этот шаблон хорошо работает при моделировании реальных объектов только с одним экземпляром. Например, если у вас есть объект, который устанавливает подключения к сети или базе данных, наличие более одного экземпляра проекта может вызвать проблемы и смешать данные. Вот почему в некоторых сценариях вы хотите ограничить создание более одного экземпляра.
В Kotlin ключевое слово object объявляет синглтон без необходимости указывать статический экземпляр, как в других языках:
Когда вам нужно получить доступ к членам объекта синглтона, вы делаете такой вызов:
Под капотом статическое поле INSTANCE поддерживает объект Kotlin. Итак, если вам нужно использовать объект Kotlin из кода Java, вы измените вызов следующим образом:
Используя объект, вы будете знать, что используете один и тот же экземпляр этого класса во всем приложении.
Синглтон, вероятно, является изначально наиболее простым для понимания шаблоном, даже слишком простым в использовании и поэтому им злоупотребляют. Поскольку он доступен из нескольких объектов, у синглтона могут быть неожиданные побочные эффекты, которые трудно отследить. Другие шаблоны проектирования могут быть безопаснее и проще в обслуживании.
Factory
Как следует из названия, Factory заботится обо всей логике создания объекта. В этом шаблоне класс factory контролирует, какой объект создать. Шаблон factory пригодится при работе со многими обычными объектами. Вы можете использовать его там, где не хотите указывать конкретный класс.
Взгляните на код ниже:
Давайте рассмотрим пошагово:
Вы можете использовать это так:
Это поможет сохранить создание объекта в одном классе. При неправильном использовании класс Factory может раздуться из-за чрезмерного количества объектов. Тестирование затруднится, поскольку класс factory сам отвечает за все объекты.
Структурные шаблоны
Будущий Вы: «Итак, когда я открою этот класс, как я запомню, что он делает и как это собрано?».
Adapter
В знаменитой сцене фильма «Аполлон-13» команда инженеров вставила квадратный стержень в круглое отверстие. Образно говоря, это роль адаптера. С точки зрения программного обеспечения, этот шаблон позволяет двум несовместимым классам работать вместе, преобразовывая интерфейс класса в интерфейс, ожидаемый клиентом.
В этой ситуации вы можете использовать подкласс RecyclerView. Adapter и реализовать необходимые методы, чтобы все работало:
Facade
Шаблон Facade предоставляет интерфейс более высокого уровня, который упрощает использование набора других интерфейсов. Следующая диаграмма иллюстрирует эту идею более подробно:
Если вашей Activity нужен список книг, она должна иметь возможность запрашивать один объект для этого списка, не понимая внутренней работы вашего локального хранилища, кэша и клиента API. Помимо сохранения чистоты и краткости кода Activity и Fragment, это позволяет вам вносить любые необходимые изменения в реализацию API, не влияя на Activity.
Это позволит вам сделать все типы настроек под капотом, не затрагивая клиента. Например, вы можете указать настраиваемый десериализатор JSON, о котором Activity ничего не знает:
Чем меньше каждый объект знает о том, что происходит за кулисами, тем легче вам в будущем будет управлять изменениями в приложении.
Decorator
Шаблон Decorator динамически наделяет объект дополнительными обязанностями, расширяя его функциональность во время выполнения кода. Взгляните на пример ниже:
Вот что делает приведенный выше код:
Composite
Вы используете шаблон Composite, когда хотите представить древовидную структуру, состоящую из однородных объектов. Составной паттерн может иметь два типа объектов: Composite и Leaf. Составной (Composite) объект может иметь дополнительные объекты, тогда как конечный объект является последним объектом.
Взгляните на следующий код:
В приведенном выше коде у вас есть:
Поведенческие шаблоны
Будущий вы: «Итак… как мне определить, какой класс за что отвечает?».
Поведенческие шаблоны позволяют вам назначать ответственность за различные функции приложения. В будущем вы можете использовать их для навигации по структуре и архитектуре проекта.
Эти шаблоны могут различаться по масштабу, от отношений между двумя объектами до всей архитектуры вашего приложения. Часто разработчики используют несколько поведенческих паттернов вместе в одном приложении.
Command
Когда вы заказываете Saag Paneer в индийском ресторане, вы не знаете, какой повар приготовит вам блюдо. Вы просто заказываете блюдо официанту, который размещает заказ на кухне, передавая его доступному повару.
После определения вашего события вы получаете экземпляр EventBus и регистрируете объект в качестве подписчика. Например, если вы зарегистрируете Activity, у вас будет:
Теперь, когда объект является подписчиком, сообщите ему, на какой тип события подписаться и что он должен делать при его получении:
Наконец, создайте и опубликуйте одно из этих событий на основе ваших критериев:
Поскольку большая часть этого шаблона творит чудеса во время выполнения, в будущем у вас могут возникнуть небольшие проблемы с отслеживанием этого шаблона, если у вас нет хорошего покрытия тестами. Тем не менее, хорошо продуманный поток команд уравновешивает и делает шаблон хорошо читаемым, и за ним будет позже легко следить.
Observer
Шаблон Observer определяет взаимозависимость между объектами. Когда один объект меняет состояние, его созависимые получают уведомление и обновляются автоматически.
Этот паттерн универсален. Вы можете использовать его для операций с неопределенным временем, таких как вызовы API. Вы также можете использовать его для ответа на ввод пользователя.
Первоначально он был популяризирован фреймворком RxAndroid, также известным как Reactive Android. Эта библиотека позволяет реализовать этот шаблон во всем приложении:
Объекты Subscriber будут прослушивать эти значения и реагировать на них по мере их поступления. Например, вы можете открыть подписку при вызове API, прослушать ответ сервера и соответствующим образом отреагировать.
Совсем недавно Android также представил собственный способ реализации этого шаблона через LiveData. Вы можете узнать больше об этой теме здесь.
Strategy
Вы используете шаблон Стратегия, когда у вас есть несколько объектов одной и той же природы с разными функциями. Для лучшего понимания взгляните на следующий код:
Вот как это использовать:
State
В шаблоне State состояние объекта соответственно изменяет свое поведение при изменении внутреннего состояния объекта. Взгляните на следующие фрагменты:
Вот как это использовать:
Архитектура приложений
Будущий Вы: «Может ли в какой-то момент появиться требование, по которому мне придется изменять или внедрять новые функции в проект?».
Архитектура приложений играет жизненно важную роль в структурировании слабосвязанной кодовой базы. Вы можете использовать ее где угодно, независимо от платформы. Архитектура приложений помогает писать легко тестируемый, расширяемый и несвязанный код.
Проще говоря, архитектура относится к общей организации вашего кода, например:
Типы архитектур приложений
Существует множество архитектур приложений, используемых для создания надежных и поддерживаемых кодовых баз, но в этой статье вы узнаете самые популярные из них:
Model View Controller
Model View Controller, или MVC, относится к одному из самых популярных архитектурных шаблонов, от которого происходят многие другие. На Android особенно легко настроить свой проект на MVC. Его название относится к трем способам классификации классов в вашем коде:
Разделение вашего кода на эти три категории будет иметь большое значение для его разделения и повторного использования.
В конце концов, клиент попросит Будущего Вас добавить в приложение новый экран, который использует его существующие данные. Следование парадигме MVC означает, что Будущий Вы сможет легко повторно использовать те же модели и изменять только вью.
Или, возможно, в будущем клиент попросит вас переместить этот модный виджет с главного экрана на экран с деталями. Разделение логики вью упрощает эту задачу.
Кроме того, перенос как можно большего количества логики макета и ресурсов в Android XML сохраняет ваш View layer чистым и аккуратным. Круто!
Возможно, вам время от времени придется рисовать на Kotlin. В этом случае это поможет отделить операции рисования от классов активности и фрагментов.
Со временем вам станет проще принимать архитектурные решения с помощью MVC и Future. Вам будет легче решать проблемы по мере их возникновения.
Model View ViewModel
Этот архитектурный шаблон с довольно запутанным названием похож на шаблон MVC. Компоненты Model и View такие же.
Объект ViewModel является связующим звеном между слоями model и view layer, но работает иначе, чем компонент Controller. Вместо этого он предоставляет команды для вью и привязывает вью к модели. Когда модель обновляется, соответствующие вью обновляются через связывание данных.
Точно так же, когда пользователь взаимодействует с вью, связывание работает в противоположном направлении, автоматически обновляя модель.
Паттерн MVVM набирает популярность, но по-прежнему является относительно недавним дополнением к библиотеке паттернов. Google теперь рекомендует этот шаблон как часть своей библиотеки компонентов архитектуры.
Clean Architecture
Чистая архитектура сама по себе не архитектура, а концепция. Она описывает общую архитектуру приложения: как различные уровни приложения, бизнес-объекты, варианты использования, докладчики, хранилище данных и пользовательский интерфейс взаимодействуют друг с другом.
MVC и MVVM существуют в чистой архитектуре на уровне внешнего представления и пользовательского интерфейса.
Вы можете найти исходное определение чистой архитектуры в этой статье. Доступно множество примеров чистой архитектуры для Android, в том числе «Архитектура Android… Эволюция».