Как работать разработчиком и получать удовольствие

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

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

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

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

  1. Первым пунктом является факт заинтересованности разработчика в том проекте/продукте, который он разрабатывает. Было время я разрабатывал продукт, которым просто не мог пользоваться сам, оно было для рынка Азии. И именно поэтому весь фидбек я получал только от самих пользователей. Или же, например, те же самые продукты, которыми пользуешься не так часто, тогда твой интерес в нем не так сильно выражен. Если брать позитивный опыт, то тот продукт, который я сейчас разрабатываю — я им пользуюсь наверно каждый день. И не только андроид версией, но и веб. И мне естественно интересно не только как разработчику чтобы продукт был на высоте, т.е. работал стабильно и развивался, но и как пользователю. Здесь я могу влиять немного на сам продукт, выражая свое мнение как пользователь. И это прекрасно. Поверьте мне, когда ты заинтересован в проекте, ты разрабатываешь с большим интересом. И даже если взять в расчет пункт с зарплатой, то поверьте, за большие деньги ты будешь кодить какое-то время, но потом, из-за того, что у тебя очень слабая заинтересованность в конечном продукте, ты устанешь, будешь вялым и твоя мотивация будет на дне. Так что, друзья мои, первым делом ищите компанию, в которой вам будет интересно разрабатывать продукт, если повезет, то приложение, которым вы бы пользовались очень часто. Тогда у вас будет четкое понимание насколько оно удобно для использования и насколько качественно сделано. Ведь нет на свете лучшего тестировщика, чем пользователь. А когда это и разработчик в одном лице, то и значит, что баги будут фикситься в кратчайшие сроки и сам багофикс не будет раздражать, а лишь радовать успешная стабилизация.
  2. На второе место я наверно все же поставлю процессы разработки. На собеседованиях очень мало (если не нисколько вообще) внимания уделяется вопросам процессов. А на самом деле это наверно самый важный пункт после заинтересованности человека. Ведь исходя из того, насколько у вас хорошо выстроены процессы разработки в компании/команде зависит общая атмосфера на работе и в том числе успешность конечного продукта и следовательно ваше психологическое состояние. Ведь представьте, если вы живете на седьмом этаже и каждое утро вам нужно тратить по 10 минут только для того, чтобы вызвать лифт, то вам рано или поздно надоест это все дело и скорей всего, если ничего не изменится, вы просто найдете квартиру на первом или втором этаже. Итак, что же не так с процессами разработки. А как показывает практика, в большом количестве компаний он не выстроен вообще или же выстроен для галочки или же выстроен таким некорректным образом, что толку не особо много. Что же такое процессы разработки, спросите меня вы? Все. Вся ваша работа. Потому что кроме написания непосредственно кода, наш разработчик проходит (12 кругов ада) несколько этапов, перед тем как приложение выйдет в релиз. Все начинается с того, что возникает потребность в некоем функционале. Дальше необходимо формализовать это дело. Продумать так называемые тест кейсы, продумать влияние этого функционала на другие. Рассмотреть сроки реализации, подготовить приличный дизайн (и я не только про макеты от дизайнера). Очень часто упускается из виду такая вещь как логика UI. Которую можно отнести к такому понятию как UX — user experience. Будет ли удобно пользователю использовать этот новый функционал или же он через 2 минуты после обновления вам напишет гневный отзыв в гугл плей маркет? После всего этого необходимо, определив сроки исполнения, приступить к самой разработке и здесь очень важно четкое и ясное понимание что требуется от каждого участника команды. Я не верю в T-shape или так называемых разнорабочих, которые и жнец и на дуде игрец. Нет, каждый должен делать то, в чем он хорош. Разделение труда, потому что ты разделяешь не только труд, но и ответственность. Мы говорим про процессы разработки и надо упомянуть такой важный аспект, как тестирование функционала перед релизом. Как показывает мой опыт, в большинстве компаний это самое слабое место. Меня поражает как в иной раз начальство решает выпускать сырой продукт и после поражается гневным отзывам пользователей. Здесь можно долго еще говорить, но в кратце проблема именно в этапе тестирования. Оно либо очень короткое и чисто для галочки, или же его в принципе нет и разработчик сам тестировщик. Так же стоит упомянуть такую вещь как DevOps. Я работал в компаниях, где до сих пор разработчик руками генерит артефакт и отдает через телеграм на тестирование начальству. На этом пункте можно долго еще оставаться, но пойдем дальше.
  3. Кадры. У меня была дилемма, поставить их на второе место или все же на третье. Но по сути эти 2 пункта очень сильно связаны, если не сказать, что одно целое. Итак, кадры решают все. Я видел много людей, поверьте мне, за 3 года и 5 мест работы я повидал как самых настоящих ботанов и профессионалов своего дела, так и абсолютно бездарных айтишников и иже с ними. Не спорю, возможно этот человек божественно готовит яичницу, но зачем он тогда здесь пытается понять что происходит вообще и в основном тщетно. Беда IT на сегодняшний день, на мой взгляд, в острой нехватке нужных кадров. Из-за чего на их места приходят кто попало, без всякой подготовки, образования и в частых случаях без малейшего желания заниматься этой работой. А только потому что здесь платят зарплату и она ничего так себе по сравнению со средней по региону. В вузе я видел некоторых аспирантов, абсолютно не имеющих интереса в том, чем они занимаются. Но это были просто студенты, которые не хотели выходить из статуса учебы. Как говорится — магистратура и другие способы избежать взрослой жизни. Точно так же я видел бездарнейших программистов, проджект менеджеров, аналитиков, всяких дизайнеров, которые просто художники (здесь я бы очень поспорил с ними). Есть такая фраза на английском — good for nothing. Так вот, это они. Им некуда было податься, они не умели ничего. И вот, он пришел в ИТ. Молодец. А начальство не может его уволить, так как замену не найти просто. На рынке труда нельзя просто так взять и найти более менее нормального сотрудника. А если и можно найти, то не за те деньги, которое начальство готово платить. Но самое ужасное еще то, что эти кадры еще и не хотят развиваться. Я было обьяснял дизайнеру как необходимо делать макеты. Знаете какой я получил ответ от начальства? У нас нет времени на это. Нет времени на что? Чтоб твой сотрудник развивался и делал качественный продукт и уменьшал время разработки? Ведь мало кто действительно понимает, что если ты за 1 неделю выпустил релиз, а потом 2 недели переделывал, то в конечном итоге ты потратил на 1 этот экран 3 недели. А если бы твои работники имели необходимые знания и умения в работе, то всего этого можно было бы избежать и ты б закончил все за 10 дней и без переделывания с нуля. Так что исходя из моего опыта скажу словами народа — рыба гниет с головы. В первую очередь обратите внимание на начальство, насколько оно адекватное. Ведь вокруг хорошего человека будут собираться хорошие люди. And vice versa. Ровно так же я пытался улучшить качество тестирования, но каждый раз получал тот же самый ответ — у нас нет времени. Ага. Нет времени чтобы сделать качественное тестирование и выпустить релиз без критичных багов, но у нас есть время собраться в офисе в пятницу перед новым годом чтобы выпустить хот-фикс. И что самое ужасное на мой взгляд — это отсутствие тесткейсов, на эту тему я могу говорить и спорить до посинения. Но как показывает практика, очень мало людей понимают важность написания тесткейсов. А если кто-то и говорит, что понимает, то опять же ссылается или на нехватку времени или же на профнепригодность кадров. А когда ты пытаешься влиять на качество скилов других работников, тебя начинают просто презирать и вообще не вмешивайся в нашу работу. Вот поэтому ты и берешь листок бумаги и пишешь — до свидания. Кадры решают все. И если в вашей команде нет хороших кадров, то дело обречено на провал. Что меня поражает на самом деле так это то, что многие начальники оправдывают это тем, что — ну наша лодка кое-как остается на плаву и слава богу. Не раскачивай лодку, дружище. Да, классно, твои гребцы потеряли весла, дно пробито и заколочено пробкой, а ты сам в истерике выгребаешь воду из лодки и говоришь — зато мы на плаву. Ну супер. Еще одним немаловажным моментом я бы хотел отметить так называемый тимбилдинг. Я давно начал задумываться о вопросе — должны ли люди на работе оставаться всего лишь людьми на работе или же должны перерасти в статус друзей/приятелей и как это влияет на успех команды. На данный момент у меня нет однозначного ответа, но я больше склоняюсь к тому варианту, где люди неплохо так дружат между собой. Открою вам маленький секрет. В студ. годы я ходил в вуз больше не потому что мне там было ох как интересно, а потому что там были интересные люди, там было общение, веселье и так далее. И ровно так же я продолжал ходить на плохую работу только потому что там есть пара-тройка интересных людей, с которыми нескучно. Да, так бывает, что на работе самое интересное это поход на обед с коллегами. И ради этого ты готов ехать на работу 5 раз на неделе (об этом подробнее ниже). Но как показывает практика, дружеские отношения с коллегами это не самое важное и поэтому я все же увольнялся из компаний, где единственным плюсом были задушевные разговоры во время обеда.
  4. Три пункта выше наверно самые важные, а теперь настало время для менее важных, но не настолько, чтобы ими пренебрегать. Давайте поговорим о том, где находится офис. Знаю знаю, очень многим абсолютно все равно и они будут говорить — ну камон. Какая разница. А разница все же есть. Неужели для вас не имеет разницы как долго и каким образом добираться до работы? Неужели вам нравится ехать на электричке полчаса, а потом еще 20 минут на метро чтобы добраться до заветного офиса? Сейчас скажут — просто переезжай поближе к месту работы. Ну а если я на этой работе не буду достаточно долго работать? Да и знаете ли вы как непросто менять место жительства? Самое интересное то, что на одной работе я доезжал до офиса быстрее, добираясь из заМКАДа, чем те самые москвичи. Просто потому что Москва большой город. Ты можешь жить на одном краю Москвы, а офис может быть на другом конце и тебе добираться полтора часа. В этот пункт можно отнести так же и график работы. Если вы не любите просыпаться рано утром, то в какой-то момент этот пункт перечеркнет все плюсы работы которые есть. Предположим, ваше начальство требует приезжать на работу не позже 9 утра. А вам ехать полтора. Значит будильник будет вас воскрешать из мертвых каждые будни в 6. Итого, вы 3 часа в дороге, 9 на работе (все говорят про 8 часов работы, но постоянно забывают про час на обед). И ровно половину суток вы потратили, поздравляю. Осталось 12 часов в сутках, 8 на сон или сколько вам нужно? Мне нужно 10, если не 11 чтобы выспаться, но по крайней мере 9. Итак, ты постоянно раздражен постоянными поездками ржд и метро и у тебя вечный недосып, окей. Но ты все так же едешь на свою работу и там получаешь удовлетворение от работы. Ура! Повторяй что ты взрослый и ты должен терпеть боль и все будет хорошо. А теперь просто представь, что ты работаешь удаленно. Ты не тратишь времени на дорогу — минус 3 часа, ты не тратишь нервишки в давке в вагонах метро и ржд. Тебе не нужно вечером гладить свою рубашку чтобы ее надеть с утра. Тебе не нужно готовить себе обед на работу и класть ее в контейнеры. Ты можешь спокойно поесть супа дома. Но даже если мы не говорим про удаленную работу, то можем обсудить такой вариант — гибкое начало графика. И это все равно вас не спасет, ведь какая разница, если вы начинаете работать в 9 утра или в 11, вам все равно сидеть в душном офисе 9 часов. По поводу душных офисов. Да, они душные. Во-первых окон нет, во-вторых система кондиционирования вроде бы есть, но толку… уж лучше бы ее не было. Где пульт от кондея, убавьте, мне дует! Так что по настоящему хорошей работой можно считать частичную занятость или же когда твое начальство не похоже на гестапо и не требует отсиживать ровно по 9 часов в офисе. Хотя у меня были коллеги, которые сами вгоняли в рамки не только себя но и других — мы приходим в 9 утра, так что давай и ты.
  5. Поговорим о зарплате? Смотрите, было время я получал хорошую зарплату, но все остальные аспекты работы угнетали. И знаете что? Это была худшая моя работа. Да, я за пару месяцев заработал очень много денег. Но еще я заработал себе психологические проблемы и моральное истощение. После чего лечился отпуском целый месяц и не контактировал с людьми от слова совсем. Проблема нашего века в общем и ИТ в частности, что из тебя хотят выжать все соки. Да, тебе предлагают в основном достойную компенсацию (очень спорно конечно), но на кой хрен она тебе нужна? Ты получаешь много денег и потом тратишь много денег чтобы вернуться в нормальное состояние. Я однажды писал про заедание стресса. Вот и смотрите. Ты работаешь в трудных условиях, а потом заедаешь свой стресс сладким. Итого у тебя ожирение, тратишь деньги на пироги, но зато у тебя хорошая зарплата. Ага. Ну да. А теперь представь что ты нашел такую работу, где тебя все устраивает, но ты не получаешь так много денег. Поверьте моему опыту, это лучший вариант. Тебе тогда не придется заедать стресс или запивать его алкоголем и следственно причинять вред собственному здоровью, а после получения зарплаты тратить на восстановление. И кто в плюсе?

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

