Русский
Русский
English
Статистика
Реклама

Рефакторинг

Приглашаем на DINS JS EVENING (online) обсуждаем рефакторинг приложений и SvelteJS

22.09.2020 14:11:12 | Автор: admin
Встречаемся 30 сентября в 19:00.

В этот вечер Андрей Владыкин из DINS расскажет, с какими трудностями столкнулся при рефакторинге Chrome Extension и с помощью каких технических решений справился с этой задачей. Михаил Кузнецов из ING Bank сделает обзор нового фреймворка SvelteJS и проведет демо с разработкой простого приложение в прямом эфире. Участники встречи смогут задать вопросы спикерам.

Участие бесплатное, но нужна предварительная регистрация. Под катом подробная программа и информация о спикерах.

image


Программа


19:00-19:30 Рефакторинг приложения на примере Chrome Extension (Андрей Владыкин, DINS)
При рефакторинге Chrome Extension Андрею пришлось написать приложение, которое на тот момент не имело аналогов: WebRTC-клиент для звонков через браузер. Вы узнаете, с какими трудностями он столкнулся, как выбирал инженерные решения, какие задачи получилось решить, а какие нет. Еще Андрей расскажет, как выстроить работу, если требования к продукту постоянно меняются.
Доклад будет интересен начинающим фронтенд-разработчикам.

Андрей Владыкин Frontend Developer в DINS. Успел поработать в Enterprise-разработке и с сервисами для контактных центров. Сейчас работает над приложением для видеоконференций. В основном пишет на React.

19:30-20:10 Разработка быстрых и легких веб-приложений на SvelteJS (Михаил Кузнецов, ING Bank)
Михаил расскажет о SvelteJS новом фреймворке, при использовании которого генерируется минимальный итоговый бандл. Вы узнаете, с чем связана популярность фреймворка и почему его стоит применить в вашем следующем проекте.
Во время доклада Михаил сделает обзор функций SvelteJS, поделится опытом его использования. Вы увидите разработку простого приложения в прямом эфире и сможете на практике познакомиться с этим продуктом, его синтаксисом и компонентами.
Уровень начальный. Доклад будет интересен тем, кто еще не сталкивался со SvelteJS на практике и только хочет попробовать им воспользоваться.

Михаил Кузнецов Team Lead в ING Bank. Разработчик, спикер, тимлид, преподаватель. Хорошо относится к фронтенду, также пишет на других стеках.

Как присоединиться


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

Как проходят встречи


Записи предыдущих митапов можно посмотреть на нашем YouTube-канале.

О нас


DINS IT EVENING это место встречи и обмена знаниями технических специалистов по направлениям Java, DevOps, QA и JS. Несколько раз в месяц мы организуем встречи, чтобы обсудить с коллегами из разных компаний интересные кейсы и темы. Открыты для сотрудничества, если у вас есть наболевший вопрос или тема, которой хочется поделиться пишите на itevening@dins.ru!
Подробнее..

Радикальный перфекционизм в коде

20.02.2021 22:11:28 | Автор: admin

Идея взята с постов telegram-канала Cross Join


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

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

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


И правда, бред. Ну а зачем тогда мы сами себе вводим бешеный фашизм в коде?


Слишком жесткие правила


Посмотрите правила code style. Стандарт PSR-12, например.


Вот несколько пунктов:


1) В конце каждого файла должен быть перевод строки. А если не будет, то кто умрёт?
2) Нельзя делать несколько statements на одной строке. Если я напишу $x = 1; $y = 1; $z = 1;, то читабельность ухудшится на 0.00001% и можно закрывать техотдел?
3) Declare statements MUST contain no spaces and MUST be exactly declare(strict_types=1). Ох, как всё серьёзно. Ни одного пробела, причем слово MUST капсом, чтобы все понимали степень ответственности. Если вставить где-нибудь пробел, то на код ревью никто код же прочесть не сможет!


Но блин, если один чудак однажды написал под воздействием каких-то веществ


declare(        strict_types                                              =1         )

то это еще не значит, что надо ВСЕХ бить плёткой ругать линтером за каждый пробел. Нужно просто вправить мозг ОДНОМУ чудаку. Готов поспорить, что именно он тогда пришел на работу без трусов.


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


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


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


Почему это важно


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


И перформишь при этом как бог! Совершенно спокойно можно все выходные провести за кодингом и особо не устать.


Короче. От программирования мы получаем фан, от внешних навязанных ограничений мы получаем раздражение. От навязанных необоснованных ограничений мы получаем выгорание.


Раньше мне казалось, что начальство перегибает палку ради дисциплины в вакууме, и меня от этого бомбило. Я это запомнил, и, став тимлидом, в своих командах стал применять демократию. Не навязывал от себя практически никаких правил. Тем не менее странные правила всё равно появлялись.


К примеру, в Go есть goimports, который форматирует код по стандарту, вшитому в стандартный тулинг. Казалось бы, о чем тут теперь спорить. Но все равно внезапно обнаруживаешь себя, переименовывающим getJson в getJSON и getById в getByID, иначе правило линтера N100500 скажет айайай. При этом читабельность если и меняется, то незначительно, и даже не ясно, в какую сторону.


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


Дофаминовый цикл "сделал задачу получил удовлетворение" нарушен. Сделал задачу получи звиздюлей от линтера и коллег на код ревью.


Дофамин еще полбеды. Возведение принципов программирования (тех же DRY или SOLID) в радикальный абсолют может привести к таким абстракциям, что код превращается в нечитабельное месиво. Увидел пару switch case сразу городи иерархию классов. Так ведь в книжке написано.


Мы ругаем бешеный принтер, ограничивающий свободу, но сами часто действуем по принципу "запретить и не пущать".


Вообще, иногда хочется пересмотреть вообще всё. Например, однажды мы думали, как назвать по-английски переменную для ЦФО (центр финансовой ответственности). Словарь говорит, что это будет financial responsibility center. Если назвать переменную "FRC", то новичку будет непонятно, что это за хрень. В документации по продукту термин везде по русски, ЦФО. Если назвать financialResponsibilityCenter, то можно догадаться, о чем речь, но слишком длинно как-то.


Понятно, что никто не рискнет так делать, но идеально было бы так и назвать переменную русскими буквами ЦФО. Если проект сугубо для русскоязычных людей и русскоязычных программистов, то почему нет? Просто потому что не принято, вот и всё. Рациональных причин мало, и с ними можно поспорить, как минимум в каком-то конкретном случае.


Вывод


Я считаю, что:


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

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

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


Очень надеюсь на дискуссию в комментариях.

Подробнее..

Функциональный Kotlin. Во имя добра, радуги и всего такого

30.01.2021 00:09:39 | Автор: admin

Введение

Сам по себе Kotlin очень мощный инструмент, но многие часто используют его не на полную мощность, превращая его в какую-то... Java 6. Попробую рассказать почему так делать не надо и как использовать функциональные фичи языка на полную.

Функции высшего порядка

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

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

val foo: () -> Unit = {  }

Тогда, мы сможем передать ее, используя синтаксис вида:

run(foo)

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

str.run(String::isEmpty)

Перейдем к более конкретному кейсу. Допустим, нам нужно распарсить строки определенным образом, но только в одном месте в программе. Очевидно, что логика будет повторяться. Что делать? Создадим для этого отдельный объект с одним методом? Или пропишем для каждого "ручками" прямо на месте?

Нет, копить технический долг ни к чему, лучше мы сделаем, например, что-то такое:

val parse: (String) -> List<Int> = { it.split(":").map(String::toInt) }val (xMin, yMin) = parse(data["from"])val (xMax, yMax) = parse(data["to"])

Функции области видимости

Теперь, когда мы разобрались с ФВП, перейдем к вещам, которыми, скорее всего, пользовались все. let,run,with,apply, иalso. Знакомые слова? Надеюсь, но все же разберем их.

inline fun <T, R> T.let(block: (T) -> R): Rinline fun <T> T.also(block: (T) -> Unit): T

Сначала let и also. Они наиболее просты и понятны, потому что все что они делают внутри - это вызов block(this) . По сути, мы просто делаем вызов определенной нами "на месте" функции. Разница лишь в том, что они возвращают. also используется когда вы хотите продолжить работу с тем же объектом, у которого решили вызвать функцию и let, если вам нужно вернуть новый объект.

inline fun <R> run(block: () -> R): Rinline fun <T, R> T.run(block: T.() -> R): Rinline fun <T, R> with(receiver: T, block: T.() -> R): Rinline fun <T> T.apply(block: T.() -> Unit): T

Теперь к run, with и apply:
run очень похож на let, apply на also, а with это почти то же самое что и run, просто receiver получаем разными способами. Чем они удобны и зачем нужны, если есть let и also? Все просто, тут вместо it используется this, который вообще можно опустить и не писать.

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

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

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

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

Классы и Объекты

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

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

let {  val some = Some()  it.run(some::doSome)}

Использование объекта позволило бы нам сделать наш код проще:

let(Some::doSome)

Как хорошо, когда нет ничего лишнего, да?

Но допустим мы все же вынуждены иметь дело с классом, у которого есть поведение, но экземпляр класса создавать не хотим? К счастью, и для этого найдется достаточно простое решение. Мы просто скинем всю статику, которая есть в объекте в companion object:

class Some {  companion object {    fun doSome(any: Any) = run {}  }}

Теперь мы можем сделать так же, как и с объектом.

Factory методы

Для начала я приведу пример кода:

val other = Other()val stuff = other.produceStuff()val some = Some(stuff)

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

Конечно, мы можем заинлайнить его:

val some = Some(  Other().produceStuff())

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

class Some {  companion object Factory {    inline fun <T>create(t: T?, f: (T?) -> Stuff) = Some(f(t))  }}

Теперь мы можем сделать так:

val some = Some(Other()) { it.doStuff() }

Или если класс Other тоже имеет свой фабричный метод:

val some = Some.create(Other) { it.create().doStuff() }

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

Подводные камни функций-расширений

Предполагаю, что не знать о наличии в Котлине возможности писать функции-расширения и как это делать могут совсем новички, но на всякий случай я объясню. Это функция, с которой вы можете взаимодействовать так же, как с методом класса. Вот как это выглядит:

fun Some.foo() = run { }

Или если хотите экзотики:

val foo: Some.() -> Unit = {  }

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

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

Давайте посмотрим на вот такой код:

class Some {  fun Other.someExtention() = run { }}

В принципе, ничто не мешает нам так сделать, но есть одно "но", из-за которого я считаю это очень плохой практикой.

Все дело в том, что передать ссылку на эту функцию просто не получится. Никак. Даже внутри самого класса. Если так сделать, то Котлин просто не поймет, как обращаться к этой функции - как к методу класса Some или Other.

Однако, если мы, например, вынесем расширения в отдельный файлик под расширения определённого класса или вообще растащим их по пакетам на свое усмотрение - сможем легко обратиться к функции как Some::someExtention. Естественно, в данном случае класс или объект не важно - поведение будет одинаковым.

P.S.

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

fun Some.overlay(f: KFunction1<Some, Any>) = f(this)

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

Подробнее..

Перевод Разработка на Android как найти подходящую абстракцию для работы со строками

15.02.2021 12:06:03 | Автор: admin

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

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

Фото: UnsplashФото: Unsplash

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

Уровень абстракции для строк?

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

  • Простой строковый ресурс вроде R.string.some_text, отображаемый на экране с помощью resources.getString(R.string.some_text)

  • Отформатированная строка, которая форматируется во время выполнения, т.е. context.getString(R.string.some_text, arg1, 123) с

<string name=some_formatted_text>Some formatted Text with args %s %i</string>
  • Более сложные строковые ресурсы, такие как Plurals, которые перегружены, например resources.getQuantityString(R.plurals.number_of_items, 2):

<plurals name="number_of_items">  <item quantity="one">%d item</item>  <item quantity="other">%d items</item></plurals>
  • Простой текст, который не загружается из ресурсов Android в XML-файле вроде strings.xml, а уже загружен в переменную типа String и не требует дальнейшего преобразования (в отличие от R.string.some_text). Например, фрагмент текста, извлеченный из json ответа с сервера.

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

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

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

