Философия простого ПО. Делаем код простым и понятным

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

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

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

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

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

«Как упростить нашу систему?» — спросите вы. Давайте избавляться от лишнего!

Лишние абстракции

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

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

Мы часто устанавливаем бюрократические ограничения, например, «функция не должна быть длиннее X строк» или «класс не должен содержать более X методов». Да, понять именно маленькие компоненты проще по отдельности, однако становится ли проще понимание всей системы? Зачем нам делить один класс на три, если они всегда будут использоваться вместе? Особенно если все три находятся на одном уровне абстракции и протестировать их вместе так же просто, как и отдельно друг от друга? Ведь чем меньше компонентов в нашей системе, тем проще осознать полную картину происходящего.

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

Рассмотрим пример, где можно избавиться от лишнего метода:

// Исходный вариант
class SubmissionsService {
  public submit(submission) {
    // примерно 50 строк кода на среднем уровне абстракции
    [отправка заявки]
    // уровень абстракции выше, чем у остального кода метода "submit"
    await this.updateJobStatusAfterSubmit()
  }
 
  // Зависимость от порядка вызова:
  //  нужно помнить, что выполнялась основная логика "submit"
  private updateJobStatusAfterSubmit() {
    // зависимость от родительского метода
    // при этом сам метод всего 5 строк кода
    [пара дополнительных проверок, связанных с самой отправкой]
    this.jobsService.update(...)
  }
}
 
// После упрощения:
class SubmissionsService {
  public submit(submission) {
    // после объединения получилось 55 строк кода на среднем уровне абстракции
    [вся логика отправки заявки]
  }
}

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

Я не призываю объединять все приложение в одну огромную процедуру. Однако необходимо осознавать цену деления абстракций на более мелкие. Каждый раз спрашивайте себя: «Станет ли приложение проще после этого изменения?»

Внутреннее состояние

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

Рассмотрим пример:

// Исходный вариант:
class EfficiencyIntervalsValidator {
  public async validateChanges(changes: ChangedInterval[]) {
    await this.generateNewIntervals(changes);
    // зависимость от вызова generateNewIntervals
    this.assertNoDuplicates();
    this.assertNoIntervalGaps();
    return this.intervals; 
  }
 
  private async generateNewIntervals(changes: ChangedInterval[]) {
    const currentIntervals = this.repository.getIntervals();
    // для тестирования нужен репозиторий,
    //  и его нужно либо мокать, либо запускать реальную БД
    [слияние интервалов с изменениями]
    // одну сущность класса нельзя использовать для параллельных вычислений
    this.intervals = mergedIntervals;
  }
 
  private assertNoDuplicates() {
    [проверяется this.intervals]
  }
 
  private assertNoIntervalGaps() {
    [проверяется this.intervals]
  }
}
 
// После упрощения:
class SimpleEfficiencyIntervalsValidator {
  public async validateChanges(changes: ChangedInterval[]) {
    const currentIntervals = await this.repository.getIntervals();
    return applyChangesToIntervals(changes, currentIntervals);
  }
}
// Функция также экспортируется отдельно, ее можно легко протестировать
export function applyChangesToIntervals(changes, intervals) {
  // достаточно единственного прохода по массиву для слияния и валидации
  [применение изменений к интервалам]
}

Итог

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

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

По первому вопросу, ИМХО, идёт весьма спорная игра трейд-оффами, при этом проблема никуда не девается. Хотя с посылом я полностью согласен, с тактикой действий — не очень. CQS же позволяет не тратить на это лишние ресурсы, просто выкинув сервисный слой бизнес логики by default и соответственно его абстракции, оставив by default только инфраструктурный (а-ля посылалки нотификаций или имейлов). Но, важное уточнение — это хорошо заходит именно под микросервисы (либо под идеально раскроенный монолит, что является практически невозможным в реальной жизни :) ).

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

ФП имеет и другие принципы, которые помогают значительно упростить код и систему.
Лучший доклад на эту тему: Simple made Easy

Хорошая статья. Архитектор человек который грамотно борется со сложностью в проекте. Любая система рано или поздно скатится в слабоподдерживаемый трешь. Задача отодвинуть как можно дальше этот час Ч.

надо валить! (к) (тм)

Реально так все і є, дуже корисна стаття. Доречі, ці самі принципи легко масштабуються на будь-яку іншу сферу розробки чи дизайну.
Хочу підняти ще один аспект цієі теми і це можливість підтримувати чи продовжувати розробку розділеними командами; тут ні пам’ять ні документація не допоможе. Інколи, при потребі змін, складно написаний модуль простіше переписати з нуля. В інженерній справі є такий хороший критерій як ремонтнопридатнісь, найкраще про нього може розповісти працівник автосервісу, тут будуть і абстракції в залежності і оббиті пальці :) en.m.wikipedia.org/wiki/Maintainability

Делаем код простым и понятным

То есть другими словами, переходим на Go 👍

Объясню причину: оперативная память человека короткая, она короче, чем этот коммент. Короче, чем этот абзац. Но память гипер-ассоциативна. Грубо говоря, она удерживает не данные, но «зоны видимости» вашей памяти, и всё, с чем вы имеете дело — есть «замыкания» по своей сути.

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