А вот и да! Это было очень сложно, но я смог найти такую работу. Итак, в ней есть все 5 пунктов плюсов.

  1. Я пилю приложение музыкального плеера/соц.сети.
  2. С самого начала я выстроил с командой процессы разработки таким образом, каким нужно мне.
  3. Минутка сексизма — со мной работают в команде одни мужчины. Все они взрослые и адекватные люди, каждый из которых делают свою работу просто замечательно и имеет достаточный опыт, чтобы его никто не учил как и что делать. А если и есть погрешности, то все с радостью готовы выслушать.
  4. Я работаю удаленно, и не просто удаленно, а еще и 15 часов в неделю. Да, да, мой день выглядит так — проснуться тогда, когда выспишься, без будильника. После чего спокойно позавтракать, заняться тем, чем хочется, в какой-то момент начать кодить и ровно через 3 часа закончить (я решаю какие задачи успею за 3 часа кодинга и ставлю таймер). После чего я закрываю ноутбук и живу. Как ни странно, я наконец-то начал гулять в парке. У меня появилось не только время, но и желание. И когда я гуляю в парке, я не заедаю стресс сладким и не размышляю о том, как же все хреново. Потому что во-первых невозможно устать за 3 часа кодинга, а во-вторых не о чем думать. Все нормально.
  5. Ну и зарплата у меня соответственно небольшая. Но ее хватает ровно на то, чтобы оплатить жилье и необходимые траты на еду и т.д. У меня нет причин запивать стресс или заедать его, значит я не трачу огромные суммы на водку, бургеры и пирожные. Да и по факту, тогда, когда я много зарабатывал я не особо тратил. Я не транжира по сути. А пойти погулять в парке не стоит ничего. Берешь свой мобильник, наушники, открываешь приложение которое разрабатываешь и гуляешь вечером. Красота. Ничего от жизни больше не нужно.