Давайте шаг за шагом рассмотрим эти моменты на конкретном примере: предположим, мы хотим загружать строку с сервера по http, и если это не удается, мы отображаем аварийную fallback-строку из strings.xml. Например, так:

class MyViewModel(  private val backend : Backend,  private val resources : Resources // ресурсы Android из context.getResources()) : ViewModel() {  val textToDisplay : MutableLiveData<String>  // MutableLiveData используется для удобства чтения   fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = text    } catch (t : Throwable) {      textToDisplay.value = resources.getString(R.string.fallback_text)    }  }}

Детали реализации просочились в нашу MyViewModel, что в целом усложняет ее тестирование. Действительно, чтобы написать тест для loadText(), нам надо либо замокать Resources, либо ввести интерфейс наподобие StringRepository (по шаблону "репозиторий"), чтобы при тестировании мы могли заменить его другой реализацией:

interface StringRepository{  fun getString(@StringRes id : Int) : String} class AndroidStringRepository(  private val resources : Resources // ресурсы Android из context.getResources()) : StringRepository {  override fun getString(@StringRes id : Int) : String = resources.getString(id)} class TestDoubleStringRepository{    override fun getString(@StringRes id : Int) : String = "some string"}

Затем вью-модель получит StringRepository вместо непосредственно ресурсов, и в этом случае все будет в порядке, не так ли?

class MyViewModel(  private val backend : Backend,  private val stringRepo : StringRepository // детали реализации скрываются за интерфейсом) : ViewModel() {  val textToDisplay : MutableLiveData<String>     fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = text    } catch (t : Throwable) {      textToDisplay.value = stringRepo.getString(R.string.fallback_text)    }  }}

На эту вью-модель можно написать такой юнит-тест:

@Testfun when_backend_fails_fallback_string_is_displayed(){  val stringRepo = TestDoubleStringRepository()  val backend = TestDoubleBackend()  backend.failWhenLoadingText = true // заставит backend.getText() выкинуть исключение  val viewModel = MyViewModel(backend, stringRepo)  viewModel.loadText()   Assert.equals("some string", viewModel.textToDisplay.value)}

С введением interface StringRepository мы добавили уровень абстракции и решили задачу, верно? Нет. Мы добавили уровень абстракции, но реальная проблема все еще перед нами:

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

  • Кроме того, если рассматривать реализацию TestDoubleStringRepository и тест, который мы написали, насколько он является значимым? TestDoubleStringRepository всегда возвращает одну и ту же строку. Мы могли бы совершенно испортить код вью-модели, передавая R.string.foo вместо R.string.fallback_text в StringRepository.getString(), и наш тест все равно бы был пройден. Конечно, можно улучшить TestDoubleStringRepository, чтобы он не просто всегда возвращал одну и ту же строку:

class TestDoubleStringRepository{    override fun getString(@StringRes id : Int) : String = when(id){      R.string.fallback_test -> "some string"      R.string.foo -> "foo"      else -> UnsupportedStringResourceException()    }}

Но насколько это поддерживаемо? Вы хотели бы так делать для всех строк в вашем приложении (если их у вас сотни)?

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

Нам поможет TextResource

Придуманная нами абстракция называется TextResource. Это модель для представления текста, которая относится к слою domain. Таким образом, это объект первого класса в нашей бизнес-логике. И выглядит это следующим образом:

sealed class TextResource {  companion object { // Используется для статических фабричных методов, чтобы файл с конкретной реализацией оставался приватным    fun fromText(text : String) : TextResource = SimpleTextResource(text)    fun fromStringId(@StringRes id : Int) : TextResource = IdTextResource(id)    fun fromPlural(@PluralRes id: Int, pluralValue : Int) : TextResource = PluralTextResource(id, pluralValue)  }} private data class SimpleTextResource( // Можно будет также использовать inline классы  val text : String) : TextResource() private data class IdTextResource(  @StringRes id : Int) : TextResource() private data class PluralTextResource(    @PluralsRes val pluralId: Int,    val quantity: Int) : TextResource() // можно будет добавить и другие виды текста...

Так выглядит вью-модель с TextResource:

class MyViewModel(  private val backend : Backend // Обратите, пожалуйста, внимание, что не надо передавать ни какие-то ресурсы, ни StringRepository.) : ViewModel() {  val textToDisplay : MutableLiveData<TextResource> // Тип уже не String     fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = TextResource.fromText(text)    } catch (t : Throwable) {      textToDisplay.value = TextResource.fromStringId(R.string.fallback_text)    }  }}

Основные отличия:

1) textToDisplay поменялся c LiveData<String> на LiveData<TextResource>, поэтому теперь вью-модели не нужно знать, как переводить разные типы текста в String. Она должна уметь переводить их в TextResource. Однако, это нормально, как будет видно далее, TextResource это абстракция, которая решит наши проблемы.

2) Посмотрите на конструктор вью-модели. Нам удалось удалить неправильную абстракцию StringRepository (при этом нам не нужны Resources). Вас, возможно, интересует, как теперь писать тесты? Так же просто, как напрямую протестировать TextResource. Дело в том, что эта абстракция также абстрагирует зависимости Android, такие как ресурсы или контекст (R.string.fallback_text это просто Int). И вот как выглядит наш юнит-тест:

@Testfun when_backend_fails_fallback_string_is_displayed(){  val backend = TestDoubleBackend()  backend.failWhenLoadingText = true // заставит backend.getText() выкинуть исключение  val viewModel = MyViewModel(backend)  viewModel.loadText()   val expectedText = TextResource.fromStringId(R.string.fallback_text)  Assert.equals(expectedText, viewModel.textToDisplay.value)  // для data class-ов генерируются методы equals, поэтому мы легко можем их сравнивать}

Пока все хорошо, но не хватает одной детали: как нам преобразовать TextResource в String, чтобы можно было отобразить его, например, в TextView? Что ж, это касается исключительно отрисовки в Android, и мы можем создать функцию расширения и заключить ее в слое UI.

// Можно получить ресурсы с помощью context.getResources()fun TextResource.asString(resources : Resources) : String = when (this) {   is SimpleTextResource -> this.text // smart cast  is IdTextResource -> resources.getString(this.id) // smart cast  is PluralTextResource -> resources.getQuantityString(this.pluralId, this.quantity) // smart cast}

А поскольку преобразование TextResource в String происходит в UI (на уровне представления) архитектуры нашего приложения, TextResource будет переводиться при изменении конфигурации (т.е. при изменении системного языка на смартфоне), что обеспечит правильную локализацию строки для любых ресурсов R.string.* вашего приложения.

Бонус: вы можете легко написать юнит-тест для TextResource.asString(), создавая моки для ресурсов. При этом не следует создавать мок для каждого отдельного строкового ресурса в приложении, потому что на самом деле нужно протестировать всего лишь работу конструкции when. Поэтому здесь будет корректно всегда возвращать одну и ту же строку из замоканного resources.getString(). Кроме того, TextResource можно многократно использовать в коде, и он соответствует принципу открытости/закрытости. Так, его можно расширить для будущих вариантов использования, добавив всего несколько строк кода: новый класс данных, который расширяет TextResource, и новую ветку в конструкцию when в TextResource.asString().

Поправка: как правильно подметили в комментариях, TextResource не следует принципу открытости/закрытости. Можно было бы поддержать принцип открытости/закрытости для TextResource, если бы у sealed class TextResouce была abstract fun asString(r: Resources), которую реализуют все подклассы. Я лично считаю, что можно пожертвовать принципом открытости/закрытости в пользу упрощения структур данных и работать с расширенной функцией asString(r: Resources), которая находится за пределами иерархии наследования (именно этот способ описан в статье и является достаточно расширяемым, хотя и не настолько, как с принципом открытости/закрытости). Почему? Я считаю, что добавление функции с параметром Resources к публичному API TextResource проблематично, потому что только часть подклассов нуждается в этом параметре (например, SimpleTextResource такого вообще не требует). Кроме того, если такая реализация станет частью общедоступного API, это может привести к увеличению накладных расходов на поддержку кода, а также к появлению дополнительных сложностей (особенно при тестировании).

Выводы

Описанный в статье способ можно применять для работы со строками и другими ресурсами: dimens, изображениями, цветами. При этом в каждом случае важно анализировать, насколько уместна работа с абстракциями. Хотя злоупотреблять ими нежелательно, в некоторых случаях абстракции могут быть полезны в том числе, как мы уже упоминали, для более простого написания тестов. Ждем ваших отзывов об этом методе и его практическом применении!

Подробнее..

Перевод Языки любимые и языки страшные. Зелёные пастбища и коричневые поля

07.05.2021 14:19:42 | Автор: admin


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

В опросах есть категории Самые страшные языки программирования (The Most Dreaded Programming Languages) и Самые любимые языки. Оба рейтинга составлены на основе одного вопроса:

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

Страшный язык это такой, с которым вы активно работаете в текущем году, но не хотите продолжать его использовать. Любимый язык тот, который вы широко используете и хотите продолжать использовать. Результаты интересны тем, что отражают мнения людей, которые активно используют каждый язык. Не учитываются мнения типа Я слышал, что Х это круто, когда люди высоко оценивают вещи, которые они НЕ используют, потому что они слышали, что это новый тренд. Обратное тоже правда: люди, которые выражают отвращение к какому-то языку, реально широко используют его. Они боятся языка не потому, что слышали о его сложности, а потому, что им приходится работать с ним и испытывать настоящую боль.

Топ-15 страшных языков программирования:
VBA, Objective-C, Perl, Assembly, C, PHP, Ruby, C++, Java, R, Haskell, Scala, HTML, Shell и SQL.

Топ-15 любимых языков программирования:
Rust, TypeScript, Python, Kotlin, Go, Julia, Dart, C#, Swift, JavaScript, SQL, Shell, HTML, Scala и Haskell.

В списке есть закономерность. Заметили?

Худший код тот, что написан до меня


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

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

Джоэл Спольски Грабли, на которые не стоит наступать

Назовём это Законом Джоэла. Из этой посылки вытекает многое. Почему большинство разработчиков думают, что унаследованный ими код это бардак, и хотят выбросить его и начать всё сначала? Потому что написание чего-то нового проще для мозга, чем тяжёлая работа по пониманию существующей кодовой базы, по крайней мере, на начальном этапе. Почему попытки переписать код часто обречены на провал? Потому что многие мусорные артефакты это жизненно важные небольшие улучшения, которые накапливаются с течением времени. Без какого-то конкретного плана по рефакторингу вы в конечном итоге вернётесь к тому, с чего начали.


Scott Adams Understood

Легко понять код, который вы пишете. Вы его выполняете и совершенствуете по ходу дела. Но трудно понять код, просто прочитав его постфактум. Если вы вернётесь к своему же старому коду то можете обнаружить, что он непоследовательный. Возможно, вы выросли как разработчик и сегодня бы написали лучше. Но есть вероятность, что код сложен по своей сути и вы интерпретируете свою боль от понимания этой сложности как проблему качества кода. Может, именно поэтому постоянно растёт объём нерассмотренных PR? Ревью пул-реквестов работа только на чтение, и её трудно сделать хорошо, когда в голове ещё нет рабочей модели кода.

Вот почему вы их боитесь


Если реальный старый код незаслуженно считают бардаком, то может и языки программирования несправедливо оцениваются? Если вы пишете новый код на Go, но должны поддерживать обширную 20-летнюю кодовую базу C++, то способны ли справедливо их ранжировать? Думаю, именно это на самом деле измеряет опрос: страшные языки, вероятно, будут использоваться в существующих проектах на коричневом поле. Любимые языки чаще используются в новых проектах по созданию зелёных пастбищ. Давайте проверим это.1

Сравнение зелёных и коричневых языков


Индекс TIOBE измеряет количество квалифицированных инженеров, курсов и рабочих мест по всему миру для языков программирования. Вероятно, есть некоторые проблемы в методологии, но она достаточно точна для наших целей. Мы используем индекс TIOBE за июль 2016 года, самый старый из доступных в Wayback Machine, в качестве прокси для определения языков, накопивших много кода. Если язык был популярным в 2016 году, скорее всего, люди поддерживают написанный на нём код.

Топ-20 языков программирования в списке TIOBE по состоянию на июль 2016 года: Java, C, C++, Python, C#, PHP, JavaScript, VB.NET, Perl, ассемблер, Ruby, Pascal, Swift, Objective-C, MATLAB, R, SQL, COBOL и Groovy. Можем использовать это в качестве нашего списка языков, которые с большей вероятностью будут использоваться в проектах по поддержке кода. Назовём их коричневыми языками. Языки, не вошедшие в топ-20 в 2016 году, с большей вероятностью будут использоваться в новых проектах. Это зелёные языки.


Из 22 языков в объединённом списке страшных/любимых 63% коричневых

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

Java, C, C++, C#, Python, PHP, JavaScript, Swift, Perl, Ruby, Assembly, R, Objective-C, SQL


Зелёный язык: язык, который вы с большей вероятностью будете использовать в новом проекте.

Go, Rust, TypeScript, Kotlin, Julia, Dart, Scala и Haskell

У TIOBE и StackOverflow разные представления о том, что такое язык программирования. Чтобы преодолеть это, мы должны нормализовать два списка, удалив HTML/CSS, шелл-скрипты и VBA.2

Конечно, простое деление на зелёные и коричневые упускает много нюансов, в том числе по размеру полей. Я ожидаю, что больше зелёных пастбищ должно быть на Swift, чем на Objective-C, но и нынешняя методика, кажется, охватывает всё, что нам нужно. В этом списке гораздо больше коричневых языков, чем зелёных, но это вполне ожидаемо, ведь новых языков ежегодно появляется относительно немного.

Теперь можно ответить на вопрос: люди действительно боятся языков или же они просто боятся старого кода? Или скажем иначе: если бы Java и Ruby появились сегодня, без груды старых приложений Rails и старых корпоративных Java-приложений для поддержки, их всё ещё боялись бы? Или они с большей вероятностью появились бы в списке любимых?

Страшные коричневые языки



Страшные языки на 83% коричневые

Топ страшных языков почти полностью коричневый: на 83%. Это более высокий показатель, чем 68% коричневых языков в полном списке.

Любимые зелёные языки



Любимые языки на 54% зелёные

Среди любимых языков 54% зелёных. В то же время в полном списке всего лишь 36% языков являются зелёными. И каждый зелёный язык есть где-то в списке любимых.

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

Курт Воннегут

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

Другими словами, Rust, Kotlin и другие зелёные языки пока находятся на этапе медового месяца. Любовь к ним может объясняться тем, что программистам не надо разбираться с 20-летними кодовыми базами.

Устранение предвзятости




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

Цикл хайпа языков программирования


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


Цикл хайпа языков программирования

У меня под рукой нет данных, но я отчётливо помню, что Ruby был самым популярным языком в 2007 году. И хотя сегодня у него больше конкурентов, но сегодня Ruby лучше, чем тогда. Однако теперь его боятся. Мне кажется, теперь у людей на руках появились 14-летние приложения Rails, которые нужно поддерживать. Это сильно уменьшает привлекательность Ruby по сравнению с временами, когда были одни только новые проекты. Так что берегитесь, Rust, Kotlin, Julia и Go: в конце концов, вы тоже лишитесь своих ангельских крылышек.3



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

Была мысль определять статус зелёного или коричневого по дате создания языка, но некоторые старые языки нашли применение только относительно недавно.

Вот методика измерения TIOBE, а их исторические данные доступны только платным подписчикам, поэтому Wayback Machine. [вернуться]

2. HTML/CSS не являются тьюринг-полными языками, по этой причине TIOBE не считает их полноценными языками программирования. Шелл-скрипты измеряются отдельно, а VBA вообще не исследуется, насколько я понял. [вернуться]

3. Не все коричневые языки внушают страх: Python, C#, Swift, JavaScript и SQL остаются любимыми. Хотелось бы услышать какие-нибудь теории о причине этого феномена. Кроме того, Scala и Haskell два языка, к которым я питаю слабость единственные зелёные языки в страшном списке. Это просто шум или есть какое-то обоснование??? [вернуться]
Подробнее..

Какие навыки можно прокачать на проекте c большой кодовой базой

07.08.2020 18:11:02 | Автор: admin


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


Содержание



  1. Для кого этот текст
  2. Чему вы можете научиться на проекте с историей
  3. Какие вопросы задавать на собеседовании
  4. Советы тем, кто только начал работу с легаси-проектом
  5. Кратко


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

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

Disclaimer для самых маленьких


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

Для кого этот текст


Текст рассчитан как на тех, кто причисляет себя к уровню Junior, так и на тех, кто попадает под категорию Midde/Senior.
В работе с долгоживущими проектами можно многому научиться на любом из уровней разработки. Чтобы быть на одной волне в понимании уровней, прочитайте посты Вастрика: Войти вайти и К Команда. Мне нравится его классификация уровней разработчиков, и я буду ее придерживаться в дальнейшем.
В следующем блоке разберем кратко, в чем польза для развития каждого из уровней на долгих проектах.

Junior


Задача Junior разработчиков максимальное развитие технических навыков. В ход идет все: языки, фреймворки, библиотеки, подходы к разработке и т.д. На любом проекте с сильной командой вы сможете учиться решать разные задачи.

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

Middle


Задача Middle разработчика повышение своей автономности. Вы должны стать членом команды, которому можно доверить практически любую задачу на проекте. Это значит, что чем более разнообразные задачи есть на проекте, тем больше у вас потенциальных направлений роста.

Senior


Задача Senior разработчика полное осознание, что вам платят не за код, а за решение проблем. Иногда (пока все таки чаще) через написание кода, иногда через управление другими разработчиками, иногда через общение с не-разработчиками. Зрелые проекты просто кладезь задач, которые можно и нужно решить.

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

Причем тут легаси?


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

Майкл Фезерс (Michael Feathers), основатель R7K Research & Conveyance, утверждает, что легаси код это код, не покрытый тестами. Преимущество такого подхода в том что, с первого взгляда, он претендует на объективность. Но в реальности могут быть два сценария:

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


Еще один взгляд на то, что такое легаси от разработчика Дро Хелпера (Dror Helper): No longer engineered continuedly patched and hacked (Легаси-код код код, который постоянно ломают и подпирают костылями вместо того, чтобы его развивать).

Веб-разработчик Николас Карло (Nicolas Carlo) считает, что легаси код это код, с которым некомфортно работать.

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

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

В общем, если проекту больше полугода, то скорее всего в нем будет легаси.

Чему вы можете научиться на проекте с историей?





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

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

Анализировать проект


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

Самое главное, что могу посоветовать для углубления в тему анализа проекта прочесть книгу Эффективная работа с легаси-кодом Майкла Физерса. Она старая и известная. В ней описывается большое количество практик по работе с унаследованным кодом.

И еще один совет зайти на сайт Understand Legacy Code. Это блог, посвященный одной тематике работой с легаси. Важно, что там можно подписаться на рассылку. Уверен, многие Android-разработчики знают про рассылки Android Weekly и Kotlin Weekly. Рассылка ULC тоже очень полезна. Она не навязчивая, с практическими статьями про рефакторинг и написание кода.

Делать рефакторинг


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

Проектировать и создавать архитектуру


Этот пункт вытекает из предыдущего вам неизбежно нужно будет прокачиваться в проектировании ПО и архитектуре. При правильной культуре разработки у вас не будет необходимости принимать серьезные архитектурные решения в сжатые сроки. У вас будет время на проработку архитектуры и ваша ответственность будет в том, чтобы сделать это хорошо. Что я тут всегда советую? Читайте книги.

Я считаю книги не менее важными, чем другие источники информации. Чем больше опыта вы наберетесь из книг, тем больше и быстрее будете понимать, как решать типовые проблемы. К типовым/типичным проблемам всегда есть типичные решения, которые можно найти в книгах. Работа с любым видом легаси так же предполагает решение типовых проблем.

Книги по архитектуре

Роберт Мартин (Robert C Martin). Чистая архитектура, Чистый код
Мартин Фаулер (Martin Fowler). Рефакторинг. Улучшение проекта существующего кода


Инструменты
для проектирования архитектуры


UML используем для проектирования.
PlantUML (PUML) библиотека и сервис, которые позволяют в текстовом виде описывать UML-диаграмму. Его можно хранить в git, следовательно можете к процессу обновления и обсуждения этих диаграмм подключить те инструменты, которые используете для работы с обычным кодом.


Декомпозировать


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

Автоматизировать и уметь применять CI/CD


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

Общаться


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

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

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

Какие вопросы задавать на собеседовании





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

Как устроен рабочий процесс и почему именно так


Обычно сейчас работают по системам Scrum или Kanban. Но при этом часто адаптируют их под себя: взяли лучшее, ненужное выкинули. Вопрос: что именно они выкинули, а что оставили? Потому что в Scrum guide, на основе которого строится процесс разработки, есть довольно интересные и полезные практики.

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

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

Другой интересный для меня вопрос по внутренним процессам: как в команде проводится code review? Есть ли какие-то внутренние правила проведения review, которые помогают сократить его время? Существует ли общая база знаний, в которую заносят результаты холиваров, чтобы не устраивать их каждый раз?

Как происходит планирование, кто участвует и насколько активна команда


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

Сколько в команде людей, которые обладают всей картиной


Здесь возможны две крайности. Первая крайность это когда вы приходите в команду, которая работает над продуктом последние 2-4 года. Если при этом команда не сильно менялась, а только расширялась, то это очень хорошо, потому что у вас всегда будут люди, к которым вы сможете обратиться за помощью. Они смогут что-то подсказать, объяснить причину, рассказать вам историю проекта. Знать контекст и почему все устроено так, а не иначе очень важно.

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

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

Как ведется бэклог технического долга


Проблемы есть в любом проекте, и важно понимать, как именно команда с ними работает. О работе с техническим долгом очень хорошо написал alexanderlebedev в статье Ланнистеры всегда платят свои долги! (и технические тоже).

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

Советы тем, кто только начал работу с легаси-проектом





Боритесь с соблазном броситься переписывать все с нуля


Обычная ситуация: вы начали работу над уже существующим приложением и видите, что там все сделано не так. И с этим не так вам нужно работать дальше. Вполне естественное желание в этом случае взять и переписать все это заново. Это нормальная реакция.

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

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

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

Прогнозируйте изменения проекта


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

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

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

Проводите исследования


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

Уделяйте время документации


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

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

Одним из способов ведения таких записей является Architecture Decision Records. Его основная идея состоит в том, что для каждого нетривиального архитектурного решения нужно создать файл с несколькими элементами: заголовок, дата, контекст, описание решения, предполагаемые последствия. Этот файл хранится вместе с кодом и не меняется после создания. Его главная ценность в том, что когда придет время менять это архитектурное решение, будет гораздо проще понять мотивацию, которая к нему привела.

Кратко


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


Текст подготовлен по материалам моего доклада на митапе GDG.
Подробнее..

Шаблоны GRASP Polymorphism, Pure Fabrication, Indirection, Protected Variations

01.10.2020 18:11:51 | Автор: admin
Привет, Хабр! Меня зовут Владислав Родин. В настоящее время я являюсь руководителем курса Архитектор высоких нагрузок в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

Специально к старту нового набора на курс Архитектура и шаблоны проектирования я продолжаю серию своих публикаций про шаблоны GRASP.



Введение


Описанные в книге Craig'а Larman'а Applying UML and patterns, 3rd edition, GRASP'овские паттерны являются обобщением GoF'овских паттернов, а также непосредственным следствием принципов ООП. Они дополняют недостающую ступеньку в логической лестнице, которая позволяет получить GoF'овские паттерны из принципов ООП. Шаблоны GRASP являются скорее не паттернами проектирования (как GoF'овские), а фундаментальными принципами распределения ответственности между классами. Они, как показывает практика, не обладают особой популярностью, однако анализ спроектированных классов с использованием полного набора GRASP'овских паттернов является необходимым условием написания хорошего кода.

Полный список шаблонов GRASP состоит из 9 элементов:

  • Information Expert
  • Creator
  • Controller
  • Low Coupling
  • High Cohesion
  • Polymorphism

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

Polymorphism


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

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

Наличие в коде конструкции switch является нарушением данного принципа, switch'и подлежат рефакторингу.

Злоупотребление полиморфизмом приводит к переусложнению кода и в общем случае не приветствуется.

Pure Fabrication


Необходимо обеспечивать low coupling и high cohesion. Для этой цели может понадобиться синтезировать искуственную сущность. Паттерн Pure Fabrication говорит о том, что не стоит стесняться это сделать. В качестве примера можно рассматривать фасад к базе данных. Это чисто искуственный объект, не имеющий аналогов в предметной области. В общем случае любой фасад относится к Pure Fabrication (если это конечно не архитектурный фасад в соответствующим приложении).

Indirection


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

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

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

Protected Variations


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

На самом деле, это не паттерн, а цель, достигаемая соблюдением остальных паттернов.

Вывод


Шаблоны GRASP состоят из 8 паттернов:
1) Information Expert информацию обрабатываем там, где она содержится.
2) Creator создаем объекты там, где они нужны.
3) Controller выносим логику многопоточности в отдельный класс или компонент.
4) Low Coupling 5) High Cohesion проектируем классы с однородной бизнес-логикой и минимальным количеством связей между собой.
6) Polymorphism различные варианты поведения системы при необходимости оформляем в виде полиморфных вызовов.
7) Pure Fabrication не стесняемся создавать классы, не имеющие аналог в предметной области, если это необходимо для соблюдения Low Coupling и High Cohesion.
8) Indirection любой класс вызываем через его интерфейс.
9) Protected Variations применяя все вышесказанное, получаем устойчивый к изменениям код.



