Как написать код, который будет понятен всем?

в 11:01, , рубрики: skillbox, Блог компании Skillbox, обучение, Программирование, Проектирование и рефакторинг, Учебный процесс в IT, языки
Как написать код, который будет понятен всем? - 1

От переводчика: Опубликовали для вас статью Камила Лелонека о значении читабельности кода и «эмпатии программистов».

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

«Любой дурак может написать код, который будет понятен машине. Но вот код, который понятен еще и людям, пишут лишь хорошие программисты», — Мартин Фаулер.

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

Skillbox рекомендует: Двухлетний практический курс «Я – Веб-разработчик PRO».

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Недавно я увидел нечто вроде этого:

defmodule Util.Combinators do
  def then(a, b) do
    fn data -> b.(a.(data)) end
  end

  def a ~> b, do: a |> then(b)
end

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

import Util.{Reset, Combinators}

# ...

conn = conn!()

Benchee.run(
  # ...
  time: 40,
  warmup: 10,
  inputs: inputs,
  before_scenario: do_reset!(conn) ~> init,
  formatter_options: %{console: %{extended_statistics: true}}
)

Хммм, похоже, что не только ~> импортируется, но также функции conn!/0 и do_reset!/1. Ок, давайте взглянем на модуль theReset:

defmodule Util.Reset do
  alias EventStore.{Config, Storage.Initializer}
 
  def conn! do
    {:ok, conn} = Config.parsed() |> Config.default_postgrex_opts() |> Postgrex.start_link()
     conn
  end
 
  def do_reset!(conn) do
    fn data ->
      Initializer.reset!(conn)
      data
    end
  end
end

Что касается conn!, то есть парочка способов сделать этот участок проще. Тем не менее останавливаться на этом моменте нет смысла. Я лучше сосредоточусь на do_reset!/1. Эта функция возвращает функцию, которая возвращает аргумент и выполняет сброс для Initializer; да и само имя у нее довольно сложное.

Как написать код, который будет понятен всем? - 2

Я решил выполнить реверс-инжиниринг кода. Согласно документации benchee, before_scenario принимает ввод сценария в качестве аргумента. Возвратное значение становится входным для следующих шагов. Вот что автор, вероятно, имел в виду:

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

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

Первый шаг — явное использование алиасинга вместо имплицированного импорта. Мне никогда не нравились «магические» функции, появляющиеся в моем коде, даже при условии, что Ecto.Query делает запросы элегантными. Теперь наш модуль Connection выглядит следующим образом:

defmodule Benchmarks.Util.Connection do
  alias EventStore.{Config, Storage.Initializer}
 
  def init! do
    with {:ok, conn} =
           Config.parsed()
           |> Config.default_postgrex_opts()
           |> Postgrex.start_link() do
      conn
    end
  end
 
  def reset!(conn),
    do: Initializer.reset!(conn)
end

Далее я решил написать «крючок», как это предлагается в документации:

before_scenario: fn inputs -> inputs end

Все, что осталось сделать, — подготовить данные. Финальный результат таков:

alias Benchmarks.Util.Connection
 
conn = Connection.init!()
 
# ...
 
Benchee.run(
  inputs: inputs,
  before_scenario: fn inputs ->
    Connection.reset!(conn)
 
    init.(inputs)
  end,
  formatter_options: %{console: %{extended_statistics: true}}
)
 
Connection.reset!(conn)

Идеален ли этот код? Наверное, еще нет. Но проще ли он для понимания? Я надеюсь на это. Можно ли было так сделать сразу? Определенно да.

В чем проблема?

Когда предложил решение автору, я услышал: «Круто». Впрочем, на большее я и не рассчитывал.

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

Чтобы убедить других разработчиков делать свой код читабельным, нужно нечто убедительное. И аргумент «я решил переделать твой код, поскольку он малопонятен», не будет воспринят, ответом будет «значит, ты просто плохой разработчик, что я могу поделать ¯_(ツ)_/¯».

Как написать код, который будет понятен всем? - 3

Это (не) проблема менеджмента

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

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

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

Как написать код, который будет понятен всем? - 4

Важная роль эмпатии

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

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

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

Как написать код, который будет понятен всем? - 5

В качестве вывода

Недавно я написал код на Elixir:

result = calculate_results()
Connection.close(conn)
result 

Затем я подумал о методе Ruby tap, который позволяет переписать этот код:

calculate_result().tap do
  Connection.close(conn)
end

ИМХО, было бы лучше сделать это без промежуточной переменной result. Я обдумал, как это можно сделать, и пришел к следующему выводу:

with result = calculate_results(),
     Connection.close(conn),
  do: result 

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

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

В целом, рекомендую использовать следующий принцип: «Когда вы пишете ВАШ код, думайте о ДРУГИХ».

Автор: fokus-lop

Источник

Поделиться

* - обязательные к заполнению поля