Подведем итоги? Негатив устранен (никакого общения/конфликтов с идиотами), позитив генерируется, хронической усталости нет, ровно как и поводов запивать и заедать стресс. Нет недосыпа и нет желания выпрыгнуть с 22 этажа небоскреба Москва Сити. Есть тонны свободного времени которое можно проводить как хочется, есть желание жить. Поздравляю.

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

Clean architecture. Explanation & details

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

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

Первое что у нас будет, это конечно же макет фрагмента (предположим что у нас single activity project). В общем и целом в нем будет 4 вью

<FrameLayout>
    <LinearLayout
        android:id="@+id/contentLayout">

        <EditText
            android:id="@+id/loginEditText"/>

        <EditText
            android:id="@+id/passwordEditText"/>
        <Button
            android:id="@+id/loginButton"/>
    </LinearLayout>
   
    <ProgressBar
        android:id="@+id/progressBar" />
</FrameLayout>

Поле ввода для логина, поле ввода для пароля, кнопка логина и прогресс на весь экран.

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

class LoginFragment : BaseFragment<LoginViewModel>() {

    override fun getLayoutResId() = R.layout.fragment_login

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        loginButton.setOnClickListener {
            viewModel.login(
                loginEditText.text.toString(),
                 passwordEditText.text.toString()
            )
        }