Читать ещё:


Подробнее..

Не заблудиться в трёх ifах. Рефакторинг ветвящихся условий

01.10.2020 20:16:38 | Автор: admin
На просторах интернета можно найти множество описаний приемов упрощения условных выражений (например, тут). В своей практике я иногда использую комбинацию замены вложенных условных операторов граничным оператором и объединения условных операторов. Обычно она дает красивый результат, когда количество независимых условий и выполняемых выражений заметно меньше количества веток, в которых они комбинируются различными способами. Код будет на C#, но действия одинаковы для любого языка, поддерживающего конструкции if/else.

image

Дано


Есть интерфейс IUnit.

IUnit
public interface IUnit{    string Description { get; }}

И его реализации Piece и Cluster.

Piece
public class Piece : IUnit{    public string Description { get; }    public Piece(string description) =>        Description = description;    public override bool Equals(object obj) =>        Equals(obj as Piece);    public bool Equals(Piece piece) =>        piece != null &&        piece.Description.Equals(Description);    public override int GetHashCode()    {        unchecked        {            var hash = 17;            foreach (var c in Description)                hash = 23 * hash + c.GetHashCode();            return hash;        }    }}

Cluster
public class Cluster : IUnit{    private readonly IReadOnlyList<Piece> pieces;    public IEnumerable<Piece> Pieces => pieces;    public string Description { get; }    public Cluster(IEnumerable<Piece> pieces)    {        if (!pieces.Any())            throw new ArgumentException();        if (pieces.Select(unit => unit.Description).Distinct().Count() > 1)            throw new ArgumentException();        this.pieces = pieces.ToArray();        Description = this.pieces[0].Description;    }    public Cluster(IEnumerable<Cluster> clusters)        : this(clusters.SelectMany(cluster => cluster.Pieces))    {    }    public override bool Equals(object obj) =>        Equals(obj as Cluster);    public bool Equals(Cluster cluster) =>        cluster != null &&        cluster.Description.Equals(Description) &&        cluster.pieces.Count == pieces.Count;    public override int GetHashCode()    {        unchecked        {            var hash = 17;            foreach (var c in Description)                hash = 23 * hash + c.GetHashCode();            hash = 23 * hash + pieces.Count.GetHashCode();            return hash;        }    }}