Потому, воодушевляясь простотой и полезностью абстракций, вы очень быстро упрётесь в их цену: они должны быть объектом вашей оперативной памяти. Как в игре FreeCell, очень легко начать, но чем безрассуднее вы начали, тем сложнее будет заканчивать, вплоть до полного тупика. Другими словами, работая с ВАШЕЙ памятью вы должны заканчивать сразу. Не начинать, а заканчивать, делать сразу законченные вещи, избавляясь от промежуточных сущностей вообще совсем. На любом этапе написания кода все абстракции должны иметь законченный вид: вы должны видеть, для чего каждая из них, НЕ ЧИТАЯ остального кода.

Сокращу: абстракции должны быть объектами постоянной памяти, а не временной. Именование и связывание кода — самая сложная часть работы. А продуктом является читаемый код, и это даже важнее работоспособности. Потому что читаемый код довести до работоспособности можно, а вот работоспособный нечитаемый — вы ж не верите, что нигде не ошиблись сами? Что нет ошибок в ТЗ? Что само ТЗ не поменяется (да хоть бы и прямо завтра)? Посему, продукт — это код, а не то, что он делает. А вот то, что он делает — это сервис. Предоставляемый вами, ну или кому вы там дальше ваш код во владение передадите. Он всегда будет сервисом, а продукт практически всегда будет в сервисе нуждаться. ЦЕНА сервиса — едва ли не самый важный параметр. И поскольку на начальных этапах именно вы будете осуществлять сервис (а вам за это не платят, это во многом накладные расходы), то исключительно в ваших интересах снижать себестоимость. А самый дорогой ресурс для вас — время, особенно время, занятое под что-то другое, особенно «прямо сейчас».

А время вы тратите на что? Правильно — на использование той самой маленькой памяти. Которая связывает большую. И чем дороже этот процесс для памяти одного проекта, тем жёстче по вам будет бить МУЛЬТИПЛИКАЦИЯ этой затраты на всё, чем вы занимаетесь. Потому что алгоритмов очистки быстрой памяти у вас нет. А она, чтоб вы понимали, многоуровневая. Забытые связи — как осколки, будут вписываться в каждую новую попытку запомнить то же самое. Считайте, что у вас уязвимость типа Spectre на максималках, весь ваш кеш доступен всем процессам сразу.

————————————————————————————————————————————
Ваш мозг не для работы создан, он жизнью управляет. И чаще, чем вам хотелось бы думать. 100500 путей аппаратных прерываний — вы всерьёз считаете, что вашей работе никогда ничто не помешает, что ваши потоки данных не отравят сотни других? Что вам не придётся в любой момент бросить всё. Что вам не придётся сделать самую затратную операцию — ОБНУЛИТЬ модель данных, просто ради оценки того, что уже сделано? А я напомню, обнуляются при этом только быстрые связи, а вот ассоциации остаются, модель данных представляет из себя осколки. И так каждый раз.

Этот факт усиливает требования: КАК МОЖНО МЕНЬШЕ РАЗРЫВОВ. Чем более целостный, компактный у вас код — тем меньше «осколков» он оставляет в памяти при прерывании. А прервать процесс может что угодно, например переполнение этой самой памяти — она при этом просто и незатейливо очищается, не успев сформировать связи — это естественный механизм фильтрации. Но если связи успели сформироваться — встречайте новые «осколки» конструкций. Которые, повторюсь, будут связываться с новыми конструкциями при каждой попытке повторить процедуру формирования связи.

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

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

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

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

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

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

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

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

Умение писать код — это хард скилл. Более важный, чем всё остальное, что вы когда-нибудь выучите в IT. Приходит умение только при выполнении рефакторинга. Когда вы теряете страх перед нажатием кнопки «Delete».
Умение читать чужой код — это миф. Пишите код так, чтобы это «умение» не потребовалось. В противном случае вы производите не продукт, а говно. А если вам такой код попался — выбивайте полномочия его переписать, или как минимум сделать для него понятную обёртку, если заменить сам яд невозможно.

PPS. Ваша память не подчиняется SOLID. Требования к хорошему коду выше, чем требует SOLID. А если SOLID об них сломается — просто вспомните, что сам solid — продукт работы с уже завершённым зрелым кодом, продукт рефакторинга. С его создания много воды утекло, объёмы кода выросли в десятки раз. А люди остались теми же.

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

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

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

Этим революционером был Borland примерно 20-30 лет назад, когда выпустил Delphi со своим VCL, где предполагалось программирование путём набрасывния компонентов на форму. Всё визуально. Код можно не писать. Думаю, что олдфаги хорошо это помнят.

Думаю, что олдфаги хорошо это помнят.

Работали, помним. Вот только код всё равно надо было писать, и знания WinAPI были не лишние. А вот код, который создаёт элементы управления на форме, писать не надо было. И это упрощало, но не заменяло программирование.

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

У них даже получилось. Если б ещё и баги чинили, цены б им не было. Но увы... «это не баги, это фичи».

Тема Солид не раскрыта. :) а ведь именно он помогает делать более простые и элегантные решения.

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

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

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

// Исходный вариант
class SubmissionsService {
public async submit(submission) {

?

Хорошо, но мало 😀

Простота должна быть во всем.

class SubmissionsService {

let stopWord = "FLÜGGÅӘNKб€ČHIŒßØLĮÊN’;
©

Підписатись на коментарі