        viewModel.stateLiveData.observe(viewLifecycleOwner, Observer {
            when (it) {
                is LoginState.Success -> {
                    Navigation.goToPostLogin()
                }
                is LoginState.Error -> {
                    val clickListener = DialogInterface.OnClickListener { dialog, _ >
                        dialog.dismiss()
                        viewModel.removeError()
                    }
                    AlertDialog.Builder(context)
                        .setMessage(it.message)
                        .setPositiveButton(android.R.string.ok, clickListener)
                        .setCancelable(false)
                        .show()
                }
                is LoginState.Progress -> {
                    progressBar.makeVisible()
                    contentLayout.makeGone()
                }
                is LoginState.Initial -> {
                    progressBar.makeGone()
                    contentLayout.makeVisible()
                }
            }
        })
    }
}

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

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

class LoginViewModel(app: Application, private val interactor: LoginInteractor) : BaseViewModel(app) {

    var stateLiveData = MutableLiveData<LoginState>()

    fun removeError() {
        stateLiveData.value = LoginState.Initial
    }

    fun login(login: String, password: String) = viewModelScope.launch {
        stateLiveData.value = LoginState.Progress
        when (val result = interactor.login(email, password)) {
            LoginResult.Success -> {
                stateLiveData.postValue(LoginState.Success)
            }
            is LoginResult.Failed -> stateLiveData.postValue(
                LoginState.Error(result.message)
            )
        }
    }
}