Также есть класс MergeClusters, который обрабатывает коллекции IUnit и объединяет последовательности совместимых Cluster в один элемент. Поведение класса проверяется тестами.

MergeClusters
public class MergeClusters{    private readonly List<Cluster> buffer = new List<Cluster>();    private List<IUnit> merged;    private readonly IReadOnlyList<IUnit> units;    public IEnumerable<IUnit> Result    {        get        {            if (merged != null)                return merged;            merged = new List<IUnit>();            Merge();            return merged;        }    }    public MergeClusters(IEnumerable<IUnit> units)    {        this.units = units.ToArray();    }    private void Merge()    {        Seed();        for (var i = 1; i < units.Count; i++)            MergeNeighbors(units[i - 1], units[i]);        Flush();    }    private void Seed()    {        if (units[0] is Cluster)            buffer.Add((Cluster)units[0]);        else            merged.Add(units[0]);    }    private void MergeNeighbors(IUnit prev, IUnit next)    {        if (prev is Cluster)        {            if (next is Cluster)            {                if (!prev.Description.Equals(next.Description))                {                    Flush();                }                buffer.Add((Cluster)next);            }            else            {                Flush();                merged.Add(next);            }        }        else        {            if (next is Cluster)            {                buffer.Add((Cluster)next);            }            else            {                merged.Add(next);            }        }    }    private void Flush()    {        if (!buffer.Any())            return;        merged.Add(new Cluster(buffer));        buffer.Clear();    }}

MergeClustersTests
[Fact]public void Result_WhenUnitsStartWithNonclusterAndEndWithCluster_IsCorrect(){    // Arrange    IUnit[] units = new IUnit[]    {        new Piece("some description"),        new Piece("some description"),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),    };    MergeClusters sut = new MergeClusters(units);    // Act    IEnumerable<IUnit> actual = sut.Result;    // Assert    IUnit[] expected = new IUnit[]    {        new Piece("some description"),        new Piece("some description"),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),    };    actual.Should().BeEquivalentTo(expected);}[Fact]public void Result_WhenUnitsStartWithClusterAndEndWithCluster_IsCorrect(){    // Arrange    IUnit[] units = new IUnit[]    {        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),    };    MergeClusters sut = new MergeClusters(units);    // Act    IEnumerable<IUnit> actual = sut.Result;    // Assert    IUnit[] expected = new IUnit[]    {        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),    };    actual.Should().BeEquivalentTo(expected);}[Fact]public void Result_WhenUnitsStartWithClusterAndEndWithNoncluster_IsCorrect(){    // Arrange    IUnit[] units = new IUnit[]    {        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),    };    MergeClusters sut = new MergeClusters(units);    // Act    IEnumerable<IUnit> actual = sut.Result;    // Assert    IUnit[] expected = new IUnit[]    {        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),    };    actual.Should().BeEquivalentTo(expected);}[Fact]public void Result_WhenUnitsStartWithNonclusterAndEndWithNoncluster_IsCorrect(){    // Arrange    IUnit[] units = new IUnit[]    {        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),    };    MergeClusters sut = new MergeClusters(units);    // Act    IEnumerable<IUnit> actual = sut.Result;    // Assert    IUnit[] expected = new IUnit[]    {        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("some description"),                new Piece("some description"),                new Piece("some description"),                new Piece("some description"),            }),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),        new Cluster(            new Piece[]            {                new Piece("another description"),                new Piece("another description"),                new Piece("another description"),                new Piece("another description"),            }),        new Piece("another description"),    };    actual.Should().BeEquivalentTo(expected);}

Нас интересует метод void MergeNeighbors(IUnit, IUnit) класса MergeClusters.

private void MergeNeighbors(IUnit prev, IUnit next){    if (prev is Cluster)    {        if (next is Cluster)        {            if (!prev.Description.Equals(next.Description))            {                Flush();            }            buffer.Add((Cluster)next);        }        else        {            Flush();            merged.Add(next);        }    }    else    {        if (next is Cluster)        {            buffer.Add((Cluster)next);        }        else        {            merged.Add(next);        }    }}

С одной стороны, он работает правильно, но с другой, хотелось бы сделать его более выразительным и по возможности улучшить метрики кода. Метрики будем считать с помощью инструмента Analyze > Calculate Code Metrics, который входит в состав Visual Studio Community. Изначально они имеют значения:

Configuration: DebugMember: MergeNeighbors(IUnit, IUnit) : voidMaintainability Index: 64Cyclomatic Complexity: 5Class Coupling: 4Lines of Source code: 32Lines of Executable code: 10

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

Бородатая шутка по случаю
#392487
Мне недавно рассказали как делают корабли в бутылках. В бутылку засыпают силикатного клея, говна и трясут. Получаются разные странные штуки, иногда корабли.
bash.org

Рефакторинг


Шаг 1


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

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (prev is Cluster)    {        if (next is Cluster)        {            if (!prev.Description.Equals(next.Description))            {                Flush();            }            else            {            }            buffer.Add((Cluster)next);        }        else        {            Flush();            merged.Add(next);        }    }    else    {        if (next is Cluster)        {            buffer.Add((Cluster)next);        }        else        {            merged.Add(next);        }    }}

Шаг 2


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

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (prev is Cluster)    {        if (next is Cluster)        {            if (!prev.Description.Equals(next.Description))            {                Flush();                buffer.Add((Cluster)next);            }            else            {                buffer.Add((Cluster)next);            }        }        else        {            Flush();            merged.Add(next);        }    }    else    {        if (next is Cluster)        {            buffer.Add((Cluster)next);        }        else        {            merged.Add(next);        }    }}

Шаг 3


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

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (prev is Cluster)    {        if (next is Cluster)        {            if (!prev.Description.Equals(next.Description))            {                Flush();                buffer.Add((Cluster)next);            }            if (prev.Description.Equals(next.Description))            {                {                    buffer.Add((Cluster)next);                }            }        }        if (!(next is Cluster))        {            {                Flush();                merged.Add(next);            }        }    }    if (!(prev is Cluster))    {        {            if (next is Cluster)            {                buffer.Add((Cluster)next);            }            if (!(next is Cluster))            {                {                    merged.Add(next);                }            }        }    }}

Шаг 4


Схлопываем блоки.

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (prev is Cluster)    {        if (next is Cluster)        {            if (!prev.Description.Equals(next.Description))            {                Flush();                buffer.Add((Cluster)next);            }            if (prev.Description.Equals(next.Description))            {                buffer.Add((Cluster)next);            }        }        if (!(next is Cluster))        {            Flush();            merged.Add(next);        }    }    if (!(prev is Cluster))    {        if (next is Cluster)        {            buffer.Add((Cluster)next);        }        if (!(next is Cluster))        {            merged.Add(next);        }    }}

Шаг 5


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

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (prev is Cluster)    {        if (next is Cluster)        {            if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)            {                Flush();                buffer.Add((Cluster)next);            }            if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)            {                buffer.Add((Cluster)next);            }        }        if (!(next is Cluster) && prev is Cluster)        {            Flush();            merged.Add(next);        }    }    if (!(prev is Cluster))    {        if (next is Cluster && !(prev is Cluster))        {            buffer.Add((Cluster)next);        }        if (!(next is Cluster) && !(prev is Cluster))        {            merged.Add(next);        }    }}

Шаг 6


Оставляем только блоки if, не имеющие вложенных блоков, сохраняя порядок их появления в коде.

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)    {        Flush();        buffer.Add((Cluster)next);    }    if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)    {        buffer.Add((Cluster)next);    }    if (!(next is Cluster) && prev is Cluster)    {        Flush();        merged.Add(next);    }    if (next is Cluster && !(prev is Cluster))    {        buffer.Add((Cluster)next);    }    if (!(next is Cluster) && !(prev is Cluster))    {        merged.Add(next);    }}

Шаг 7


Для каждого уникального выражения в порядке их появления в коде выписываем содержащие их блоки. При этом другие выражения внутри блоков игнорируем.

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)    {        Flush();    }    if (!(next is Cluster) && prev is Cluster)    {        Flush();    }    if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)    {        buffer.Add((Cluster)next);    }    if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)    {        buffer.Add((Cluster)next);    }    if (next is Cluster && !(prev is Cluster))    {        buffer.Add((Cluster)next);    }    if (!(next is Cluster) && prev is Cluster)    {        merged.Add(next);    }    if (!(next is Cluster) && !(prev is Cluster))    {        merged.Add(next);    }}

Шаг 8


