UI-tests in Android. Часть 3. Более сложные примеры.

В предыдущей статье мы посмотрели на более менее простые примеры юай тестов. Теперь же мы пойдем дальше по проекту и посмотрим на примеры более сложных юайтестов.

Ранее мы тестировали функционал добавления элементов, теперь же перейдем к следующему экрану — списку категорий и списку самих элементов. Здесь у нас есть такой функционал как swipe to remove — смахнуть чтобы удалить. И он касается как отдельного элемента так и целой категории. Я не хотел делать как сейчас принято — после свайпа (зачсатую случайного) отображать снекбар для отмены. Ведь зачем отменять действие, когда лучше подтвердить его?

Так же на экране списка у нас есть фунцкионал мануальной сортировки элементов. Т.е. можно нажать на элемент, держать его и перетащить вверх или вниз по списку. Увы я не нашел пока что способ для тестирования этого с помощью Espresso (если вы знаете как это сделать, напишите мне в телеграм @JohnnySC). В предыдущей статье я обещал рассказать о том, какие юайтесты неавтоматизируемы. Так вот — этот не то, чтобы не автоматизируем, я просто не нашел решения. А вот действительно неавтоматизируемый юай тест — это когда нужно мануально что-то сделать. Например на главном экране есть кнопка синхронизации — когда ты сохраняешь всю базу данных в файл. Написать юай тест можно например на такой кейс — создать элемент, сохранить базу в файл, удалить элемент из приложения и потом восстановить базу данных на экране синхронизации, после чего проверить, что элемент вернулся на место, хотя здесь нам понадобится рестартнуть приложение, т.е. закрыть его полностью и открыть заново. Вероятно это возможно, нужно посмореть на возможности UI-Automator. Но что на мой взгляд неавтоматизируемо, так это такой кейс — сохранить базу в файл, после чего удалить файлик из хранилища и попытаться восстановить. Мы все понимаем, что на разных устройствах стоит разный софт для доступа к дереву файлов, так что написать какой-то единый юай тест для этого будет затруднительно. Опять же, нужно смотреть на возможности UI-Automator.

Ладно, вернемся к юай тестам наших списков. Для этого посмотрим на пакет — ссылка. Так как у нас есть RecyclerView, а каждый элемент этого списка является иерархией вьюх, то нам нужен матчер, которого не существует в библиотеке Espresso. Например для того, чтобы на элементе списка нажать конкретно на нужную кнопку или проверить нужный текст. Для этого я написал такой вот матчер — ссылка. Суть в том, что мы можем добраться до нужной вью тем же способом что и делаем это в котлин коде. Т.е. берем и находим вью по айди, обратите внимание на второй класс — ListItemRecyclerViewMatcher. Мы передаем список иерархии нужных айди и добираемся до нужной вью на конкретной позиции. Теоретически вы можете написать свой матчер для любого случая, если его не существует в библиотеке. Точно так же вы можете написать свой кастомный ViewInteraction/ViewAction для взаимодействия с вью, но это уже сложнее.

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

Список категорий
Список элементов внутри категории

Итак, в пакете списка у нас есть следующие юай тесты (коротко пробежимся по каждому):

CategoryDetailsListTest — здесь мы создаем 3 категории, по 1, 2 и 3 элемента для каждого и просто проверяем контент, т.е. проверяем что в списках наши элементы отображаются верно.

CategoryListSwipeToRemoveTest — здесь мы удаляем полностью категорию с помощью свайпа и внутри 2 теста, первый простой, второй сложный. И если суть первого просто удалить и проверить что элемент после удаления не существует на экране, то суть второго проверить, что мы действительно удалили не только категорию, но и внутренний элемент. Ведь если мы попробуем после удаления добавить этот элемент снова, то в случае если он не удалился (элемент), то будет диалог о том, что такой элемент уже существует. Немного подробностей — мы используем библиотеку Realm для хранения базы данных и неопытный человек мог бы например при удалении категории забыть удалить вложеные элементы, не зная, что это необходимо делать самостоятельно (да да да, в реалме до сих пор нет автоматического удаления вложенных элементов).

LearningPairListSwipeToRemoveTest — здесь мы точно так же проверяем, что элемент действительно удалился, ведь если бы мы просто проверяли юай (это же юай тест, надо проверяеть юай — ничего подобного, нужно проверять функционал!), то нам бы хватило одного раза. А так мы удалим элемент, после этого попробуем его добавить еще раз, если не будет диалога о том, что такой элемент существует, значит наше удаление прошло действительно удачно.

EditLearningPairTest — также со списка элементов мы можем перейти на экран редактирования. Значит нужно проверить, что после редактирования элемента его перевод поменялся.

Более менее сложными юай тестами я называю такие, где для его проверки затрагиваются несколько экранов. На моем ноутбуке тест для проверки функционала редактирования занял чуть больше чем 7 секунд. 2 теста для проверки функционала удаления категорий — 14 секунд. 18 секунд ушло на проверку удаления элемента. Как видите, все эти тесты довольно-таки короткие и они стоят того, чтобы их написать. Ведь вы один раз написали эти тесты и можете каждый раз проверять ваш функционал.

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

Экран для изучения выглядит примерно так.

Экран изучения

Как видим здесь есть статистика по конкретному элементу — количество правильных ответов и количество ошибок. Также в тулбаре у нас имя категории, которую изучаем. Есть кнопка пропуска, если например вы не помните перевод элемента, тогда в статистику добавляется 1 ошибка и переходит вперед. Если нет элементов или же вы закончили изучение, на экране будет диалог с результатами. После этого вся статистика для категории обновится. Т.е. для категории количество правильных и неправильных ответов это сумма по каждому элементу внутри. И конечно же нам нужно понимание на каком мы элементе находимся и сколько еще осталось. Как видите, здесь действительно много функционала, который нужно протестировать. И да, если вы случайно нажали назад, то вам нужно будет подтвердить действие. Итак, рассмотрим все тесты в пакете.

StudyCancelTest — мы открываем экран изучения и просто уходим назад. Значит нам нужно проверить, что статистика ни элемента ни категории не изменилась.

StudyCorrectTest — простой тест на проверку функционала правильного ответа. После того, как мы дали правильный ответ, мы должны увидеть сообщение что был дан верный ответ, после чего увидим диалог с результатом.

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

StudySkipTest — простой тест на проверку функционала пропуска. Значит нам нужно проверить, что видели сообщение, которое содержит верный ответ и диалог с результатом. После проверить статистику элемента и категории.

Теперь, когда мы проверили каждый функционал экрана по отдельности мы должны написать действительно сложный юай тест, который бы имитировал некий настоящий опыт пользователя. Ведь никто не будет создавать 1 элемент и заучивать его. Нам нужен тест, где мы создадим несколько категорий, в каждой несколько элементов. И будем изучать элементы по разному.

StudyComplexTest — там есть джавадок(котлиндок?), можно прочитать и понять что там происходит. У нас 3 категории по 3 элемента. Мы проходим первый правильно весь, после чего второй полностью с ошибками и третий комбинируем верные и неверные ответы. После чего проходим по каждой категории еще раз но уже с другими ответами/действиями. И в конце проверяем статистику после смерти процесса. Весьма логично, что в классе юай теста так много строк — 469. Посмотрите сами — ссылка.

Итак, сколько у меня ушло времени на написание этого теста? Пару часов. Сколько по времени занимает этот тест? Полторы минуты. Что мне это дает? Каждый раз, когда я захочу проверить основной функционал моего приложения, я просто запущу этот тест и увижу, что все хорошо. А давайте посмотрим на сам юай тест в действии.

Запись юай теста StudyComplexTest на эмулаторе

Итак, давайте подытожим. Все юай тесты, которые я написал — заняли не так много времени, они проверяют функционал моего приложения и сами занимают менее 10 минут в целом на прохождение. Теперь самый важный момент — я написал код приложения не сильно придерживаясь принципов CLEAN Architecture, плюс я использовал библиотеку Realm для хранения данных. И знаете что? Я могу со спокойной душой рефакторить свой проект, ведь у меня весь важный функционал покрыт юай тестами и я могу легко и просто понять, не задел ли я функциональность приложения своим рефакторингом.

Чтобы не быть голословным я попробую помянть Realm на Room. Ведь от того, какая именно база данных используется в приложении не должно быть разницу конечному пользователю. Он ровно так же должен иметь возможность добавлять, обновлять и удалять как элементы, так и целые категории.

И да, есть еще такой функционал как чатбот, я просто пока что не написал юай тесты на него.

Чат бот

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

п.с. это был мой первый опыт написания чатбота, так что не судите строго код.

Рубрика: Программирование Java | Оставить комментарий

UI-tests in Android. Часть 2. Простые примеры.

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

Немного о проекте — что это и зачем. Году так в 2004 если не ошибаюсь, был у меня компьютер пентиум 4. И еще тогда у меня был интерес к изучению иностранных языков. Помню тогда приобрел программу ABBYY Lingvo 10. На 2 CD. Если кто не знает, это просто словарь. Ты вводил слово на английском или русском и получал перевод. Но кроме основной программы, там был еще и так называемый помощник — Tutor. Его интерфейс выглядел именно так. Есть слово, тебе нужно ввести перевод. Все. Слова для проверки ты брал из словаря или же руками вбивал. Таким образом я в свое время пополнял свой словарный запас.

Сейчас же я написал свой проект, который так же назвал Tutor (link). Суть его точно такая же — добавить в некий словарь свои слова для изучения и потом изучать их. Теперь посмотрим каким образом мне помогали в этом деле юайтесты.

Напомню — Первоочередная цель и миссия юай тестов — проверка функционала!

Для начала посмотрим как выглядит стартовый экран. Основной наш функционал это 2 вещи — добавить в словарь новую пару для изучения и собственно изучить существующие пары слов. Так как я писал пет проект для себя и без всякой помощи, то и дизайнером был тоже я.

Главный экран приложения Tutor

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

Это и есть первый тест-кейс. То есть что такое тест кейс? Это поведение приложения при каких-то условиях. Наш тест кейс будет звучать примерно так —

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

Теперь. Нам как писателям юайтестов вообще неважно какой там код. Мы смотрим на юай и на ресурсы.

Вся суть юай тестов

Если в юнит тестах мы писали проверки типа — assertThat(actual, is(expected)) то в юай тестах наша структура следующая (используем Espresso, кстати оно в градле уже по дефолту есть, ничего не надо добавлять в проект).

onView(VIEWMATCHER).VIEWASSERTION(VIEWACTION/VIEWMATCHER)

Расшифровываю — находим вью (да да, ту самую View андроида) по какому-то матчеру (чаще всего вам нужно найти вьюшку по айди), после делаем с этой вью интеракцию (взаимодействиеум с ней, чаще всего кликаем) — и проверяем действие или значение (например текст вьюшки или состояние — видимо или доступно для нажатия). Чтобы было понятнее я написал класс базового теста в который положил все возможные функции проверки, давайте посмотрим — ссылка.

Большая часть проверок реализована с помощью экстеншн функций Котлина. Это очень удобно, мы позже увидим. Самое простое что мы можем проверить, что вьюшка с каким-то айди видима на экране. Для этого пишем такой простой код —

fun Int.checkViewIsVisible() = onView(withId(this)).check(matches(isDisplayed()))

Перевожу на русский — найти вью с таким айди, проверить, что она видима. Все. Элементарно, правда?

Так же у нас есть возможность проверить, что вью невидима и что ее просто не существует на экране. Также для нашего тест кейса нам нужен метод проверки отображения сообщения. Так как наше сообщение это Toast, то напишем метод проверки checkToast. Очень важно понимать следующую вещь, мы проверяем текст сообщения, какой он есть, а не проверяем что отобразился ресурс с таким-то айди. Почему? Потому что завтра возможно кто-то (например вы) поменяете текст этого ресурса, а чтобы не забыть, что вы использовали такой текст уже где-то, у вас есть юай тест.

Теперь, когда мы разобрались более менее с тем, как писать проверки юай тестов, перейдем к самому тесту (ссылка). Если вы посмотрите на этот тестовый код, то увидите, что он максимально прост и понятен даже человеку, который впервые видит юай тест и даже котлин код. Сначала мы проверяем что видимы все вьюшки, которые принадлежат экрану, после этого кликаем на кнопку изучения и проверяем что видим Тоаст. Готово.

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

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

Итак, это был первый и самый простой юай тест, на который уходит меньше 1 секунды. Стоит ли оно того, чтобы написать его? Мне кажется что да. Ведь вы всегда будете уверены в том, что ваш функционал работает как нужно. Здесь нужно отметить, что у нас в юай тесте есть 2 функции как и в юнит тестах — Before & After. Методы, которые запускаются до теста и после. Нам в нашем тесте нужно удовлетворить условие — мы находимся в состоянии первоначальном, когда нет ни одной записи в базе данных. Для этого перед запуском нашего теста мы будем очищать нашу базу данных (смотрите BaseTest#setUp).

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

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

Экран добавления новой пары для изучения

Сразу скажу, что изначально юай был немного иной, блок ввода первых 2 данных был под блоком имени категории.

Так как я не хочу, чтобы все изучаемы пары клались в одно и то же место, я придумал так называемые категории. В начальном состоянии нам нужно создать категорию, так как нет никаких предустановленных. Значит, у меня будет радиобатоны для выбора категории — создать новую или же выбрать из списка существующих. Чтобы было понятно, тогда когда у нас нет существующих категорий у нас недоступна для нажатия радиокнопка (я решил что так правильно, но я не UI/UX expert, так что могу ошибаться). Теперь, что же будет, когда у нас будет хотя б 1 категория? Тогда кнопка выбора будет доступна для нажатия и при нажатии мы увидим вместо поля ввода имени категории выпадающий список существующих категорий. Чтобы было совсем понятно, вот вам ссылка на разметку этого экрана.

Но будем последовательны. Первый тест кейс который напрашивается на ум — проверить поля ввода на валидность. Да, у нас есть проверка введенных данных — и она на самом деле проста до невозможности — наше требование просто чтобы было введено хотя бы по 1 символу в поля для источника и перевода (кроме конечно же пробелов) и чтобы был введен хотя бы 1 символ для имени новой категории если мы создаем новую категорию, или же был выбран пункт существующей категории. Код можно посмотреть здесь (81 Line).

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

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

Теперь, вот вам ссылка на сам тест. Опять же мы создадим пейдж обьект для этого экрана (ссылка). В ней будем хранить все айди вьюх и тексты сообщений. Ну и после чего напишем наш тест — как видим он достаточно длинный, но опять же меньше 100 строк. Зато он дает нам уверенность в том, что невалидные данные не будут сохранены в базу данных. И на это все уходит всего лишь 16 секунд. Достаточно мало, неправда ли?

Рассмотрим какие методы мы здесь использовали — проверка видимости и невидимости вьюх. Тайпинг в поля ввода — здесь давайте сделаем акцент — посмотрим на метод.

fun Int.typeText(text: String) = 
onView(withId(this)).perform(
    clearText(), 
    ViewActions.typeText(text),
    closeSoftKeyboard()
)

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

Теперь, давайте пробежимся быстро по всем существующим в пакете юайтестам.

AddNewPairAlertDialogTest — проверка окна подтверждения ухода с экрана добавления. Здесь все довольно просто — нужно отменить создание нового элемента и проверить, что ничего не добавилось.

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

AddNewPairExistingCategoryTest — создаем программно 2 категории, выбираем из выпадающей вью вторую и добавляем туда новый элемент, после чего проверяем, что он туда добавился. Здесь мы проверяем функционал добавления айтема в сущестующую категорию причем когда категорий не одна. Т.е. выбор нужной категории.

AddNewPairNewCategoryTest — проверка основного функционала — добавление нового элемента в новую категорию.

А здесь давайте сделаем паузу и порассуждаем. Вот у нас есть поле ввода для новой категории. А что случится, если мы в это поле ввода напишем имя сущестующей категории? Наши элементы все уникальны и их уникальность гарантируется их именем. Т.е. у вас не может быть 2 разные категории в каждой из которых есть слово Hello с разными или одинаковыми переводами. Также сами категории уникальны и их уникальность обеспечивается их именем.

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

AddNewPairReplaceExistingWithNewCategoryTest — Создаем новый элемент в новой категории, после пытаемся создать этот же элемент для другой категории, заменяем существующий и проверяем, что теперь у нас в списке категорий есть 2 значения. Первый с количеством элементов 0, а второй с количеством элементов 1. Где лежит наш уже созданный элемент.

AddNewPairToCategoryAlreadyExistsTest — Здесь мы проверяем, что если написать в поле новой категории имя существующей не будет никаких конфликтов и в категорию добавится еще один элемент (да, я решил что ни к чему юзеру лишний раз показывать сообщение, так как это не критично, позже мы увидим что у нас есть возможность менять любой элемент).

Кто-то скажет, что все эти тесты такие простые, Оганнес, давай что-нибудь действительно интересное. Ок. Мы все знаем, что в андроид есть такой момент как смерть процесса, который происходит когда система решает, что ваш процесс перестал быть суперважным и его можно прибить. А после этого, когда юзер вернется восстановить данные. И это можно протестировать только если в настройках разработчка поставить галку на Не хранить активити(Don`t keep activities) и количество фоновых процессов 0. Так как в моем проекте нет никакой библиотеки для навигации — я написал все старым добрым фрагмент менеджером, значит по дефолту после смерти процесса мы должны увидеть приложение в первоначальном состоянии. Т.е. если вы были на экране добавления новых элементов, то после смерти процесса ваши введенные данные потеряются и вы увидите экран с 2 кнопками (стартовый). Как же нам проверить, что в коде все сделано на совесть и после смерти процесса все будет сохранено? Берем эмулятор, в нем ставим галку на Не хранить активити и пишем наш тест.

AddNewPairRestoreAfterDeathTest — Создаем новый элемент, вбиваем какие-то данные, после чего жмем на кнопку Home тем самым сворачивая приложение, после чего открываем его вновь. Проверяем что все введенные данные были сохранены, жмем сохранить и проверяем, что в списке категорий добавился новый элемент.

Здесь нас интересует метод goHomeAndReturnToApp(), который лежит в классе BaseTest. Суть в том, что у Espresso ограниченный функционал и для таких вещей как свернуть приложение и восстановить из списка недавних у нас есть такой замечательный инструмент как UI-automator (androidTestImplementation ‘androidx.test.uiautomator:uiautomator:2.2.0’)

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

Подведем итог — сегодня мы разобрали 2(3) экрана и простые (относительно) тесткейсы и юай тесты. В следующей статье мы посмотрим на остальной функционал нашего приложения и посмотрим на более сложные юай тесты.

Рубрика: Программирование Android | Оставить комментарий

UI-tests in Android. Часть 1. Что это и зачем

Предположим вы андроид разработчик. И для начала предположим вы пишете новый проект.

Вот вы закончили основной функционал, теперь нужно выкатывать в продакшен/релизить/выпускать обновление в гугл плей маркет. Что вы сделаете? Наверно протестируете ваш функционал, чтобы быть более менее уверенным в том, что все работает корректно. Ок. Предположим вы работаете не в одиночку и у вас в команде есть тестировщик. Тогда на его долю упадет ответственность за тестирование приложения и всего функционала. Предположим этого функционала не так много и он справился за пару часов. А если у вас в команде нет тестировщика, например вы пишете проект в одиночку. Тогда эти пару часов уйдет из вашего времени. Вроде как немного, правда? Что такого, пару часов потратить на то, чтобы быть уверенным, что все сделано на совесть. Но это до следующего релиза или же до первой баги.

Итак, предположим вы выкатываете новый функционал или же внесли некоторые правки в существующий. Что теперь? Вам тестировать не только новые фичи, но еще и старые, верно? Здесь мы предположили, что вы не переписали полностью с нуля все и вся и некоторая часть того, что было в первом релизе осталась и в этом. Значит вам опять тратить несколько часов на тестирование функционала, верно? Ну или тестировщику. Ок. А если это была бага и вы пофиксили ее — значит вам еще тестировать, что она действительно перестала воспроизводиться. А теперь такой вопрос — а что если это был критический баг и вам нужно быстренько залить хотфикс? У вас есть 3 часа на ручное тестирование? Если да, то ок. А если нет? И начальство требует сделать хотфикс как можно скорей? Подождите, шеф, надо потестировать функционал. Некогда! Выпускайте уже! Окей…. И вот оказалось, что ваш багфикс затронул основной функционал и вы опять имеете баг и так бесконечно. Или до тех пор, пока вы решаете перед релизом тестировать весь фунцкионал добросовестно. Значит перед каждым релизом у вас уже будет уходить на X времени больше при каждой новой фиче или багофиксе. Верно? Сначала у вас был 1 экран и 1 фича, потом 2 фичи. И вот прошло 3 месяца и вам уже нужно 3 человека, которые бы тестировали весь функционал приложения целый день. Хорошо, если на проверку функционала уходит 1 день. А если это перерастает уровень HelloWorld, то тогда тесткейсов становится действительно много. Это называется временем регресса. Т.е. момент, когда вы отдаете сборку на тестирование и просто ждете, пока эту сборку протестируют полностью и тогда уже можно выкатывать в гуглплей новую версию. И конечно же бизнесу важно чтобы время регресса было как можно меньше. Ведь время это деньги. А время 5 человек это деньги умноженные на 5.

Итак, юай тесты. Вот наш бизнесмен думает — о боже! Каждый релиз нашего приложения это 3 дня тестирования! Было бы замечательно уменьшить это время до 3 часов хотя бы. Или еще лучше, до 30 минут. И вот вам решение — юай тесты.

Если вы читали про юнит тесты, то вам более менее понятно что такое тесты в принципе. Отличие юай тестов в том, что эти тесты используют… юай! Сюрприз! И для проверки юай тестов вам нужен девайс. Или физический или же эмулятор. Что из себя представляет сам юай тест — джава/котлин код, который пишется 1 раз и его можно запускать перед каждым релизом. А если еще настроить такую замечательную вещь как CI/CD (DEVOPS) то эти тесты будут автоматически проверяться перед каждым релизом. Т.е. представьте на секунду. Вы написали новые фичи и вам нужно релизить — вы просто нажимаете 1 кнопку — все юай тесты прогоняются за полчаса и вуаля — вы прекрасны. Можно выкатывать в продакшн сборку. А еще можно уволить 3 тестировщиков. Ведь они не нужны. Можно оставить в принципе 1 из них или пускай разработчик сам пишет юай тесты. Они пишутся немного долго, но это зависит от тест кейсов (что такое тест кейс наверно отдельной статейкой распишу).

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

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

Надеюсь я вас убедил, что юай тесты нужны. А если нет, то вот вам второй случай. Вы пришли на старый проект где есть уже существующий код. Что вам нужно сделать? Конечно же разобраться в нем. Но увы и ах, вы опять же единственный разработчик, а тестировщика или нет или же он тоже новый и не ведает все прелести приложения.

А начальство вам говорит — разберись в проекте, пофиксь багу и сделай новую функциональность. И здесь начинается русская рулетка. Естественно никаких юай тестов в проекте нет. Ведь если бы они были, ты бы их просто прочитал или запустил и понял бы что к чему. Но вот незадача, раньше был программист, который считал что юай тесты не нужны и не написал ни единого. А тем временем в проекте 100.000 строк кода. Да начнется игра! Ты, как сапер, должен в сжатые сроки понять что к чему, пофиксить багу, которой 3 месяца и допилить функционал. А теперь вопрос, как ты можешь быть уверен, что старый функционал который худо бедо работал ты не задел? Вот именно, вся надежда на новичка тестировщика — т.е. оставь надежду и просто заплачь.

Что бы сделал человек, который знаком с юай тестами? Он бы написал их. Да, на существующий проект написал бы юай тесты, ведь примерно он видит в коде что и как. После этого он бы начал фиксить баги и на них тоже написал бы юай тесты. А после уже новый функционал — и юай тесты на него. Тогда уже он был бы более менее уверен, что не сделал проекту только хуже. Ведь у проекта как никак есть аудитория и они сильно расстроятся, если новый разработчик привнесет в стабильные части проекта новые баги.

Как убедить начальство, что тебе нужно немного времени на юай тесты? Легко!

Вот вы не писали юай тесты, верно? Сколько у вас багов? 38. А если бы писали юай тесты то их бы было гораздо меньше. Как считаете, стоит ли оно того, чтобы потратить немного больше времени на написание кода и юай тестов чтобы после релиза не делать хотфиксы и забыть уже об огромной доле багов, котроые никак не пофиксятся?

И здесь опять можно возразить — у нас стартап, у нас нет времени. Окей. Как же так — времени делать качественно нет, но тратить каждую неделю на багфикс у вас время есть? Поймите наконец — если не обращать внимание на качество работы, то рано или поздно проект превратится в тыкву, в которую просто невозможно будет вносить изменения.

Вот вы убедили начальство, написали юай тесты. Они прогоняются перед каждым релизом за полчаса и вы спокойно пишете новый функционал. А баги становятся все реже и реже. Ведь вы при каждой новой фиче пишете тонну другую ай тестов. Заметьте, сам код (нетестовый) может быть самым худшим в мире, без архитектуры и т.д. Но если ваши юай тесты подтверждают, что все работает прекрасно, вы можете со спокойной душой приступить к такому страшному делу как рефакторинг. ДА! Это еще одно из плюсов юайтестов. Вы пишете юай тесты на существующий проект. После этого рефакторите весь проект и если после этого все юай тесты также зеленые (т.е. проходят), значит вы качественно порефакторили проект.

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

Рубрика: Программирование Android | Оставить комментарий

Clean Architecture — Junit Tests

Итак, третья в серии статья по чистой архитектуре (если вы не ознакомились с предыдущими двумя, то настоятельно рекомендую). Проект доступен по ссылке.

А начнем мы с того, что такое юнит-тесты и зачем они вообще нужны. Если говорить о том, что существует такая практика разработки как TDD — test driven development, то сразу становится понятным — сначала пишем тесты, а после уже к нему код. И на самом деле это то, к чему всем стоит на мой взгляд стремиться. Ведь как оно бывает на самом деле — тебе дают задачу, в которой написаны все тест-кейсы — в таком случае поведение/результат такое, а в таком случае такое. И по хорошему, для того, чтобы быть уверенным в том, что ты все эти условия продумал и написал им удовлетворяющий код — ты пишешь тесты.

Первая и самая сложная вещь для тех, кто никогда не писал тесты — понять что это и как они пишутся. И почему их реально нужно писать. Как показывает мой опыт — при написании тестов после кода они зачастую помогают тебе выявить некоторые мелкие и не очень проблемы кода. Вот ты например написал код и думаешь, что все правильно. Ок. А как проверить? Замучать тестировщика? Чтобы он ручным тестированием проверил, что ты написал правильный код и фича делает именно то, что она должна делать? Вам не кажется, что мы тратим ресурсы в таком случае не самым рациональным образом. Согласен, час работы тестировщика стоит меньше чем час работы программиста, но в любом случае, если вы написали кривой код, то вам опять вернется таска на доработку и опять проверка и по циклу. В конечном итоге вы проиграете в общем времени завершении таски. От начала ее написания в джире до передачи ее в релиз.

Добавим сюда еще такой кейс — вам в уже существующем коде необходимо сделать доработки, но таким образом, чтобы все остальное не затронулось и продолжало работать как и до внесения изменений. А теперь представьте, что у вас не HelloWorld проект, а проект уровня какого-нибудь известного мессенджера или где 100500 фич и экранов. Перепроверять все это дело будет очень затратным по времени и по ресурсам. Сколько нужно тестировщиков, чтобы перепроверить перед релизом все фичи? Это называется временем регресса. Т.е. время, после того как программисты сдали все фичи (влили их в релиз кандидат версию гита) и до момента, когда все тестировщики протестировали все фичи, которые входят в релиз и все старые которые остались с предыдущих. Итак, если вам важно, чтобы это время было минимальным, то надо бы действительно задуматься о написании юнит-тестов.

Еще недавно на одном митапе один человек сказал одну очень хорошую фразу — что такое Legacy код — код, который не покрыт юнит-тестами. И я с этим действительно согласен. Ведь сами подумайте, ты приходишь в проект, где код не покрыт тестами и не можешь понять что в нем происходит вообще. И ты с ужасом вносишь какие-то правки в этот пугающий код на 4к строк и молишься, что ничего нигде не задел, хотя это почти невозможно.

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

Давайте более подробно рассмотрим тесты, написанные для проекта SpaceX. Начнем мы опять же с data слоя. Самый простой класс в нашем случае это DiskLaunchesDataStore. По логике у него 2 метода, первый отдает все значения из кеша для какого-то года, другой дает подробную информацию. Мы смотрим в код и видим, что в конструктор мы передаем кеш LaunchesCache. Значит его можно замокать, чтобы он отдавал нужные нам значения. Мокирование это именно тот нужный нам инструмент, ведь если у вас коде не соблюден принцип SOLID- D, то вы не сможете подменить нужную реализацию вашего класса.

Итак, пишем первый тест-кейс. Предположим наш кеш отдает нам пустой список, значит и наш источник данных тоже вернет пустой список. Все просто и понятно. С помощью мокито библиотеки пишем этот код — when(something.doCode).thenReturn(mockedData). Когда мы будем вызывать метод у нашего замокированного класса, тогда вернуть замокированные данные. Следующий тест мы пишем для случая, если наш кеш вернул 1 данные в списке, тогда мы ожидаем, что и источник вернет нам 1 элемент в списке. Это очень просто и смысла в этом нет, скажет мне сноб, а я отвечу — ок, идем дальше. Метод, который отдает детали. По сути он берет из списка по индексу элемент. И здесь возникают вопросы, а что будет, если наш список содержит 1 элемент или нисколько, а метод пробует взять по индексу 1, т.е. второй. Мы ожидаем исключение и для этого пишем наш тест, который как результат работы проверит выброс исключения. Но чтобы быть уверенным, что этот метод работает нормально в позитивном кейсе, то мы напишем также и позитивный тест — когда в списке 1 элемент и мы берем по индексу 0.

Ок. Это был очень простой тест. Давайте посмотрим на тест для другого источника данных — из облака — CloudLaunchesDataStoreTest. Здесь все намного сложнее. Во-первых нам нужно замокировать несколько классов — менеджер соединения, сервис для получения данных из облака и кеш. Первым делом, мы проверим, что нельзя брать данные из облака для детального экрана. Мы напишем тест, который должен закончиться исключением. Дальше мы должны проверить, что если нет соединения с интернетом, то так же должно выброситься исключение нужного класса. Дальше мы рассмотрим случай, когда что-то пошло не так, во время получения данных из сети.

Далее мы предположим, что соединение с сетью есть, никаких исключений не было выброшено во время получения данных. Но сам ответ от сервера закончился ошибкой, например 404 (или 500 и т.д.). Что мы должны тогда получить? Иключение о том, что сервер недоступен.

А вот вам еще одна ситуация — мы получили ответ 200, но в теле ответа нет ничего, нул. Что будет тогда? По нашей логике мы должны отдать ошибку — сервер недоступен. И уже тогда, когда мы продумали и протестировали все возможные ошибки и случаи, когда что-то пошло не по позитивному сценарию, мы можем начать писать тесты на позитивный и успешный сценарии.

Итак, мы проверяем, что при удачном ответе от сервера нам может прийти пустой список. В нашем случае наше апи говорит что это ОК. Мы мокаем ответ от сервера — пустой список, мокаем манагер соединения, чтоб отдавал тру на вопрос есть ли соединение и все. Готово.

Дальше мы протестируем, что нам пришел ответ от сервера например с 1 элементом в нем и мы должны не только отдать результат Успех, но еще и положить его в кеш. Для этого есть метод у мокито — verify, проверить, что метод у какого-то класса был вызван.

Следующий класс в дата слое — фабрика источников данных LaunchesDataStoreFactoryImpl.

В нем один метод, который принимает на вход всего 2 параметра. По логике, он отдает нужный источник данных исходя из 2 условий, если данные уже лежат в кеше и исходя из входного параметра приоритета. Значит нам нужно написать по крайней мере 4 теста — 2 параметра, влияющих на результат, каждый с 2 состояниями (есть в кеше или нет, приоритет или облако или кеш). И вот мы поочередно мокаем входящие данные и проверяем, что на выход из фабрики источников данных идет нужный источник данных.

Дальше идем в слой домейн. Здесь всего 2 теста. Так как остальные классы слишком простые чтобы на них писать тесты (но вы всегда можете их написать, ведь в боевом проекте тестами нужно крыть все классы, на одной работе условием прохождения функционала было покрытие тестами на 80 процентов, кстати это можно легко узнать с помощью запуска тестов с покрытием — правый клик на зеленой галке — run with coverage).

Первый класс это валидатор года (да да да, по хорошему его надо было уже перебросить в слой презентации). Посмотрим на этот тест. Он принимает на вход строку и отдает 3 значения — null, true, false. По нашей логике, нам не нужно говорить юзеру, что но ввел нечто некорректное, если там всего 3 символа. Поэтому мы напишем тест под этот кейс. В цикле мы проверим все возможные строки меньше 4 символов, они могут содержать цифры, спец.символы и что угодно. Результат должен быть нул.

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

Второй и более важный класс это имплементация главного интерактора — LaunchesInteractorImpl. Он на вход принимает 2 класса в конструкторе, репозиторий и валидатор, значит мы сможем замокать ответы от методов этих классов легко и просто. Хотя ради интереса в этом классе мы обошлись без Мокито. Итак, метод принимает на вход год и отдает 4 статуса в зависимости от того, что отдает наш репозиторий. Значит у нас должно быть минимум 4 теста для всех статусов. Проверяем статус — нет результатов, значит нам нужно чтобы наш репозиторий отдал пустой список. Если нам нужен успешный статус — мы должны от репозитория отдать список с хотя б 1 элементом. Можно заметить, как мы даем на вход в реализацию интерактора нужную реализацию репозитория.

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

Казалось бы, все просто и понятно и зачем писать тесты, но я вас уверяю — они нужны. Завтра добавится еще один статус, или поменяется репозиторий, нужно чтобы сохранились ваши все остальные случаи в рабочем состоянии. А что может быть проще и легче чем просто запустить все тесты на том же CI (continuous integration). Ведь от вас ничего даже не требуется сделать, запушили код в ветку, все тесты автоматически прогнались. Красота!

И наконец посмотрим на тест в модуле презентации. Там у нас из сложных классов MainScreenViewModel. Логика юай такова, что мы вводим в поле ввода какие-то символы и мы должны начать обрабатывать ввод только спустя 300 миллисекунд после начала. Значит нам нужно в тестах это все проверить. Так как у нас здесь корутины и ливдата, то никаких особых сложностей замокировать нужный скоуп и мейн тред нет. Первое что мы сделаем это протестируем случай с невалидным вводом. Так как вьюмодель зависит от интерактора, то мы замокаем его через Мокито. Так же замокаем все ливдаты.

Итак, мы отдаем на вход невалидную строку, точнее заставлям наш валидатор отвечать нам false. После чего повторяем это все дело 10 раз и через полусекундную задержку проверяем, что на условном экране сработали методы от ливдаты — ошибка показалась 1 раз, результатов не было показано нисколько, ровно как и прогресс не начался крутиться. Ведь по нашей логике прогресс начинает отображаться, только если идет запрос в сеть. А если у нас невалижная строка, то и прогресс не должен отображаться в пустую.

Дальше мы тестируем кейс, когда ввод от юзера недостаточен для начала проверки, т.е. менее 4 символов. Это мы имитируем просто вернув от валидатора/интерактора нул. Проверяем, что все ливдаты не были стригернуты — verify (viewModel.someState, never()).method().

Дальше у нас есть кейс с пустыи данными и мхы проверим его. Значит мы возвращем true от интерактора на валидацию и также вернем статус нет результата. Тогда проверим, что прогресс сработал 1 раз со значением true, 1 раз со значением false, т.е. он отобразился и через время исчез. и после чего сработал метод для показа данных.

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

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

И после всего этого конечно же хочется проверить, что наш делей работает корректно. Т.е. мы будто вводим в поле ввода нужное количество символов, после 300 миллисекунд повторяем. Значит методы должны сработать по 2 раза. Для этого в цикле имитируем ввод несколько раз с разными временными задержками. Итого будет ввод и после 400 миллисекунд еще один. И тогда с помощью мокито times(N) можно проверить, что метод вызывался столько раз, сколько мы предположили.

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

Если у вас есть вопросы по данной теме, то я могу на них ответить в телеграме в личке @JohnnySC или же в чате андроид — t.me/android_ru

Рубрика: Программирование Java | Оставить комментарий

CLEAN Architecture — example

Итак, вчера мы рассмотрели в общем и целом что такое чистая архитектура (настоятельно рекомендую к прочтению). Теперь же, перейдем от общих слов к конкретному коду (доступен по ссылке).

О чем проект? Начнем с того, что у спейсХ есть открытое апи и выглядит оно так — https://api.spacexdata.com/v2/launches?launch_year=2019 Можете перейти и посмотреть на джейсон ответа. Апи отдает всю информацию по запускам ракет по годам. Т.е. наше приложение предельно простое, есть поле ввода для года, получаем информацию по запускам ракет и отображаем в списке. Попутно кладем в кеш, но об этом подробнее ниже. Вообще проект писался исходя из примера Фернандо Сехаса — пример чистой архитектуры (ссылка).

Итак, как и вчера, начнем мы с того, что рассмотрим дата слой. В нем у нас 4 пакета — cache, entity, net, repository. Entity это обычный класс, который хранит в себе модель данных от сервера. Да-да, те самые сырые данные, которые мы получаем от сервера и они непригодны ни для чего, кроме как мапинга к удобочитаемым классам доменного слоя. Собственно поэтому и в пакете entity есть мапер. В чем конкретно неудобство сырых данных — в том, что их структура очень некрасивая. Если посмотреть на класс ентити, то можно обнаружить например лишнюю вложенность классов, или же одну кашу для ссылок разного типа — ссылки на википедию, ютуб, картинки и пдф.

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

В пакете net у нас лежат такие простые вещи как ConnectionMangaer, который просто проверяет наличие сети и сам сервис для получения данных от апи спейсХ. В нашем случае мы используем ретрофит, чтобы было все проще.

Далее нас интересует пакет кеша. В нем мы просто кешируем данные, которые получили от сервера в сыром виде. Так как у нас ключевым понятием является год запуска, то будем просто (на время, позже перепишем на Room возможно) класть в кеш SharedPreferences наши данные. Прошу заметить, что можно будет легко подменить реализацию кеша, так как у нас есть интерфейс и есть первая реализация на префах.

И самое интересное у нас в пакете repository. Так как у нас данные могут идти из 2 разных источников, то мы создаем некий интерфейс DataSource, у которого 2 имплементации — получение свежих данных от сервера и из кеша. Для того, чтобы выбрать нужный датасорс, мы написали фабрику источников данных. В которой заложена простая логика — если нам нужно отдать в следующий слой данные запусков ракет текущего года, то мы по возможности будем брать свежие данные от сервера, для всех других годов — по возможности из кеша, если ранее юзер вводил этот год и мы закешировали. Здесь еще нужно подметить, что на уровне дата слоя мы оперируем таким понятием как приоритет — от сервера или из кеша. На следующих уровнях это понятие недопустимо, так как человек, не связанный с кодингом напрямую не должен знать вообще такие вещи как сервер или кеш. Для него более простым понятием является флаг — свежие данные (об этом подробнее в домейн слое).

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

В следующей статье мы рассмотрим юнит тесты на все классы.

Идем дальше, следующий слой доменный, в котором лежат человеку понятные классые и удобочитаемый код. Структура здесь понятна — exception, interactor, repository, validator, data. Как мы и говорили, данные доменного слоя это данные которые удобны для оперирования ими, ровно так же как и исключения. Давайте посмотрим на них. 2 человеку понятных имени исключения — нет соединения с сервером и сервер недоступен. Все. В пакете репозитория лежит только интерфейс репозитория, ведь имплементацию мы оставили в дата слое.

Валидатор в нашем случае просто проверяет входные данные от пользователя. Некоторые спросят, почему он возвращает 3 состояния — true false null. Я заложил такую логику, что если символов недостаточно для проверки на то, является ли строка годом — 4значным числом, то мы и не начинаем ничего валидировать. Ведь согласитесь, зачем проверять строку меньше 4 символов если этого можно не делать. Здесь может возникнуть спорный вопрос. А должен ли лежать этот валидатор года в доменном слое, ведь он по сути проверяет ввод пользователя? Согласен, хороший вопрос. Но давайте пока посмотрим остальной код и решим, надо ли его из доменного слоя убирать или нет.

И самый важный пакет доменного слоя это интеракторы. Здесь тоже много холиваров по поводу наименования, одни решают называть их интеракторами, другие юзкейсами. Третьи считают, что интерактор это имплементация многих юзкейсов. Здесь вопрос стоит так — или в одном презентере/вьюмодели 1 интерактор, в котором множество юзкейсов, или же в одном презентере/вьюмодели несколько интеракторов (интерактор как интерфейс и как имплементация).
Сейчас рассмотрим LaunchesInteractor, потому что остальные интеракторы предельно просты — один показывает все результаты, другой детали запуска. Итак, наш интерактор должен принимать на вход строку года и что же он отдаст следующему слою? Так как слой домена это код, который можно понять любому человеку — статус операции — наше апи говорит нам, что случай когда не было ни одного запуска ракет в каком-то году вполне себе нормальный. Поэтому мы сделаем енам со статусами в котором 4 статуса — нет результатов, успех, сервер недоступен и нет соединения.

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

Заметим, что в проекте используются корутины котлина и скажем спасибо разработчикам за то, что у нас нет колбеков как в джава. Так же не пришлось ради запрос ответа и повторного при случае ошибки пихать огромную либу РХ в наш грубо говоря хеловорлд. Хотя масштаб проекта не важен в нашем случае. На мой взгляд в 2019 году можно обойтись без либы РХ. И данный метод с рекурсией тому доказательство.

Итого, мы отдаем тому классу, который вызовет метод интерактора 4 статуса. Они простые и понятные любому человеку. Так что взлгянем на слой презентации.

Здесь у нас все просто — класс диай (который по сути сервис локатор, да знаю. холивар) и класс Апликейшна, где инициализируем наш диай обьект (аля синглтон в джава). Также мы будем запускать периодическую задачу которая каждые 2 часа будет проверять — есть ли соединение с интернетом и стоит ли девайс на зарядке — тогда она будет пытаться обновить данные по текущему году. Зачем это нужно спросите вы, если в нашем коде и так в первую очередь берутся данные из сети. Вот вам ответ — например вы не заходили месяц в приложение, а когда зашли — не было интернет соединения. Наш воркманагер от гугла за этот месяц пытался обновить данные и если у него это получилось, то вы узнаете о самых новых запусках ракет не имея возможности подключиться к мировой сети.

И самое замечательное в нашей истории то, что благодаря чистой архитектуре, я могу получить мой интерактор в классе воркманагер и просто запустить его в 1 строку. Он сам уже все сделает — получит данные хотя б с 3 попытки и положит в кеш и блаблабла.

Теперь, посмотрим на реализацию юай. У нас сингл активити — мы будем подменять фрагменты для каждого состояния — когда есть результаты — простой список с ними, когда их нет — простой фрагмент с текстом, что нет данных.

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

Весь ввод пользователя происходит в едином тулбаре с помощью вью поиска, который из коробки схлопывается и расхлопывается. Посмотрим детально на класс вьюмодели мейн активити — MainScreenViewModel. Здесь у нас по сути 1 метод, получение данных. Если мы повторили запрос после неудачи, то повторим его имея заранее сохраненный ввод юзера. А для того, чтобы не отправлять на сервер много запросов, мы заюзаем нечто похожее на рх-овый дебаунс. Спасибо еще раз корутинам за это. Просто стартуем блок кода через 300мс, если пришел новый запрос, то отменяем старый. И когда я говорю отменяем, здесь мы его реально отменяем, а не отписываемся от результата как в случае с рх или другой любой асинхронщиной.

Далее происходит банальная вещь, проверяем с помощью интерактора введенную строку на валидность и после этого уже смотрим на результат нашей операции и в соответствии с этим подменяем нужный фрагмент или отправляем на юай сообщение об ошибке ввода. Все предельно просто и понятно. Еще раз поблагодарим гугл за компонент навигации — мы не писали здесь типичный код с фрагментМанагером. А гугловые вьюмодели дают нам такую замечательную вещь как viewModelScope, которая автоматически отменит все jobs корутин в методе onCleared.

И посмотрим на самое интересное в слое отображения — это мапинг данных от домейн слоя к презентационному. Мы от сервера получали огромный джейсон, который более менее исходя из бизнес логики мапили к таким обьектам как картинка, пдф, ссылка и т.д. Теперь же у нас стоит задача это все дело отобразить. Так как многие вещи могут быть нул, то и наш юай будет динамическим. Спасибо на этот раз андроид сдк за ресайклер с его вьютайпами. Смотрим в пакет presentation/details. Там у нас — о боже! — данные юай слоя. Да, это классы, которые хранят информацию именно в том виде, в котором нужно юай слою. т.е. конкретные строки и т.д.

Дальше мы мапим наш один класс от домейн слоя к списку из элементов. Элементом списка может быть отдельная информация по деталям запуска конкретной ракеты — начиная с информации о названии миссии, времени запуска, флага успешного запуска заканчивая медиа данными — картинкам, ссылками на википедию, ютуб, реддит и куда угодно плюс ссылка на пдф (в первом приближении откроем пдф файлик в стороннем приложении, но если у вас есть желание, можно воспользоваться интрументами андроид и отрендерить его для устройст от 5 версии).

Теперь, заключительная цепочка событий для полного понимания что, где и как происходит. На старте приложения инициализируются все необходимые классы для работы приложения, плюс стартует воркманагер.
Юзер вводит год запуска ракет, из searchView который в toolbar эта строка прокидывается в MainScreenViewModel. Если после ввода прошло 300мс и больше, то эта строка идет на валидацию в YearValidator через LaunchesInteractor. Если символов меньше 4, то ничего не происходит, если от 4 и более — валидатор вернет или true или false, где от viewModel стригерится LiveData errorState для ошибок в случае невалидного ввода. Если же введен валидный год, то мы тригерим LiveData progressState и отображаем прогресс. Далее запускаем главный метод у интерактора — fetch. Он в свою очередь проверяет год — если равен текущему, то пытается взять данные с сервера, иначе из кеша при наличии. Все это дело уходит в репозиторий, где в фабрике источников данных LaunchesDataStoreFactory осуществляется проверка наличия данных в кеше или приоритет запроса — из кеша или из облака. После чего вызывается метод получения данных у источника данных — getLaunchEntityList. При удачном получении данных из облака — мы положим данные в кеш и вернем их в репозиторий LaunchesRepositoryImpl, где смапим их к обьектам доменного слоя и вернем их в LaunchesInteractorImpl, где просто проверим на пустоту или получим ошибку. Далее, наша вьюмодель перенаправит нас на фрагмент с результатами поиска SearchResultsFragment. Где своя вьюмодель, с помощью своего уже интерактора получит из кеша данные, которые смапит сначала из слоя данных к слою домейн и потом уже получит просто имя миссии и отобразит в простом списке. Далее, если юзер тапает на любой элемент списка, мы передаем позицию в фрагмент детального отображения LaunchDetailsFragment, где получаем всю детальную информацию по запуску, сначала из кеша берем данные дата слоя, мапим их к домейн слою и после мапим к айтемам ресайклервью LaunchDetailsAdapter. Далее уже каждый вьюхолдер имеет всю нужную информацию юай слоя чтобы отобразить всю информацию.

Давайте посмотрим на скриншоты, чтобы было понятнее.

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

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

Информация об удачном запуске, ссылки и картинки.

Я думал над тем, каким образом отобразить флаг об удачном запуске и остановился на чекбоксе. Для ссылок оставил типичную для веба underlined текст.

Еще немного картинок запуска ракеты.

Так как картинок может быть много, то я решил их как-то разграничить — простым отступом, почему бы и нет.

И теперь, подведем итоги. Все что я написал за условную неделю можно было бы сделать за 2 дня старым добрым говнокодом — god objects activity. Но я попробую доказать в следующей статье, что оно того стоило. Хотя мне кажется и сейчас довольно-таки понятно, что писать на чистой архитектуре стоит этого. Во-первых у вас весь код структуирован. Каждый слой занимается своим делом. Почти все классы содержат максимум 70 строк кода (исключение — маперы, там много преобразований из-за апи). Так же все классы (почти все) я покрыл тестами и в следующей статье мы поговорим о том, почему их нужно писать и как они помогают реально в работе разработчика.

А на этом у меня пока что все. Следите за серией статей. Они будут выходить часто и мне есть что рассказать. Если у вас возникли вопросы или у вас есть пожелания/предложения — прошу в телеграм @JohnnySC. Или же в чат андроид архитектуры t.me/Android_Architecture.

Рубрика: Программирование Java | Оставить комментарий

CLEAN ARCHITECTURE — overview

Сразу предупрежу — это вводная статья на тему чистой архитектуры. Здесь вы не найдете все ответы на ваши возможные вопросы. Возможно в последующей статье — планируется серия.

Итак, наверно каждый андроид разработчик слышал про такое понятие как чистая архитектура (Clean Architecture). Если вы не слышали до сих пор, значит вы, скорей всего, живете в бункере с 2012 года.

Ранее мы говорили о таких патернах архитектуры как MVC, MVP, MVVM. И кто-то может сказать — у нас есть патерны архитектуры, мы разделяем наш код на 3 класса — model, view и что-то еще. И это первое самое большое недопонимание. Патерны архитектуры MV* являются лишь способом выстроить логику для отображения. Если вам кажется, что вам достаточно 3 классов для того, чтобы ваш код работал, то да, возможно это так. Но ведь в принципе достаточно и 1 класса — hello god object Activity. Дело в том, что если мы будем писать весь код, который отвечает за данные в одном классе, то он будет слишком громоздский.

В чем проблема больших классов? Во-первых их сложно читать. Во-вторых сложно вносить правки. В-третьих, чем больше кода, тем сложней писать тесты на класс.

И здесь мы дадим некоторое определение чистого кода (если вы не читали книгу Роберта Мартина — Чистый код, то сейчас самое время). Чистым можно считать тот код, который легко читать (названия классов, методов, переменных настолько понятны, что даже джавадоки не нужны, а комментарии тем более), нетрудно понять суть, можно легко вносить изменения не боясь ошибок, которые могут возникнуть в другом месте — т.е. слабосвязанный код. Плюс конечно же класс должен быть тестируем. И не стоит забывать о таких принципах разработки как SOLID, DRY, KISS, YAGNI.

И этого всего недостаточно. Ведь если в патернах MV* мы разделяли все на 3 слоя — отображение, данные и логика отображения, то в рамках CLEAN architecture мы будем разделять весь код на условные 3 слоя — data, domain, presentation. И самое сложное на мой взгляд во всей этой истории — понимание этих слоев. Кто за что отвечает.

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

Главное для каждого разработчика, на мой взгляд, это найти такое решение в плане архитектуры, которое самым удачным способом подходит в рамках конкретной задачи/проекта. Многие люди узнав о чистой архитектуре пишут условные хеловорлды не за 5 минут, а за 50. Продуктовой ценности в этом конечно же не так много. Плюс затраты по времени не оправданы. Поэтому — как сказал один умный разработчик (Сонмез) — не будьте религиозны по поводу технологий.

Теперь, давайте попробуем разобраться в этих понятиях 3 слоев. Data — данные. Здесь вроде как все понятно, но нет. Дело в том, что термин данные имеет слишком широкий круг использования. Мы употребляем этот термин повсеместно. И поэтому нужно уточнение. Что есть данные в чистой архитектуре. Ведь данные действительно разные — данные пользовательского ввода, данные полученные от сервера, данные промежуточного слоя (некий буфер данных).

С первым видом данных все вроде понятно — данные пользовательского ввода. Так как они получены непосредственно пользователем, то мы поговорим о них позже, не наш случай. Данные среднего звена — тоже не то (об этом позже). Данные для нашего слоя data мы будем считать те данные, которые пользователь по сути и не видит. Например данные, полученные от сервера в сыром виде, данные полученные от локальной базы данных, данные (далее информация) любого рода, которые не видны конечному пользователю в том виде, в котором они получены. Также к слою данных можно отнести такие вещи как ошибки, в том виде, в котором они пользователю не особо интересны, плюс конечно же они не удобочитаемы для рядового разработчика или человека со стороны, который случайно открыл ваш проект (например ошибка сетевая 404, 500, любые наследники Throwable).

И на этой ноте давайте сакцентируем внимание. Следующий слой это domain. В простонародии мы называем его — слой бизнес логики. И это наверно самый сложный к пониманию слой. Ведь если с данными более менее понятно — данные из сети, базы данных, их ошибки в чистом виде (джава Exception), то что такое бизнес логика и что есть данные бизнес логики? Удобочитаемый код даже для нетехнических профессий. Мне кажется это могло быть одним из лучших определений домейн слоя. В слое бизнес логики не должно быть ничего из всего того, что вы написали в дата слое. Никаких упоминаний баз данных, сетевых запросов и их ошибок. В домейн слое должны быть максимально дружелюбные и удобочитаемы для человека (нетехнического) классы и методы.

Для примера возьмем любое приложение. Наш продакт оунер( или кто у вас отвечает за бизнес требования) говорит — нужен экран, на котором мы покажем пользователю все его сообщения. Также в случае ошибок — ошибки. Предположим мы упростили наш экран до 2 видов ошибок — нет соединения с интернетом и сервер недоступен. Здесь мы не будем говорить о том, что нет соединения с интернетом ни через 4г ни через вайфай, это неважно. Нет соедиения и точка. Каким образом мы все проверили — все в дата слое. Наш дата слой отвечает за это — какими способами мы проверяем наши данные о наличие соедиения с интернет. И здесь самое время сказать о таких вещах как маперы. Ведь с одной стороны у нас данные от устройства об отсутствии сети или того, что сервер недоступен (ошибки 400, 500), а с другой стороны нам нужны ясные и понятные классы для этих вещей. Тогда мы создаем классы бизнес логики под это дело и пишем мапер, который исходя из логики данных будет предоставлять нам тот или иной класс бизнес логики — ServiceUnavailable, NetworkConnection, etc.

То есть человек, который будет читать ваш код, пусть даже не разработчик, должен видеть нечто такое — получаем сообщения, можем получить или их или ошибки 2 видов. Ему должно быть все понятно. Именно поэтому я и придерживаюсь того, что юнит тесты может писать на ваш код кто-то другой, например тестировщик. Ведь код понятен без лишних слов. Все классы и методы названы просто и понятно. Класс сообщения, в котором такие вещи как тело сообщения, флаг доставлено/прочитано собеседником, плюс время отправления в нормальном виде, например ISO8601, а не миллисекунды, которые могут быть в дата слое (предположим вам сервер отправил время в миллисекундах, не знаю).

Чтобы суметь отделить зерна от плевел — бизнес логику от всего остального(спойлер — логика отображения) — нужно просто внимательно послушать человека, который пишет вам таску. Получить сообщения и показать их или же ошибки 2 видов. Все. Каким образом например вы покажете время отправления или же ошибку (полноэкранным сообщением или всплывающим) — это уже не бизнес логика, а логика отображения. Понятийно у вас есть такое существо как сообщение и у него есть несколько ингредиентов. Каким образом эти поля наполняются — вопрос к маперу, который неважно откуда возьмет эти данные (от сервера или из кеша) и проверив их валидность создаст модель бизнес логики или же породит ошибку. Кстати, ровно так же нам неважно на уровне бизнес логики каким образом отобразим ошибку и какой у нее будет текст,стиль и т.д. это все логика отображения.

Итак, вроде разобрались, у нас есть данные откуда-то там в каком-то непонятном для простого человека виде в слое дата, которые мы с помощью маперов трансформируем в удобочитаемый код для нетехнарей. Дальше нам осталось сделать одну простую вещь — показать что-то в каком-то виде пользователю. И это наш presentation слой. Вот здесь уже пишем наши любимые viewModel, activity/fragment. Но опять же нужно из тех классов, которые хранят данные бизнес логики перебросить в слой презентационный. И опять пишем маперы. Маперы от слоя бизнес логики к слою презентации простые — например если нам пришла ошибка об отсутствии соединения с интернетом — мы порождаем некий класс, который хранит в себе (например) строчку (на разных языках, это же андроид) в которой сам текст ошибки. Может быть еще и изображение какое-то. Этим всем занимается вьюмодель или презентер, что там у вас. Он отдаст в активити/фрагмент ваше сообщение — R.string.connection_error, а там уже вы с помощью Toast или наследников View отобразите юзеру эту ошибку.

Это была первая в серии статья про чистую архитектуру. В последующей мы рассмотрим конкретный пример, который я выложил по этой ссылке.

Если у вас есть вопросы и/или пожелания к последующей статье (например вы бы хотели более подробно что-либо из чистой архитектуры рассмотреть или же у вас специфичный случай и вы не знаете к какому слою относится эта логика) то можете написать мне в телеграм @JohnnySC

Рубрика: Программирование Android | Оставить комментарий

Архитектурный патерн MVVM

Ранее мы рассмотрели такие патерны архитектуры как MVP MVC и теперь, время поговорить о таком патерне как MVVM. Несложно перейти на википедию и понять, что она расшифровывается как Model View ViewModel. Ровно так же как и MVP/MVC — модель, вью и что-то еще.

Давайте для начала поговорим о том, почему вообще все эти патерны существуют. Для простоты ограничимся платформой андроид (или любой другой мобильной). Вся суть разработки под мобильную платформу сводится к решению одной простой задачи — синхронизировать то, что видит пользователь с теми данными, которые он вводит или же получает извне. Собственно поэтому у нас и 3 понятия — модель данных, вью и чтобы как-то их вместе связать нам нужен третий компонент. В одном случае мы назовем его презентером, в другом вьюмоделью. В третьем… как собственно захотите. Суть от этого не поменяется.

Итак, начнем мы с того, чем по сути отличается MVVM от MVP. И это не только название третьего класса, нет. Здесь есть одна изюминка. Если посмотреть на страницу википедии, то там ясно обозначен один термин — связывание данных — a.k.a. DataBinding. В windows-подобных системах патерн MVVM имел больший успех именно из-за специфики платформы — очень удобное связывание данных с отображением. Т.е. каким-то образом задается правило по которому отображение связывается с данными. Например — пользовательский ввод сразу же влияет на некоторые данные и ровно так же изменение этих данных извне напрямую влияет на пользовательский интерфейс.

Почему же MVVM, скажем так, в последнее время заинтересовал Google и огромный процент андроид разработчиков? Для этого давайте вспомним некоторые особенности реализации MVP и такую забавную штуку как жизненный цикл компонентов андроид. Кто-то скажет, что под это дело было создано Moxy, который занимался именно актуализацией отображения при смене состояния отображения. Но, как вы все хорошо помните и понимаете, все это происходит очень больно и мучительно, ведь от такой страшной вещи как смерть и восстановления процесса никто не застрахован. И следственно этот редкий (на самом деле не такой уж и редкий) случай привносил и привносит в жизнь андроид разработчика немало счастья.

Плюс давайте вспомним, что для того, чтобы связать презентер с отображением мы писали отдельный интерфейс для вью и давали его на сьедение нашему презентеру. С одной стороны связь довольно слабая, но с другой она все равно существует. И если вы не фанат Moxy, то вам самостоятельно нужно разруливать все страшные события жизненного цикла. Только вот от одного события наша всеми любимая библиотека не сильно спасает — когда мы пытаемся поменять фрагмент в активти после того, как было вызвано сохранение состояния. Некоторые гениальные разработчики читая джавадоки решают сделать commitAllowStateLoss и верят что у них все замечательно. Но нет. Ладно, оставим эту боль и перейдем к божественному решению.

Как всем известно, в последнее время сообщество андроид разработчиков просит корпорацию добра иметь свое мнение и видение в разработке и поэтому отважные разработчики из Google представили свое решение проблемы, которая мучает всех последние дцать лет. Оно входит в Android Architecture Components и в простонародии имеет название ViewModel от Google. Конечно же сами эти вьюмодельки всего лишь классы, которые можно получить напрямую из активити/фрагмента с помощью статических методов провайда. Т.е. первое отличие вьюмодель от презентера в том, что нет необходимости в лишнем интерфейсе вью. Плюс, так как они лежат в статической области — они будут жить своей жизнью, вне зависимости от того, что происходит с нашим активити/фрагмент. Как же тогда вьюмодель узнает обо всех этих изменениях состояния? Для этого наши гении придумали такую интересную вещь как lifeCycleObserver, которая работает в купе с офигенной вещью как LiveData. Живые данные. Вполне себе интересное название, так как живые они просто потому что, мало того, что они не умирают, но еще и умеют реагировать на все события жизненного цикла компонетов андроид. Т.е. тогда, когда активити перейдет в состояние видимости — все события из ливдаты, которые были готовы, но тогда наш экран не был в видимом состоянии (onResume) — будут благополучно доставлены пользователю. Т.е. вот таким вот простым способом мы обходим ручное разруливание всех событий компонентов.

Еще одной гениальной особенностью ливдаты является то, что отдавать данные ей можно из любого потока. Т.е. никаких больше runOnUiThread, Handler и т.д. Стоит добавить еще сюда замечательную интеграцию с котлин и в частности с корутинами. Если вы пишете без корутин — у вас свой велосипед (или тот же хайповый рх), то вам нужно заботиться о таких вещах как очистка потоков, отмена запущенных асинхронных работ (точнее отписка от результата). И под это дело в вьюомоделях от Гугл есть метод onCleared. Только вот если у вас котлин корутины, то вы во-первых получаете скоупвьюмодели, который автоматом очистится в этом методе, плюс вам не нужно писать руками скоупы своих корутин.

Немного личного мнения и истории. Ранее я писал как и многие — MVP/Moxy, Rx, Dagger, Java. И очень скептически относился ко всем нововведениям. Как никак боязнь всего нового, плюс зачем учить/переучиваться, когда у тебя устоявшийся стек. Но как показывает практика — сменив свой стек полностью я ни о чем не жалею. Сейчас я пишукод, используя все блага от Гугла — MVVM/LiveData, Kotlin-coroutines. Добавим сюда навигацию из архитектурных компонентов и воркманагер — вуаля. Больше никаих сервисов, ошибок из-за того, что вы пытаетесь поменять фрагмент в неудобном для системы месте и т.д.

Да, забыл сказать о датабиндинге. В андроид он тоже есть. Это когда вы в хмл файле своего фрагмента/активити пишете код… Выглядит это непривычно и многие не пользуются этим. Наверно дело привычки. Но в любом случае — главный пойнт мввм у нас тоже в андроид есть — связывание данных. Насколько все прекрасно работает я не знаю. Нужно пробовать.

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

Если у вас есть вопросы или замечания или предложения или просто хотите обсудить это все дело со мной — прошу в телеграм — @JohnnySC

Рубрика: Программирование Android | Оставить комментарий

Design Pattern Visitor — Посетитель

Одиннадцатый шаблон проектирования в серии.

Посетитель — суть в том, чтобы определить операцию над группой обьектов, без изменения кода в каждом из обьектов.

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

От слов к делу.

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

Для начала выделим интерфейс продукта

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

interface Good {

    var quantity: Int

    fun getName(): String

    fun getPrice(): Int

    fun getDescription(): String
}

Создадим 3 товара, которые реализуют данный интерфейс. Здесь для упрощения работы выделим абстрактный класс, который по умолчанию хранит 0 штук товара.

abstract class AbstractGood(override var quantity: Int = 0) : Good
class Telephone : AbstractGood() {

    override fun getName(): String {
        return "Telephone N1"
    }

    override fun getPrice(): Int {
        return 10
    }

    override fun getDescription(): String {
        return "Simple telephone for home and office"
    }
}
class MediaPlayer : AbstractGood() {

    override fun getPrice(): Int {
        return 5
    }

    override fun getDescription(): String {
        return "MediaPlayer with USB, supports all well-known audio formats"
    }

    override fun getName(): String {
        return "MediaPLayer N2"
    }
}
class Radio : AbstractGood() {

    override fun getName(): String {
       return "Radio N3"
    }

    override fun getPrice(): Int {
        return 2
    }

    override fun getDescription(): String {
        return "Portable radio station, supports all MHz"
    }
}

Определив наименования, цену и описание товара нашишем интерфейс посетителя, который будет иметь методы под конкретные виды товара.

interface Visitor {

    fun visitTelephone(telephone: Telephone): String

    fun visitMediaPlayer(mediaPlayer: MediaPlayer): String

    fun visitRadio(radio: Radio): String

    fun incrementQuantity(good: Good)
}

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

Взглянем на реализацию интерфейса.

class GoodVisitor : Visitor {
    
    override fun incrementQuantity(good: Good) {
        good.quantity++
    }

    override fun visitTelephone(telephone: Telephone): String {
        return "Telephone. \n" + toString(telephone)
    }

    override fun visitMediaPlayer(mediaPlayer: MediaPlayer): String {
        return "MediaPlayer. \n" + toString(mediaPlayer)
    }

    override fun visitRadio(radio: Radio): String {
        return "Radio. \n" + toString(radio)
    }

    private fun toString(good: Good): String {
        val price: Int = good.getPrice()
        return "Name: " + good.getName() +
                ", description: " + good.getDescription() +
                ", quantity: " + good.quantity +
                ", price: $" + price +
                ", total: $" + good.quantity * price
    }
}

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

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/goodsRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@id/calculateCheckButton"
     app:layoutManager="android.support.v7.widget.LinearLayoutManager" />

    <Button
        android:text="show check"
        android:id="@+id/calculateCheckButton"
        style="@style/Base.Widget.AppCompat.Button.Borderless"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true" />

</RelativeLayout>

Накидаем адаптер и вьюхолдер для отображения товаров.

class GoodViewHolder(view: View, private val clickListener: GoodClickListener) : RecyclerView.ViewHolder(view) {

    private val nameTextView = view.goodNameTextView
    private val addButton = view.addGoodButton

    fun bind(good: Good) {
        val text = "${good.getName()}\n$${good.getPrice()}\n${good.getDescription()}"
        nameTextView.text = text
        addButton.setOnClickListener { clickListener.onGoodClick(good) }
    }
}

interface GoodClickListener {

    fun onGoodClick(good: Good)
}
class GoodsAdapter(private val goods: List<Good>, private val clickListener: GoodClickListener) : RecyclerView.Adapter<GoodViewHolder>() {

    override fun onCreateViewHolder(viewGroup: ViewGroup, position: Int): GoodViewHolder {
        val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.good_layout, viewGroup, false)
        return GoodViewHolder(view, clickListener)
    }

    override fun getItemCount(): Int {
        return goods.size
    }

    override fun onBindViewHolder(holder: GoodViewHolder, position: Int) {
        holder.bind(goods[position])
    }
}

Здесь все просто как 2*2. Текстовое поле, кнопка для добавления и разделитель на дне каждого элемента.

И приступим к самому важному. Коду главной страницы.

class MainActivity : AppCompatActivity(), GoodClickListener {

    private val telephone = Telephone()
    private val mediaPlayer = MediaPlayer()
    private val radio = Radio()

    private val goodVisitor = GoodVisitor()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        goodsRecyclerView.adapter = GoodsAdapter(getGoods(), this)

        calculateCheckButton.setOnClickListener {
            val check = goodVisitor.visitTelephone(telephone) + "\n" +
                    goodVisitor.visitMediaPlayer(mediaPlayer) + "\n" +
                    goodVisitor.visitRadio(radio)
            Toast.makeText(this, check, Toast.LENGTH_LONG).show()
        }
    }

    override fun onGoodClick(good: Good) {
        goodVisitor.incrementQuantity(good)
        Toast.makeText(this, "Good added", Toast.LENGTH_SHORT).show()
    }

    private fun getGoods(): List<Good> {
        return listOf(telephone, mediaPlayer, radio)
    }
}

Здесь мы используем котлин синтетик для простого доступа к вью элементам. Создаем категории товаров простым путем (здесь не будем углубляться в чистоту решения, без репозиториев и т.д.).

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

Главный экран с товарами.
Нажаие на кнопку +
Нажатие на кнопку отображения чека.

В данном случае можно было бы сделать абстрактный класс, указав ему поля и переопределив например метод toString(). Но опять же, цель у нас была показать как обойти множественное наследование применив для этого шаблон проектирования Посетитель.

Рубрика: Программирование Android, Программирование Kotlin | Оставить комментарий

Шаблон проектирования Посредник — Mediator

Десятый шаблон в серии Design Patterns.

Посредник — суть в том, чтобы иметь слабую связь между абстрактными объектами и иметь возможность переопределять конкретные реализации, причем иметь явную связь и инкапсулированное взаимодействие.

Все станет ясней, когда мы перейдем от слов к коду.

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

Я же решил придумать иной пример. Предположим у нас некая игра, в которой участвуют несколько игроков. У каждого есть определенное количество единиц здоровья и сила, с которой он может ударить других участников. И при каждом его ходе он атакует всех, кроме себя (здесь кстати можно еще разделить участников на группы и не атаковать «своих»).

Как мне кажется для этой ситуации данный шаблон вполне подходит. Давайте рассмотрим главный интерфейс посредника.

Здесь у нас 1 метод отправки (атаки), где мы укажем объем урона и игрока, который его инициирует. Рассмотрим абстрактный класс пользователя.

Класс отправитель или пользователь имеет в себе ссылку на посредника, которую задаем через конструктор. Также имеем конкретный метод отправки (атаки), в котором указываем только размер урона и вызываем аналогичный метод у посредника, указывая текущего пользователя. Но кроме того, что наш игрок отправляет событие (атакует), он также получает урон, но мы объявляем этот метод абстрактным.

Теперь у нас есть слабая связь между посредником и юзером. Нам осталось создать конкретного посредника, где мы определим метод отправки события (атака) и создать конкретный класс игрока, где мы будем получать урон.

Рассмотрим класс посредника.

Здесь у нас список игроков, метод добавления игрока в список и самое интересное, имплементация метода отправки(атаки), мы проходим через весь список и если мы имеем дело не с тем игроком, который вызывает этот метод, то получаем урон.

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

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

А теперь рассмотрим конкретного игрока.

При создании передадим игроку всю информацию и при получении урона (receive) будем уменьшать здоровье (decreaseHealth).

Теперь, чтобы это все посмотреть в действии давайте создадим экран со списком участников.

Нам нужен адаптер для списка.

При создании адаптера просто передадим список данных участников. В конструкторе создаем посредника, наполняем игроками. Когда игрок будет атаковать других будем вызывать метод send и передадим объем урона, после чего просто обновим экран с данными.

Элемент с данными игрока выглядит так

Отобразим имя игрока и какой у него урон, сколько осталось здоровья и кнопку, по которой он атакует других. Если у игрока не осталось здоровья, то отображаем что он мертв и при нажатии на кнопку атаки ничего не должно произойти (можно бы было просто убирать мертвых из списка).

Главный экран выглядит максимально просто. Вот разметка

И сам код

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

Что интересно, если бы игроки атаковали снизу вверх, все равно в живых бы остался игрок номер 2.

Итак, плюсы шаблона

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

Минусы — некоторая сложность понимания в начале, потому как у главного класса вызывается метод, который дергает метод у посредника, а тот уже в свою очередь выполняет вызов другого метода у главного метода с некоторыми условиями.

 

Рубрика: Программирование Android, Программирование Java | Оставить комментарий

Шаблон проектирования Шаблонный метод — Template method

Итак, у нас девятый по счету шаблон из серии Design Patterns.

Шаблонный метод — суть в том, чтобы выносить некоторый шаблонный метод в отдельный класс, а конкретные реализации составных методов оставлять наследникам.

Рассмотрим типичный пример. Вам нужно при каждом нажатии на кнопки ходить на сервер и получать данные. Во-первых, нужно проверить наличие интернета. Во-вторых обработать неудачный случай (которых может быть много на самом деле). Ну и предположим что у вас более менее одинаковые ответы от сервера и нужно отобразить пользователю данные.

Перейдем от слов к коду.

Напишем некий абстрактный класс, который в себе инкапсулирует эти одни и те же действия (алгоритм обработки запросов в сеть — собственно шаблон).

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

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

Посмотрим на интерфейс помощника

Простенький интерфейс, который мы заимплементим в активити.

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

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

Итак, давайте для покрытия всех возможных случаев создадим 2 наследника от абстрактного класса — класс для избранных треков и например топ 100.

Здесь мы будем имитировать загрузку данных с сервера, поэтому просто захардкодим некоторые данные и будем отображать их с некоей задержкой в 2 секунды.

Рассмотрим второй класс, где просто вернем информацию, что не удалось загрузить.

Здесь все просто, в методе получения данных просто возвращаем false. Будто наш сервер не вернул то, чего мы от него ожидали, а например ошибку.

Теперь, чтобы все это протестировать нам нужен некий экран с 2 кнопками для избранных треков и топ100. Также будем отображать избранные списком.

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

И наконец посмотри на то, как это все работает.

Создаем 2 экземплара наших классов, передаем им интерейс помощника. При нажатии на кнопки просто вызываем у них шаблонный метод. Вся логика и алгоритм остаются инкапсулированными в абстрактном методе.

На выходе имеем такие скриншоты.

Сначала нажимаем на получение избранных треков, видим прогресс. После чего отображаются все треки (при условии наличия интернета).

После чего нажимаем на топ 100 и получаем ошибку.

Теперь выключим интернет и попробуем опять. Получаем ошибку.

Плюсы шаблона — позволяет выносить и инкапсулировать одни и те же повторяющиеся последовательности методов в отдельный класс.

Минусы — зоопарк начинается тогда, когда кодер пытается использовать его для отличающихся алгоритмов и пихает всякие разные проверки, от чего алгоритм превращается в нечитаемый грязный код.

Рубрика: Программирование Android, Программирование Java | Оставить комментарий