sealed class LoginState {
    object Initial : LoginState()
    object Progress : LoginState()
    object Success : LoginState()
    data class Error(val message: String) : LoginState()
}

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

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

Итак, наша вьюмодель спрашивает интерактор о том, чтобы произвести логин. И вьюмодель интересует всего 1 вещь, закончилась ли операция успехом или же провалом. От этого у нас и 2 состояния — Success & Failed. Здесь конечно же возникнет вопрос, а почему текст ошибки приходит от интерактора. Чуть позже взглянем на интерактор и будет понятно, почему текст ошибки приходит от него. Здесь конечно же кто-то может сказать, что интерактор должен просто дать некую константу как причину, почему не удалась операция, и только потом мапить ее результат к строковым ресурсам. Но мы в данном случае будем упрощать этот момент и оставим принятие окончательного решения на читателя. А я в свою очередь обьясню почему в данном случае решил вопрос именно пробросом из интерактора текста ошибки.

Итак, взглянем на интерактор.

class LoginInteractorImpl(private val repository: LoginRepository, 
                          private val resourceManager: ResourceManager) : LoginInteractor {

    suspend fun login(login: String, password: String) =
         try {
            if (repository.login(email, password))
                LoginResult.Success
            else
                throw ServerUnavailableException()
        } catch (e: Exception) {
            when (e) {
                is NoNetworkException -> LoginResult.Failed(resourceManager.getString(R.string.no_connection_message))
                is ServerError -> LoginResult.Failed(e.serverMessage)
                is ServerUnavailableException -> LoginResult.Failed(resourceManager.getString(R.string.service_unavailable_message))
                else -> LoginResult.Failed(resourceManager.getString(R.string.generic_error_message))
            }
        }
}

sealed class LoginResult {
    object Success : LoginResult()
    data class Failed(val message: String) : LoginResult()
}

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

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

class LoginRepositoryImpl(
    private val prefManager: PrefManager,
    private val loginCloudDataStore: LoginCloudDataStore
) : LoginRepository {
    
    suspend fun login(login: String, password: String): Boolean {
        val token = loginCloudDataStore.login(login, password)
        val isTokenValid = !token.isNullOrEmpty()
        if (isTokenValid) 
            prefManager.saveToken(token)
        return isTokenValid
    }
}

Как ни странно, но репозиторий прост до безобразия (но это пока). В нашем случае, мы каждый раз идем в сеть чтобы залогиниться, но если например у нас будет больше логики, предположим будет не 1 облачный дата стор, а еще и локальный кеш, то будет еще и логика выбора дата сорса, хотя для этого можно написать датасорсфактори. Суть репозитория именно в этом, получить данные, их как-то обработать, с ними что-то сделать и передать на следующий уровень то, что нужно интерактору. Интерактору нужно знать только 1 вещь, завершился ли процесс успехом или ошибкой, если ошибкой, то какой. Далее мы рассмотрим источник данных и увидим, что он может бросать исключения, которые обработает наш интерактор. Здесь есть тоже холиварный вопрос о том, хорошo ли использовать метод выброса исключения или же использовать так называемую пару, успех и провал. Это дело каждого разработчика, я остановился на данном этапе на варианте с выбросом исключения, потому что мне кажется это более естественным для языка и более безопасно. Кстати, здесь могла бы быть еще одна сущность, под названием ТокенВалидатор, если предположим наша валидация отличается чем-то от примитивного сравнения строки с пустотой.