Объединяем блоки с одинаковыми выражениями, применяя к их условиям оператор ||.
Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster ||        !(next is Cluster) && prev is Cluster)    {        Flush();    }    if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster ||        prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster ||        next is Cluster && !(prev is Cluster))    {        buffer.Add((Cluster)next);    }    if (!(next is Cluster) && prev is Cluster ||        !(next is Cluster) && !(prev is Cluster))    {        merged.Add(next);    }}

Шаг 9


Упрощаем условные выражения с помощью правил булевой алгебры.

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (prev is Cluster && !(next is Cluster && prev.Description.Equals(next.Description)))    {        Flush();    }    if (next is Cluster)    {        buffer.Add((Cluster)next);    }    if (!(next is Cluster))    {        merged.Add(next);    }}

Шаг 10


Рихтуем напильником.

Результат
private void MergeNeighbors(IUnit prev, IUnit next){    if (IsEndOfCompatibleClusterSequence(prev, next))        Flush();    if (next is Cluster)        buffer.Add((Cluster)next);    else        merged.Add(next);}private static bool IsEndOfCompatibleClusterSequence(IUnit prev, IUnit next) =>    prev is Cluster && !(next is Cluster && prev.Description.Equals(next.Description));

Итого


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

private void MergeNeighbors(IUnit prev, IUnit next){    if (IsEndOfCompatibleClusterSequence(prev, next))        Flush();    if (next is Cluster)        buffer.Add((Cluster)next);    else        merged.Add(next);}

А метрики так:

Configuration: DebugMember: MergeNeighbors(IUnit, IUnit) : voidMaintainability Index: 82Cyclomatic Complexity: 3Class Coupling: 3Lines of Source code: 10Lines of Executable code: 2

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

P.S. Все куски знаний, которые сложились в описанный в публикации алгоритм, были получены автором еще в школе более 15 лет назад. За что он выражает огромную благодарность учителям-энтузиастам, дающим детям основу нормального образования. Татьяна Алексеевна, Наталья Павловна, если вы вдруг это читаете, большое вам СПАСИБО!
Подробнее..

Не бойтесь кода

18.11.2020 12:16:11 | Автор: admin
Всем привет.

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

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

Вы их легко узнаете, это постоянные пациенты, просто напомню про них:

  1. Архитектура хромает, внедрить новое решение сложно
  2. Ошибки кодирования
  3. Отсутствует автоматическое тестирование

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

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

Разработанный продукт это коллективная экспертиза


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

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

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

Это замкнутый круг 1 экспертизы команды не хватает, но начинается рефакторинг, и делают его, внимание, те же посоны, которые это и написали.

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

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

Люди боятся менять код


Добро пожаловать, это сеанс психологии для программистов.

Программисты реально боятся менять код, и их можно понять:

  1. Тестов не хватает или нет
  2. Как работает код не понятно
  3. Сроки горят
  4. Ресурсов нет
  5. Менеджмент не поймет (обычный, но если управление огонь, всё будет хорошо)

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

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

Проект начинает разваливаться, сроки срываются, новые фичи становятся дико дороже для проекта, разработчики начинают нервничать, некоторые уходят, новых найти сложнее, ну вы поняли

Так вот, я считаю что это проблема 1, страх перед кодом и рисками.

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

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

Совет огонь, все про него знают, но на деле список выше никуда не делся, поэтому нельзя просто взять и отрефакторить, потому что получите проект, который разваливается, а почему?

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

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

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

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

Тесты это ключ


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

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

Поэтому получаем цепочку:
Тесты -> Рефакторинг -> Прощай борода и легаси

Звучит просто, красиво, но на практике тестов бывает мало. Или вообще не бывает, и причин тому несколько, как обычно:

  1. Разработчики считают тесты отдельной темой и не вкладывают в оценки, пишут отдельно от разработки. Еще сложнее, если так думает управление проекта и хотят урезать тесты чтобы сложиться в сроки.
  2. Тесты это время, а проект нужно сдавать сейчас, некогда писать нам тесты (это по идее тоже самое что и пункт 1)
  3. Проект/компонент простой, зачем там тесты, там всё предельно просто и работает?
  4. Сначала код напишем, потом покроем тестами. Но нет, руки не дошли, проект на месте не стоит, времени не нашлось. Так и лежит эта задача в черном ящике веки вечные.

Причин на самом деле миллион, но факт в том, что это блокирует рефакторинг, и, как следствие, не дает качеству расти вверх.

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

Что делать, Хьюстон?


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

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

В результате поймете что тесты это:

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

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

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

P.S. Если есть желание, напишите ваш опыт рефакторинга в комментариях, всем будет интересно.
Подробнее..

Рефакторинг без особой боли

30.11.2020 00:22:29 | Автор: admin

Рефакторинг - практически неотъемлемая часть процесса разработки. Его необходимость связана со следующими предпосылками:

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

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

Обоснованные причины для рефакторинга

  1. Дублирование кода

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

  2. Универсализация

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

    Например, мне нужно было сделать интеграцию платежного шлюза с GooglePay. В процессе выяснилось, что процесс оплаты ApplePay и SamsungPay практически идентичен и изменения в одном из них часто требуются и в двух других. В итоге я сделал оплату для абстрактного VendorPay со стратегиями для каждого метода оплаты

  3. Код не понятен

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

  4. Переусложненный класс/метод

    Для меня достаточно таких формальных признаков, как больше 50 строк в методе или больше 500 строк в классе, чтобы задуматься над рефакторингом

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

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

Почему многие не делают рефакторинг

  • страшно поломать существующую функциональность

  • трудоемко: если код, который ты рефакторишь, меняется, то мержить (многократно) его из основной ветки становится болью

  • есть риск не успеть к дедлайну задачи

Как быть?

Чтобы минимизировать проблемы, связанные с обоснованным рефакторингом, стоит придерживаться следующих приниципов:

  • иметь тесты (unit и регресс) с хорошим покрытием

  • не смешивать пулл реквесты с рефакторингом и бизнес-логикой

  • мержить рефакторинг сразу, не дожидаясь бизнес-задачи

Как я подхожу к рефакторингу

По нашему процессу мы создаем feature-ветку от master'а для каждой задачи.

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

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

После разделения изменения на две ветки я прошу коллег проревьюить ветку рефакторинга, мержу ее в master и делаю rebase бизнес-ветки на master.

Результат

Этот подход несет некоторые накладные расходы, но смягчает основные трудности при рефакторинге:

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

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

  • в случае когда сроки подпирают, можно прекратить делать рефакторинг после очередного двух-трех дневного цикла

Подробнее..

Заметки о codestyle

10.02.2021 14:12:50 | Автор: admin

Довольно часто сталкиваюсь с вопросом касательно качества кода: "Почему написано именно так, а не так?". И я объясняю каждый раз. Именно поэтому решил составить эдакую заметку с некоторыми примерами и объяснениями.

Второй возникающий вопрос: "Где научился так красиво писать?". Ответ на который будет к концу статьи.

Приведу три примера, с которыми сталкиваюсь чаще всего.

Пример 1

use App\Models\Payment;use Illuminate\Database\Eloquent\Model;class Foo extends Model {  public function user(){    return $this->hasOne('App\Models\User');  }  public function payments(){    return $this->hasMany(Payment::class);  }}

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

Начнём с того, что открывающиеся фигурные скобки лучше переносить на новую строку. Это позволит сделать код более приятным в чтении (Привет, PSR-12).

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

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

