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 | Оставить комментарий

Шаблон проектирования Итератор — Iterator

Рассмотрим восьмой шаблон в серии Design Patterns.

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

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

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

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

Сначала обозначим все возможные типы продуктов

У нас будет 4 фильтра — ноутбук, смартфон, телевизор и все продукты.

Далее создадим моделю данных для продукта с 2 полями — тип и имя.

И для простоты создадим некий класс синглтон с некоторыми данными.

10 элементов: 3 ноутбука, 5 смартфонов и 2 телевизора.

Теперь перейдем к самому шаблону. Нам нужен интерфейс итератора.

Итератор должен делать 2 вещи — проверять что есть еще элемент и получать этот следующий элемент.

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

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

Рассмотрим детальней имплементацию итератора.

На вход получаем тип, когда перебираем коллекцию то смотрим, чтобы этот тип совпадал с очередным элементом коллекции или же если тип ВСЕ то пусть вернет нам true, если же совпадения не было, то увеличиваем индекс и идем дальше.

Когда получаем следующий элемент также увеличиваем позицию. Вот и все.

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

Здесь у нас 4 кнопки для фильтров и сам список продуктов.

И наконец сам код главного экрана.

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

В итоге имеем такие скриншоты.

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

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

Шаблон проектирования Стратегия — Strategy

Рассмотрим седьмой шаблон из серии Design Patterns — стратегия.

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

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

Перейдем от слов к коду. Напишем интерфейс стратегии расчета стоимости (например бутылки кока-колы неважно).

Метод получает цену на вход и отдает некоторую обработанную (например уменьшенную за счет скидки).

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

Никаких изменений, что пришло на вход то и вернем.

Также создадим 2 скидочные стратегии — с большой скидкой (10%) и маленькой (5%)

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

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

Здесь текстовая информация о скидках, поле ввода для количества и текстовка вывода. Рассмотрим конечный класс.

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

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

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

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

Шаблон проектирования Интерпретатор (Interpreter)

Рассмотрим шестой в серии шаблон — интерпретатор.

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

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

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

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

Для начала нам нужен некий интерфейс выражения.

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

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

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

Далее создадим 2 простых оператора — прибавление и вычитание.

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

Аналогично для вычитания.

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

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

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

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

И наконец рассмотрим конечный код.

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

Посмотрим детальней сам метод интерпретации.

Будем предполагать, что все наши выражения (числа и операторы) разделены пробелом, потому и преобразуем текст ввода в массив строк. Под него создадим такой же массив для выражений. Проходим циклом по всем элементам и если мы наткнулись на число (не оператор), то сохраняем его в виде числового выражения (nwe Number(items[i]). После чего проходим еще раз циклом и находим все операторы, после чего в результат сохраняем то, что было интерпретировано оператором.

Глянем скиншоты и прольем ясности.

Итак, вводим 5 + 3 и жмем на кнопку, в коде будет следующее — сначала находятся числа 5 и 3 и записываются в ячейки массива, после чего повторным циклом находится оператор «+» и высчитывается сумма располагающихся слева и справа от индекса выражений.

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

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

Минусы — сложность написания действительно хорошего интерпретатора.

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