Итак, рассмотрим облачный дата сорс для логина.

class LoginCloudDataStore(private val service: LoginService) {

    suspend fun login(login: String, password: String): String? {
        if (Repository.isNetworkAbsent())
            throw NoNetworkException()
        try {
            val result = service.loginAsync(email, password).await()
            if (result.status.equals(STATUS_OK))
                return result.token
            if (!result.message.isNullOrEmpty())
                throw ServerError(result.message)
            throw ServerUnavailableException()
        } catch (e: Exception) {
            if (e is ServerError)
                throw e
            else
                throw ServerUnavailableException()
        }
    }
}

Итак, здесь, исходя из конфигурации нашего сервера мы делаем следующее. Сначала проверяем наличие сети, если его нет, бросаем исключение, которое удобочитаемо для слоя бизнес логики. Дальше мы пробуем получить данные у сервиса. Наш сервер нам говорит, что он отправит нам ответ со статусом ОК и там будет токен. Иначе же он может ответить неким собщением с другим статусом (например эррор). Тогда мы должны обработать и этот вариант. Здесь суть и отличие от интерактора в том, что мы на уровне сети обрабатываем нужные исходы. Интерактор не должен знать ничего о структуре нашего ответа от сервера. Ему не нужно знать о том, что сервер отдал ошибку 502 или 404 или что там еще. Это все должен обработать наш облачный источник данных и отдать нужный результат интерактору через репозиторий. Здесь мы используем котлин-корутины, поэтому и везде пишем suspend.

Думаю дальше все понятно. Сервис может выглядеть так.

interface LoginService {

    @Headers("SOME HEADERS HERE")
    @POST("/login")
    @FormUrlEncoded
    fun loginAsync(
        @Field("login") login:String,
        @Field("password") password:String
    ) : Deferred<LoginResponse>
}

data class LoginResponse(
    @SerializedName("status") val responseStatus: String,
    @SerializedName("message") val serverMessage: String?,
    @SerializedName("token") val loginToken: String?
)

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

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

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

Если же кому-то покажется, что наша вьюмодель могла бы делать что-то еще кроме как обращаться к интеркатору и обрабатывать ее результаты, то отвечу вам — да, вы правы. Так же в роль вьюмодели может входить первичная обработка данных от юзера. Например у вас может быть задача на клиенте зашить проверку данных от юзера. Тогда вы напишите такие классы как LoginUiValidator или PasswordUiValidator. Т.е. перед тем как дергать метод интерактора вы убедитесь в валидности введенных данных и не будете например посылать на сервер пустые строки или скажем если у вас логин это эл.почта, то вы сразу проверите что она содержит @ и точку.

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

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

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

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

А теперь давайте я попробую привести пример из реальной жизни. Например есть семья. В которой есть бабушка(аналогия с вьюмоделью) и она говорит, что у нее разболелась голова. Бабушке абсолютно не интересно как лечиться. Для этого у нее есть дочь (аналог интеркатора). Она уже сама решит что нужно предпринять. Итак, первым делом она попросит сына(аналог репозитория) посмотреть в аптечке есть ли нужная таблетка от головы (аналогия с локальным источником данных). Если же сын скажет ей, что в аптечке нет ничего нужного, тогда она пошлет его в аптеку за нужным лекарством. Матери абсолютно неважно откуда ее сын принесет лекарство (аналогия с сервисом). Ей нужен результат. Результатом же будет или успех — сын купил лекарство или же ошибка — не было ни в одной аптеке в округе лекарства. Тогда мать в свою очередь если сын вернул лекарство перемешает с водой и отдаст бабушке (аналог мапинга данных). Или же если она не могла найти лекарство из одного репозитория (сын), тогда она может обратиться к другому репозиторию (позвонить мужу). И ровно так же наш интерактор — мать, может переиспользоваться и для другой вьюмодели, например в случае с дедушкой.

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

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

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