class Foo{    public function user()    {        //    }}

То у другого он может быть таким:

class Foo{        public function user()        {                //        }}

Или таким:

class Foo{  public function user()  {    //  }}

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

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

Итак, после ревью код будет выглядеть так:

use App\Models\Payment;use App\Models\User;use Illuminate\Database\Eloquent\Model;class Foo extends Model{    public function user()    {        return $this->hasOne(User::class);    }    public function payments()    {        return $this->hasMany(Payment::class);    }}

Пример 2

Недавно встретил такой участок кода (вставлю картинкой, чтобы не нарушить его вид):

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

Данный участок очень плохо читается. Приходится вычитывать каждое слово с целью понять что в нём происходит и не пропустил ли чего при чтении.

Проведём код-ревью:

class NewsService{    public function sendNews(EmployerNews $employerNews)    {        $this->dispatch($employerNews, WorkPositionIdsJob::class);        $this->dispatch($employerNews, WorkPositionTypesJob::class);        $this->dispatch($employerNews, EmployerIdsJob::class);    }    protected function dispatch(EmployerNews $news, string $class): void    {        $job   = $this->job($news, $class);        $delay = $this->delay();        dispatch($job)->delay($delay);    }    protected function job(EmployerNews $news, string $job): AbstractJob    {        return new $job($news->getAttributes());    }    protected function delay(): Carbon    {        return Carbon::now()->addSecond();    }}

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

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

То же самое касается и примерно таких проблемных участков, как:

switch($value){    case 'foo':        //        break;    case 'bar':        //        break;}
if ($employerPriceCategory->default) {    return false;}$defaultCategory = EmployerPriceCategory::where('default', true)    ->first(['id']);Employer::query()    ->where('employer_price_category_id', $employerPriceCategory->id)    ->update([        'employer_price_category_id' => $defaultCategory->id,    ]);
$districts = $this->getDistricts();$this->output->writeln('districts: ' . sizeof($districts));$period = $this->getPeriod();foreach ($period as $start) {    $end = $start->copy()->endOfDay();    $this->output->writeln('date = ' . $start->format('Y-m-d'));    //}

Это же просто ужас! Как такое вообще можно читать? А писать?!

Давайте отрефакторим их!

switch($value){    case 'foo':        //        break;    case 'bar':        //        break;}
if ($employerPriceCategory->default) {    return false;}$defaultCategory = EmployerPriceCategory::query()    ->select(['id'])    ->where('default', true)    ->first();Employer::query()    ->where('employer_price_category_id', $employerPriceCategory->id)    ->update(['employer_price_category_id' => $defaultCategory->id]);

Мы давно пережили времена недостатка места на дисках и не стоит дорожить лишним символом "пробела", который компилятор всё-равно удалит для себя. Это обычная экономия на спичках. Зачем усложнять жизнь не только себе, но и тем, кто после Вас будет поддерживать этот код? Заканчивайте какать там, где едите.

Пример 3

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

Логический блок - это участок кода состоящий из нескольких строк, объединённых между собой какой-либо общей чертой.

Для примера возьмём следующий участок:

$salaryFirstBaseEmployer = $employer->salaryBases->first()->salary_base;$salaryLastBaseEmployer = $employer->salaryBases->last()->salary_base;$begin = Carbon::parse('2020-05-01')->startOfMonth()->startOfDay();$end = $begin->clone()->endOfMonth()->endOfDay();$endMonth = $begin->clone()->endOfMonth();$startPeriod = new CarbonPeriod($begin, $end);$endPeriod = new CarbonPeriod($begin, $end);

В нём мы видим непонятный массив текста. Отформатируем его, разбив на логические блоки:

$salaryFirstBaseEmployer = $employer->salaryBases->first()->salary_base;$salaryLastBaseEmployer  = $employer->salaryBases->last()->salary_base;$begin = Carbon::parse('2020-05-01')->startOfMonth()->startOfDay();$end      = $begin->clone()->endOfMonth()->endOfDay();$endMonth = $begin->clone()->endOfMonth();$startPeriod = new CarbonPeriod($begin, $end);$endPeriod   = new CarbonPeriod($begin, $end);

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

Заключение

Очень давно мне дали дельный совет:

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

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

Подробнее..

Перевод Актуальность принципов SOLID

05.06.2021 22:17:23 | Автор: admin

Впервые принципы SOLID были представлены в 2000 году в статье Design Principles and Design Patterns Роберта Мартина, также известного как Дядюшка Боб.

С тех пор прошло два десятилетия. Возникает вопрос - релевантны ли эти принципы до сих пор?

Перед вами перевод статьи Дядюшки Боба, опубликованной в октябре 2020 года, в которой он рассуждает об актуальности принципов SOLID для современной разработки.

Недавно я получил письмо с примерно следующими соображениями:

Годами знание принципов SOLID было стандартом при найме. От кандидатов ожидалось уверенное владение этими принципами. Однако позже один из наших менеджеров, который уже почти не пишет код, усомнился, разумно ли это. Он утверждал, что принцип открытости-закрытости стал менее важен, так как по большей части мы уже не пишем код для крупных монолитов. А вносить изменения в компактные микросервисы - безопасно и просто.

Принцип подстановки Лисков давно устарел, потому что мы уже не уделяем столько внимания наследованию, сколько уделяли 20 лет назад. Думаю, нам стоит рассмотреть позицию Дена Норса о SOLID - Пишите простой код

В ответ я написал следующее письмо.

Принципы SOLID сегодня остаются такими же актуальными, как они были 20 лет назад (и до этого). Потому что программное обеспечение не особо изменилось за все эти годы, а это в свою очередь следствие того, что программное обеспечение не особо изменилось с 1945 года, когда Тьюринг написал первые строки кода для электронного компьютера. Программное обеспечение - это все еще операторы if, циклы while и операции присваивания - Последовательность, Выбор, Итерация.

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

Итак, пройдемся по принципам по порядку.

SRP - Single Responsibility Principle Принцип единственной ответственности.

Объединяйте вещи, изменяющиеся по одним причинам. Разделяйте вещи, изменяющиеся по разным причинам.

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

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

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

OSP - Open-Closed Principle Принцип открытости-закрытости

Модуль должен быть открытым для расширения, но закрытым для изменения.

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

Или... хотим ли мы отделить абстрактные понятия от деталей реализации? Хотим ли мы изолировать бизнес-правила от надоедливых мелких деталей графического интерфейса, протоколов связи микросервисов и тонкостей поведения различных баз данных? Конечно хотим!

И снова слайд Дэна преподносит это совершенно неправильно.

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

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

LSP - Liskov Substitution Principle Принцип подстановки Лисков

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

Люди (включая меня) допустили ошибку, полагая что речь идет о наследовании. Это не так. Речь о подтипах. Все реализации интерфейсов являются подтипами интерфейса, в том числе при утиной типизации. Каждый пользователь базового интерфейса, объявлен этот интерфейс или подразумевается, должен согласиться с его смыслом. Если реализация сбивает с толку пользователя базового типа, то будут множиться операторы if/switch.

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

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

ISP - Interface Segregation Principle Принцип разделения интерфейса

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

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

Проблема особенно остро стоит в статически типизированных языках, таких как Java, C#, C++, GO, Swift и т.д. Динамически типизированные языки страдают гораздо меньше, но тоже не застрахованы от этого - существование Maven и Leiningen тому доказательство.

Слайд Дэна на эту тему ошибочен.

(Примечание. На слайде Ден обесценивает утверждение Клиенты не должны зависеть от методов, которые они не используют фразой Это же и так правда!!)

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

(Примечание. Речь о фразе Если классу нужно много интерфейсов - упрощайте класс!)

Да, если вы можете разбить класс с двумя интерфейсами на два отдельных класса, то это хорошая идея (SRP). Но такое разделение часто недостижимо и даже нежелательно.

DIP - Dependency Inversion Principle Принцип инверсии зависимостей

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

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

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

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

Подробнее..

Чему можно научиться у фикуса-душителя? Паттерн Strangler

12.06.2021 20:19:26 | Автор: admin

Ссылка на статью в моем блоге

Тропические леса и фикусы-душители

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

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

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

Рефакторинг сервиса приложения доставки продуктов

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

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

Допустим имеются следующие действия, которые у нас хранятся в одной таблице:

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

  • Можно оформитьвозврат товара. Если вам не понравился кефир - вы оформляете возврат и вам возвращают его цену.

  • Можносписать бонусысо счета. В таком случае часть стоимости оплачивается этими бонусами.

  • Начисляются бонусы. Каким-либо алгоритмом нам не важно каким конкретно.

  • Также заказ может бытьзарегистрирован в некотором приложении-партнере(ExternalOrder)

Все перечисленная информация по заказам и пользователям хранится в таблице (пусть она будет называтьсяOrderHistory):

id

operation_type

status

datetime

user_id

order_id

loyality_id

money

234

Order

Open

2021-06-02 12:34

33231

24568

null

1024.00

233

Order

Open

2021-06-02 11:22

124008

236231

null

560.00

232

Refund

null

2021-05-30 07:55

3456245

null

null

-2231.20

231

Order

Closed

2021-05-30 14:24

636327

33231

null

4230.10

230

BonusAccrual

null

2021-05-30 09:37

568458

null

33231

500.00

229

Order

Closed

2021-06-01 11:45

568458

242334

null

544.00

228

BonusWriteOff

null

2021-05-30 22:15

6678678

8798237

null

35.00

227

Order

Closed

2021-05-30 16:22

6678678

8798237

null

640.40

226

Order

Closed

2021-06-01 17:41

456781

2323423

null

5640.00

225

ExternalOrder

Closed

2021-06-01 23:13

368358

98788

null

226.00

Логика такой организации данных вполне справедлива на раннем этапе разработки системы. Ведь наверняка пользователь может посмотреть историю своих действий. Где он одним списком видит что он заказывал, как начислялись и списывались бонусы. В таком случае мы просто выводим записи, относящиеся к нему за указанный диапазон. Организовать в виде одной таблицы банальная экономия на создании дополнительных таблиц, их поддержании. Однако, по мере роста бизнес-логики и добавления новых типов операций число столбцов с null значениями начало расти. Записей в таблице сотни миллионов. Причем распределены они очень неравномерно. В основном это операции открытия и закрытия заказов. Но вот операции начисления бонусов составляют 0.1% от общего числа, однако эти записи используются при расчете новых бонусов, что происходит регулярно.В итоге логика расчета бонусов работает медленнее, чем если бы эти записи хранились в отдельной таблице. Ну и расширять таблицу новыми столбцами не хотелось бы в дальнейшем. Кроме того заказы в закрытом статусе с датой создания более 2 месяцев для бизнес-логики интереса не представляют. Они нужны только для отчетов не более.

И вот возникает идея.Разделить таблицу на две, три или даже больше.

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

Изменение структуры хранения в три этапа

Предположим, что наше legacy монолитное приложение хоть и плохое, но не совсем. Как минимум зарезервировано. То есть работает как минимум два экземпляра. Так, что при падении одного из них - второй продолжит обслуживать пользователей:

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

Оба экземпляра работают с одной базой данных. Реализуя паттернShared Database.

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

Отдельная новая база данных вполне может появиться. Однако не всегда. Ввиду сложностей обеспечения транзакционности между двумя БД. Все в конечном счете зависит от реализации и от ограничений бизнес-логики.

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

BonusOperations:

id

operation_type

datetime

user_id

order_id

loyality_id

money

230

BonusAccrual

2021-05-30 09:37

568458

null

33231

500.00

228

BonusWriteOff

2021-05-30 22:15

6678678

8798237

null

35.00

Отдельную таблицу для данных из внешних систем -ExternalOrders:

id

status

datetime

user_id

order_id

money

225

Closed

2021-06-01 23:13

368358

98788

226.00

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

Для оставшихся типов записей -OrderHistoryArchive(старше 2х недель). Где теперь также можно удалить несколько лишних столбцов.

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

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

Монолит версии 1 и монолит версии 2 вполне могут работать совместно. Лишь для тех запросов, которые обрабатывались монолитом версии 1 в новой базе данных будут пробелы. Эти пробелы, а также недостающие данные можно будет в дальнейшем скопировать отдельным скриптом или утилитой.

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

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

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

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

Выводы

  • ПаттернStranglerпозволяет совершенствовать системы с высокими требованиями к SLA.

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

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

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

Подробнее..

Маленькими шагами к красивым решениям

16.05.2021 12:20:18 | Автор: admin

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

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

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

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

Архитектура системы определяет скорость ее развития. Красивая и правильная архитектура вдохновляет работать и помогает команде разработки.

Вдохновляет простое. К простым и красивым решениям не всегда возможно прийти с первого раза. Ошибка тоже результат.

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

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

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

Модель

Объектная модель должна быть понятна не только разработчикам, но и пользователям.

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

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

Логика

Упрощайте алгоритмы. Делите сложные части на простые. Не переусердствуйте.

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

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

Алгоритм должен легко читаться: в тексте, в любой нотации моделирования, в коде.

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

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

Нейминг решает

Если назвать бокал стаканом, то назначение "пить" вроде бы не меняется, но что-то все-таки не то.

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

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

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

MVP и прототипы

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

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

Рефакторинг

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

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

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

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

Документация

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

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

Выводы

Не усложнять.

Ничего не прятать.

Называть все своими именами.

Не стоять на месте.

Не поддаваться отчаянию, если не получилось.

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

Подробнее..

Перевод Turbolift инструмент для масштабного рефакторинга

08.05.2021 16:18:41 | Автор: admin

Системы Skyscanner сложно назвать маломасштабными. Наш сайт и приложение каждый месяц используются миллионами путешественников, мы обрабатываем умопомрачительные объёмы запросов, используя микросервисную архитектуру, которая сама по себе далеко не маленькая.В общей совокупности у нас задействовано несколько сотен микросервисов и микросайтов (веб-приложений, поддерживающих определённую часть нашего сайта), обслуживаемых сотнями экземпляров AWS Lambda и библиотек. Каждое из этих средств хранится в своём собственном репозитории GitHub, что даёт некоторые преимущества с точки зрения разделения задач, но имеет и свою цену: когда одно и то же изменение нужно выполнить во всех этих репозиториях, как это можно осуществить?


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

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

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

Долгое время мы разрабатывали свою внутреннюю систему под названием Codelift. В первую очередь это была система пакетной обработки, которая в ночное время применяла написанный на Python сценарий изменения для каждого из сотен репозиториев, отправляя предложения на изменения кода в чужих репозиториях (PR-предложения) для всех таких изменений. Но, как оказалось, очень сложно написать такой сценарий, который бы надёжно отрабатывал со всеми репозиториями. Главным узким местом была потребность в квалифицированных специалистах, которые требовались для проверки этих сценариев изменений. И самим сценариям часто требовалось несколько раундов настройки, чтобы преодолеть неизбежные сбои. Система Codelift постепенно выводилась из эксплуатации, но потребность в ней оставалась.

Появление Turbolift

Система Turbolift это переосмысление процесса внесения массовых изменений.

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

  • Подготовка сценариев изменений на Python накладывала свои ограничения: иногда самым простым способом реализации изменения является просто вызов команды из оболочки или запуск более специализированного инструмента рефакторинга, такого как codemod или comby. Иногда предпочтителен вызов редактора или интегрированной среды разработки это будет хоть и тяжеловесным, но самым верным способом. А иногда самым простым вариантом выполнения будет автоматическое изменение, которое сработает для 95 % репозиториев с последующей ручной настройкой для нескольких репозиториев, где такая настройка потребуется.

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

  • Одной из тонких проблем с Codelift было то, что все его PR-предложения исходили от пользователя-бота: при этом от владельцев системы Codelift ожидалась тщательная проверка каждого изменения, и это становилось серьёзным узким местом. Мы поняли, что оптимальным будет создание предложений на изменения кода в чужих репозиториях инженером, который фактически несёт ответственность за их выполнение. В этом случае обеспечиваются прозрачность распределения прав, более простая обратная связь и отсутствие необходимости создавать целую команду посредников.

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

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

Система Turbolift начинала жизнь как наскоро написанный набор bash-сценариев, но она быстро доказала нам свою полезность. Теперь, когда мы переписали эту систему на Go, привели в порядок и сделали её инструментом с открытым исходным кодом, хотелось бы поделиться ею с вами. По сравнению с первоначальной версией язык Go помог сделать этот инструмент более удобным для использования и обслуживании в долгосрочной перспективе. У нас есть множество идей о дальнейшем развитии этого инструмента, и мы приветствуем все поступающие от вас предложения о том, как улучшить его.

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

Как инструмент Turbolift помог нам

  • При приближении момента, когда истечёт срок действия какого-либо внутреннего SSL-сертификата, наша команда сопровождения промышленной платформы использовала Turbolift для выполнения PR-предложений сотен репозиториев, в которых были ссылки на истекающий сертификат.

  • Turbolift применяется нашей командой веб-поддержки для стандартизации версий и тестирования библиотек на наших микросайтах.

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

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

В целом за последние три месяца, используя Turbolift, мы отправили свыше 1200 внутренних предложений на изменения кода в чужих репозиториях. Каждый из этих случаев означает устранённую проблему или исправленную техническую недоработку, которые в противном случае превратились бы в создаваемые вручную PR-предложения. Мы надеемся, что инженеры в Skyscanner и других компаниях в полной мере ощутят преимущества от упрощения рабочего процесса при выполнении масштабных изменений.

Turbolift написан на Go компилируемом языке от Google, который вы за год освоите с нуля на курсе Backend-разработчик на Go от ключевых понятий в IT, основ Linux и до применения Go для DevOps. Мы используем модель фундаментального образования, поэтому вы получите не только практические навыки, но и крепкую теоретическую базу, научитесь мыслить по-новому и в этом вам помогут эксперты в своём деле и менторы, которые с удовольствием ответят на ваши вопросы и передадут вам свои знания.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Это не я! История одного рефакторинга

29.10.2020 10:19:47 | Автор: admin

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

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

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

Но одному модулю было нехорошо.

Аудирование.

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

Вот что предстояло воскресить.Вот что предстояло воскресить.

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

- Мы никак не можем обойтись без листенинга в этом приложении?

- Никак.

- Его давно никто не собирал Доков нет. Растительности нет. Населена роботами.

- Ага.

- А кто его последний трогал?

- Я и трогала. Был прошлой весной один баг в субтитрах

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

Типичный диалог того времени выглядел так:

- Как ты себя чувствуешь?

- Я рефакторю листенинг вторую неделю. Не очень. А кто его писал, кстати?

- Это не я!

И вот почему.

Документация и тесты

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

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

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

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

Код в модуле

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

public abstract class LceActivityWithTracking <T, V extends LceView<T>, P extends LcePresenter<T, ?, ? extends RxUseCase<T, ?>, V>>        extends LceActivity<T,V,P>

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

Скорее всего, написанный код был крайне очевидным для его автора. Для меня он очевидным не был. Зато навёл на не менее очевидную мысль, что лучше бы не забивать на доки, даже если всё кажется очевидным. Возможно, стоит даже писать javadoc ко всему, что вызвало вопросы на код-ревью но это плавный и непрерывный процесс, который сложно исправить одним волевым усилием. Особенно если надо на какое-то время срочно законсервировать модуль.

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

public class GetSubtitlesUseCase extends SerialUseCase<List<SubtitleItem>, SubtitlesIds>

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

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

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

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

Переписывая экраны

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

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

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

Чтобы не сойти с ума, мы созванивались с тимлидом и дробили задачи на более мелкие с более понятным definition of done. Интересные задачи приносили фан сами по себе, с неинтересными и нудными я придумывала челленджи. Жизнь на тот момент так или иначе состояла в основном из четырёх стен, кота и кода. А большую часть времени в четырех стенах естественным образом занимала работа: как деятельность с наибольшей и гарантированной эмоциональной отдачей.

В конце концов модуль, доведённый до примерно рабочего состояния, влился в мастер, мы выдохнули

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

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

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

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

p.s. И да, чувак, которому не повезёт заглянуть в этот модуль следующим. Listening писала не я. Я его рефакторила ;)

Подробнее..

AppCode 2020.3 локализация для Swift, переход к определению до индексации, улучшенные рефакторинги и многое другое

14.12.2020 12:16:00 | Автор: admin

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


КПДВ



Поддержка Swift


Поддержали пачку новых возможностей языка:


  • SE-0279, SE-0286: Multiple trailing closure syntax.
  • Allow synthesis of Equatable and Hashable in conditional conformances (see the SE-0185 amendment).
  • SE-0276: Multi-pattern catch clauses.
  • SE-0269: Increased availability of implicit self in @escaping closures when reference cycles are unlikely to occur.
  • SE-0044: Import as member (OC-20445).
  • SE-0280: Enum cases as protocol witnesses.

Локализация


В AppCode давно есть локализация для строк в Objective-C, в этом релизе реализовали то же самое для Swift:


  • Добавили действие для выделения строки в .strings-файл: Локализация строки
  • Сделали фолдинг для NSLocalizedString: Фолдинг для локализованных строк
  • Реализовали навигацию, автодополнение и поиск использований для ключей локализации.

Действия для изменения кода


Добавили несколько небольших, но полезных действий по модификации кода:


  • Проверку и удаление ненужных self:Проверка и удаление ненужных self
  • Действие для удаления ненужных аргументов в замыканиях: Удаление ненужных списков аргументов
  • Конвертацию замыканий в конце выражения в аргументы метода (и наоборот):Замыкание в аргумент метода
  • Превью для быстрых исправлений: Превью

Change Signature


Rename, который работает для смешанного Objective-C/Swift кода, у нас уже есть. А в этом релизе доработали Change Signature, чтобы он тоже работал сразу же со смешанным кодом. Кроме этого:


  • Добавили выбор типа throw в диалог рефакторинга: Change Signature
  • Стали нормально обрабатывать значения по умолчанию для аргументов и variadic-параметры
  • Стали правильно показывать превью для init-методов.

Rename


Сделали новое отображение для настроек рефакторинга Rename открыть их можно по :


Rename


Переход к определению типа


Работает даже до конца индексации реализовали по тому же принципу, что и автодополнение с использованием SourceKit.


Отладчик


В отладчике появилось несколько полезных платформенных возможностей:


  • Возможность просмотреть поля переменной прямо в редакторе и добавить ее в Inline Watches:
    Inline watches
  • Отображение Inline Watches в табе Variables:Inline Watches
  • Стрелочка счетчика команд, которую можно двигать во время отладки: Program counter

Code With Me


Code With Me


Многие, наверное, слышали про новый сервис от JetBrains для совместного редактирования кода Code With Me. Теперь он работает в AppCode через соответствующий плагин. Подробнее про него можно прочитать вот тут.


Контроль версий


Теперь вместо changelistов можно включить git stage:


Git stage


А Search Everywhere получил новый таб для поиска по коммитам:


Git tab


Поддержка XCFrameworks


Это про сущности из .xcframework теперь они корректно определяются IDE.


Просмотр определения


Возможен прямо из Project view с помощью Space:


Просмотр определения


На этом всё! Все вопросы и пожелания пишите прямо тут в комментариях будем рады ответить!


Команда AppCode

Подробнее..

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

04.12.2020 14:04:54 | Автор: admin

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


Давным давно в далеком далеком проекте появился сервис, отправляющий письмо с новым паролем пользователям. Примерно вот такой:

<?phpclass ReminderPasswordService{    protected function sendToUser($user, $message)    {        $this->getMailer()->send([            'from' => 'admin@example.com',            'to' => $user['email'],            'message' => $message        ]);    }    public function sendReminderPassword($user, $password)    {        $message = $this->prepareMessage($user, $password);        $this->sendToUser($user, $message);    }    protected function prepareMessage($user, $password)    {        $userName = $this->escapeHtml($user['first_name']);        $password = $this->escapeHtml($password);        $message = "Привет {$userName}!        Твой новый пароль {$password}";        $message = $this->format($message);        $message = $this->addHeaderAndFooter($message);        return $message;    }    protected function format($message)    {        return nl2br($message);    }    protected function escapeHtml($string)    {        return htmlentities($string);    }    protected function addHeaderAndFooter($message)    {        $message = "<html><body>{$message}<br>С уважением, Админ!</body>";        return $message;    }    protected function getMailer()    {        return new Mailer('user', 'password', 'smtp.example.com');    }}

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

<?phpclass ReminderPasswordCopyToManagerService extends ReminderPasswordService{    protected function send($user, $message)    {        $this->getMailer()->send([            'from' => 'admin@example.com',            'to' => 'manager@example.com',            'message' => $message        ]);    }    protected function prepareMessage($user, $password)    {        $userName = $this->escapeHtml($user['first_name']);        $message = "Привет {$userName}!        Твой новый пароль ****";        return $message;    }    protected function getMailer()    {        return new Mailer('user2', 'password2', 'smtp.corp.example.com');    }}

Со временем сервис обрастал наследниками, использующими частично его методы, частично новые. В один прекрасный солнечный день пришел менеджер с задачей переключиться с smtp на API популярного сервиса. Класс Mailer уже не подходит, а в коде уже целый зоопарк его упоминаний. Давайте посмотрим на этом этапе, что можно было сделать вначале, чтобы эта задача не превратилась в головную боль?

Dependency Injection (Внедрение зависимостей, DI)

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

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

<?phpclass ReminderPasswordService{    /**     * @var Mailer     */    protected $mailer;    public function __construct(Mailer $mailer)    {        $this->mailer = $mailer;    }    // удалили метод getMailer, заменив его protected свойством $mailer    // ...}

Также большинство популярных реализаций позволяет подменить объект на другой для определенного сервиса, и нашему потомку не требуется уже реализовывать свой getMailer():

<?phpclass ReminderPasswordCopyToManagerService extends ReminderPasswordService{    protected function send($to, $message)    {        $this->mailer->send([            'from' => 'admin@example.com',            'to' => 'manager@example.com',            'message' => $message        ]);    }    protected function prepareMessage($user, $password)    {        $userName = $this->escapeHtml($user['first_name']);        $message = "Привет {$userName}!        Твой новый пароль ****";        return $message;    }}

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

Принцип инверсии зависимостей (Dependency Inversion Principle, DIP)

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

Давайте попробуем переписать код с использованием интерфейсов. Сразу хочу отметить, что написание интерфейсов налагает на нас больше ответственности, и мы должны хорошенько подумать, куда код будет двигаться дальше. С отправкой писем это довольно очевидно: у нас могут появится копии, скрытые копии и другие параметры электронного письма. Поэтому мы не можем просто объявить в интерфейсе мейлера метод

<?phpinterface MailerInterface{    public function send($emailFrom, $emailTo, $message);}

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

<?phpinterface MailMessageInterface{    public function setFrom($from);    public function getFrom();    public function setTo($to);    public function getTo();    public function setMessage($message);    public function getMessage();}

и наш MailSenderInterface, соответственно, обретает вид

<?phpinterface MailerInterface{    public function send(MailMessageInterface $message);}

Но в этом случае нам придется как-то создавать объект MailMessageInterface, и в этом нам поможет фабрика

<?phpinterface MailMessageFactoryInterface{    public function create(): MailMessageInterface;}

Наш сервис, соответственно, обретает такой вид

<?phpclass ReminderPasswordService{    /**     * @var MailerInterface     */    protected $mailer;    /**     * @var MailMessageFactoryInterface     */    protected $messageFactory;    public function __construct(MailerInterface $mailer, MailMessageFactoryInterface $messageFactory)    {        $this->mailer = $mailer;        $this->messageFactory = $messageFactory;    }    protected function send($user, $messageText)    {        $message = $this->messageFactory->create();        $message->setFrom('admin@example.com');        $message->setTo($user['email']);        $message->setMessage($messageText);        $this->mailer->send($message);    }    // далее ничего не менялось    public function sendReminderPassword($user, $password)    {        $message = $this->prepareMessage($user, $password);        $this->sendToUser($user, $message);    }    protected function prepareMessage($user, $password)    {        $userName = $this->escapeHtml($user['first_name']);        $password = $this->escapeHtml($password);        $message = "Привет {$userName}!        Твой новый пароль {$password}";        $message = $this->format($message);        $message = $this->addHeaderAndFooter($message);        return $message;    }    protected function format($message)    {        return nl2br($message);    }    protected function escapeHtml($string)    {        return htmlentities($string);    }    protected function addHeaderAndFooter($message)    {        $message = "<html><body>{$message}<br>С уважением, Админ!</body>";        return $message;    }}

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

<?phpclass ReminderPasswordCopyToManagerService extends ReminderPasswordService{    protected function send($to, $messageText)    {        $message = $this->messageFactory->create();        $message->setFrom('admin@example.com');        $message->setTo('manager@example.com');        $message->setMessage($messageText);        $this->mailer->send($message);    }    protected function prepareMessage($user, $password)    {        $userName = $this->escapeHtml($user['first_name']);        $message = "Привет {$userName}!        Твой новый пароль ****";        return $message;    }}

Наследование VS композиция.

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

Плюсы:

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

2. Легко покрыть тестами маленький кусок логики, а не большой класс с вызовом кучи protected/private методов

3. Легко подменить этот класс другим, если вдруг нам где-то потребуется делать что-то иначе.

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

Для начала хочу привести более понятный и очевидный пример, где композиция выигрывает. Мы имеем сервис, который дергает внешнее API, забирая какие-то данные

<?phpclass SomeAPIService implements SomeAPIServiceInterface{    public function getSomeData($someParam)    {        $someData = [];        // ...        return $someData;    }}

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

<?phpclass SomeApiServiceCached extends SomeAPIService{    public function getSomeData($someParam)    {        $cachedData = $this->getCachedData($someParam);        if ($cachedData === null) {            $cachedData = parent::getSomeData($someParam);            $this->saveToCache($someParam, $cachedData);        }        return $cachedData;    }    // ...}

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

<?phpclass SomeApiServiceCached implements SomeAPIServiceInterface{   private $someApiService;    public function __construct(SomeApiServiceInterface $someApiService)    {        $this->someApiService = $someApiService;    }    public function getSomeData($someParam)    {        $cachedData = $this->getCachedData($someParam);        if ($cachedData === null) {            $cachedData = $this->someApiService->getSomeData($someParam);            $this->saveToCache($someParam, $cachedData);        }        return $cachedData;    }    // ...}

Согласитесь, тут гораздо больше гибкости, да и тесты написать гораздо проще.

Вернемся к нашему старому коду и взглянем на ReminderPasswordCopyToManagerService и посмотрим, что можно вынести "за скобки". Первое, что бросается в глаза - класс наследует ненужные методы addHeaderAndFooter и format, а также метод prepareMessage сильно отличается от родителя (нарушая также принцип открытости-закрытости (Open-Closed Principe), модифицируя, а не расширяя родительский класс), и ему не нужен второй параметр

Общее - тело сообщения, метод escapeHtml.

Давайте попробуем вынести общее в отдельные классы.

<?phpclass ReminderPasswordMessageTextBuilder{    public function buildMessageText($userName, $password)    {        return "Привет {$userName}!        Твой новый пароль {$password}";    }}class Escaper{    public function escapeHtml($string)    {        return htmlentities($string);    }}

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

<?phpclass ReminderPasswordService{    // Обратите внимание, что свойства стали приватными    private $mailer;    private $messageFactory;    private $escaper;    private $messageTextBuilder;    public function __construct(        MailerInterface $mailer,        MailMessageFactoryInterface $messageFactory,        Escaper $escaper,        ReminderPasswordMessageTextBuilder $messageTextBuilder    ) {        $this->mailer = $mailer;        $this->messageFactory = $messageFactory;        $this->escaper = $escaper;        $this->messageTextBuilder = $messageTextBuilder;    }    public function sendReminderPassword($user, $password)    {        $messageText = $this->prepareMessage($user, $password);        $message = $this->messageFactory->create();        $message->setFrom('admin@example.com');        $message->setTo($user['email']);        $message->setMessage($messageText);        $this->mailer->send($message);    }    private function prepareMessage($user, $password)    {        $userName = $this->escaper->escapeHtml($user['first_name']);        $password = $this->escaper->escapeHtml($password);        $message = $this->messageTextBuilder->buildMessageText($userName, $password);        $message = $this->format($message);        $message = $this->addHeaderAndFooter($message);        return $message;    }    // методы ниже тоже будут вынесены в отдельные классы.    private function addHeaderAndFooter($message)    {        $message = "<html><body>{$message}<br>С уважением, Админ!</body>";        return $message;    }    private function format($message)    {        return nl2br($message);    }}

и бывший наследник

<?phpclass ReminderPasswordCopyToManagerService{    private $mailer;    private $messageFactory;    private $escaper;    private $messageTextBuilder;    public function __construct(        MailerInterface $mailer,        MailMessageFactoryInterface $messageFactory,        Escaper $escaper,        ReminderPasswordMessageTextBuilder $messageTextBuilder    ) {        $this->mailer = $mailer;        $this->messageFactory = $messageFactory;        $this->escaper = $escaper;        $this->messageTextBuilder = $messageTextBuilder;    }    public function sendReminderPasswordCopyToManager($user)    {        $messageText = $this->prepareMessage($user);        $message = $this->messageFactory->create();        $message->setFrom('admin@example.com');        $message->setTo($user['email']);        $message->setMessage($messageText);        $this->mailer->send($message);    }    private function prepareMessage($user)    {        $userName = $this->escaper->escapeHtml($user['first_name']);        $message = $this->messageTextBuilder->buildMessageText($userName, '****');        return $message;    }}

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

P.S. конечно же, данные классы еще далеки от идеала, но об этом в другой раз.

Подробнее..

Чистим пхпшный код с помощью DTO

31.05.2021 00:23:47 | Автор: admin

Это моя первая статья, так что ловить камни приготовился.

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

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

Возможно, такой подход в PHP сложился исторически, из-за отсутствия строгой типизации и такого себе ООП. Ведь как по мне, то только с 7 версии можно было более-менее реализовать типизацию+ООП, используя strict_types иtype hinting.

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

$userService->create([      'name' => $object->name,      'phone' => $object->phone,      'email' => $object->email,  ]);

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

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

Собственно, так и появился мой пакет.

Использование ClassTransformer

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

class UserController extends Controller {public function __construct(      private UserService $userService,) {}public function createUser(CreateUserRequest $request){      $dto = ClassTransformer::transform(CreateUserDTO::class, $request);      $user = $this->userService->create($dto);      return response(UserResources::make($user));}}
class CreateUserDTO{    public string $name;    public string $email;    public string $phone;}

В запросе к нам приходит массив параметров: name, phone и email. Пакет просто смотрит есть ли такие параметры у класса, и, если есть, сохраняет значение. В противном случае просто отсеивает их. На входе transform можно передавать не только массив, это может быть другой object, из которого также будут разобраны нужные параметры.

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

class CreateUserDTO{    public string $name;    public string $email;    public string $phone;        public static function transform(mixed $args):CreateUserDTO    {        $dto = new self();        $dto->name = $args['fullName'];        $dto->email = $args['mail'];        $dto->phone = $args['phone'];        return $dto;    }}

Существуют объекты гораздо сложнее, с параметрами определенного класса, либо массивом объектов. Что же с ними? Все просто, указываем параметру в PHPDoc путь к классу и все. В случае массива нужно указать, каких именно объектов этот массив:

class PurchaseDTO{    /** @var array<\DTO\ProductDTO> $products Product list */    public array $products;        /** @var \DTO\UserDTO $user */    public UserDTO $user;}

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

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

Что мы получаем?

  • Метод сервиса работает с конкретным набором данным

  • Знаем все параметры, которые есть у объекта

  • Можно задать типизацию каждому параметру

  • Вызов метода становится проще, за счет удаления приведения вручную

  • В IDE работают все подсказки.

Аналоги

Увы, я не нашел подобных решений. Отмечу лишь пакет от Spatie - https://github.com/spatie/data-transfer-object

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

Я же, в свою очередь, был вдохновлен методом преобразования из NestJS - plainToClass. Такой подход не заставляет реализовывать свои интерфейсы, что позволяет делать преобразования более гибким, и любой набор данных можно привести к любому классу. Хоть массив данных сразу в ORM модель (если прописаны параметры), но лучше так не надо:)

Roadmap

  • Реализовать метод afterTransform, который будет вызываться после инициализации DTO. Это позволит более гибко кастомизировать приведение к классу. В данный момент, если входные ключи отличаются от внутренних DTO, нужно самому описывать метод transform. И если у нас из 20 параметров только у одного отличается ключ, нам придется описать приведение всех 20. А с методом afterTransform мы сможем кастомизировать приведение только нужного нам параметра, а все остальные обработает пакет.

  • Поддержка атрибутов PHP 8

Вот и все.

Подробнее..

Перевод Как заставить руководство проникнуться техническим долгом

18.11.2020 12:16:11 | Автор: admin

Руководство не даёт мне заняться рефакторингом legacy-кода! Знакомая ситуация? Раздражает жутко. Большинство разработчиков рано или поздно сталкивается лбами с менеджером, который совершенно не заинтересован в том, чтобы совершенствовать уже готовое. То нужно реализовать что-то новое, то срочно потушить пожар, то исправить какой-то баг В общем, причина отложить рефакторинг запущенной кодовой базы у них всегда найдётся.

И даже когда пытаешься им объяснять, какие преимущества даёт опрятный код, они то ли не понимают, то ли не хотят понимать. У них только затраты и сроки на уме, а до качества никому нет дела. И получается, что вы абсолютно бессильны что-то сделать с техническим долгом, который всё копится и копится. Программисты работают на прод, а прод на запросы нетерпеливых пользователей. За рефакторинг никто платить не будет. Положение выглядит безнадёжным.

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

Менеджеры не программисты


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

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

Пять аргументов, которые вы можете привести


1) Рефакторинг поможет снизить волатильность предельных издержек на единицу функциональности ПО

Это точная цитата из замечательного выступления Дж. Б. Рейнсберга на конференции на тему Экономика в разработке ПО. Стойте, не уходите! Всё, что здесь сказано, вам прекрасно известно, просто оно сформулировано в абстрактно-заумном духе.

Пойдём по порядку:

  • Рефакторинг поможет снизить пока что всё нормально
  • волатильность иными словами, непредсказуемость
  • предельных издержек то есть сколько ресурса потребует производство ещё одной дополнительной единицы
  • на единицу функциональности ПО она же фича, имеющая ценность для бизнеса. Ура! Ценность для бизнеса это как раз то, что нам нужно.

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

2) За последние месяц 63% ресурса разработки ушло на исправление проблем с качеством продукта

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

Вот несколько метрик, на которые вы можете обратить внимание:

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

Покажите руководству, во что именно выливается для них плохое качество кода. А ещё лучше найдите способ перевести эти цифры в денежный эквивалент.

Как-то я присутствовал на post-mortem бага, которого можно было бы избежать, если бы была проведена проверка типов данных. Код писался на JavaScript. Как раз в то время в компании велись споры о том, стоит ли переходить на TypeScript.

Разработчики, которые разбирались в случившемся, не поленились поднять данные и сумели оценить урон, который баг нанёс бизнесу. Оказалось, что за несколько месяцев своего существования баг высосал из компании миллион канадских долларов. Миллион! Одного этого с головой бы хватило, чтобы окупить стоимость перехода на TypeScript.

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

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

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

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

4) Если инвестировать 10% рабочего времени в качество кода, текучесть кадров существенно снизится.

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

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

А теперь давайте зададимся вопросом: во что компании обходится замена уволившегося разработчика? Нового специалиста нужно найти, провести через процесс найма, ввести в курс дела. Это требует вложений, занимает время и затормаживает всю команду. Руководство однозначно предпочло бы не заниматься подобными перестановками каждый год. Поэтому снижение текучести может стать серьёзным аргументом. А сам факт наличия плана по устранению технического долга сразу даст всей команде +100 к мотивации.

5) Если вложить 20% ресурса в рефакторинг, FRT сократится вдвое и для производительности разработчиков ROI будет положительным

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

Здесь мы проделываем следующее:

  • Выбираем метрики, которые имеют большой вес в техподдержке;
  • Находим пару проблем, которые возникают регулярно и требуют вмешательства разработчиков;
  • Предлагаем план по сокращению числа обращений в техподдержку за счёт устранения исходной причины.
  • Если проблемы такого рода разрешатся, разработчиков реже будут привлекать к задачам, связанным с поддержкой пользователей. То есть у них освободится больше времени, чем было затрачено на рефакторинг, а значит, вложение окупится тот самый положительный ROI.

В конечном счёте, решать им


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

Проводите рефакторинг по ходу дела

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

Правило работает даже для баз с legacy-кодом. Ну ладно, в божеский вид вы её к завтрашнему дню точно не приведете. И пытаться проводить крупномасштабный рефакторинг между делом тоже дохлый номер. Но с таким подходом положение, по крайней мере, не будет ухудшаться. А в работе с lrgacy-кодом приходится ориентироваться не на хорошо, а на лучше.

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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru