Программисты должны быть параноиками.

  • «Я дважды проверил код»
  • «Код проходит все тесты»
  • «Ревьюер одобрил мой код»

«Так ли корректен мой код?»

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

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

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

▍ Абстракции


Что такое «расширение понимания»? Давайте рассмотрим одну из граней понимания, которая часто актуальна для программистов: абстракции.

Абстракции…

  • это мысленные модели того, как работает некая система;
  • когда мы относимся к объекту А, как если бы он был объектом Б;
  • если говорить метафорически…
    • это результат сжатия данных, происходящего в голове,
    • способность разглядеть лес за деревьями,

  • используются постоянно в повседневной жизни.

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

Примеры абстракции:

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

  • Мы воспринимаем время, как если бы оно текло для всех с одной скоростью.
    • Замедление времени немного меняет течение времени для каждого человека/объекта на основании его скорости и влияющей на него силы гравитации.
    • Кружащиеся вокруг Земли GPS-спутники ежедневно подстраивают свои часы примерно на 38 микросекунды, чтобы учитывать в расчётах эффект замедления времени (источник).
    • Эта абстракция работает, потому что влияние замедления времени слишком незначительно, чтобы заметить его, если только мы не занимаемся чрезвычайно точными расчётами.

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

  • зажигание запускает машину;
  • педаль газа заставляет машину двигаться;
  • педаль тормоза заставляет машину остановиться;
  • рулевое колесо поворачивает машину;
  • машине нужен бензин/дизельное топливо.

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

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

  • Базовые особенности языка (циклы, условные операторы, функции, операторы и выражения) — это абстракции, скрывающие:
    • Подробности аппаратного уровня: команды CPU, регистры, флаги и детали, относящиеся к архитектуре CPU…
    • Подробности уровня операционной системы: управление стеком вызовов, управление памятью…

  • Портируемость: языки абстрагируют необходимость разбираться в различиях между машинами.
    • Любая скомпилированная программа на Java (например, файл jar) должна иметь возможность запускаться на любой машине с установленным Java Runtime Environment (например, JVM).
    • Скрипт на Python должен иметь возможность запускаться на любой машине с интерпретатором Python.
    • Программа на C должна иметь возможность компилироваться и запускаться на любой машине, если на этой машине есть компилятор C.

▍ Сбой абстракций


К сожалению, абстракции иногда нас подводят.

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

Абстракция водителя хорошо работает в короткой перспективе (на одну поездку), но приводит к неудаче в длительной перспективе (много лет). Джоел Спольски называет такие неисправные абстракции «протекающими»; он придумал закон протекающих абстракций:

Все нетривиальные абстракции в той или иной степени оказываются протекающими.

Этот закон похож на максиму из статистики:

Все модели ошибочны, но некоторые из них полезны.

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

  • Сборка мусора избавляет от необходимости заниматься управлением памятью (если только для нас не важны колебания задержек).
  • Умные указатели в C++ делают память безопасной (если вы не храните сырые указатели из неё).
  • Хэш-таблицы быстры, потому что имеют операции O(1) (но при малых размерах массивы быстрее).
  • Передача по ссылке быстрее, чем передача по значению (за исключением случаев пропуска копирования и значений наподобие int, умещающихся в регистры CPU).

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

▍ Press X to doubt


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

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

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

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

▍ Доверяй, но проверяй


Программист должен исповедовать принцип «доверяй, но проверяй».

Вот несколько примеров:

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

▍ Опасайтесь неизвестных неизвестных


Самая страшная проблема с точки зрения знания для программистов — это «неизвестное неизвестное».

Существует…

  • то, что мы знаем (то есть «известное»);
  • то, что мы знаем, что мы не знаем («известные неизвестные»);
  • то, что мы даже не знаем, что мы не знаем («неизвестные неизвестные»);

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

Возможно, вы никогда не слышали…

  • О санации пользовательского ввода
    • Если вы используете переданные пользователем строки как часть SQL-запроса, то ваш сервис могут взломать SQL-инъекциями.

  • О кодировке символов
    • Все текстовые данные, обрабатываемые вашим кодом, должны использовать кодировку символов (например, ASCII, UTF-8, UTF-32 и так далее), которую ожидает/поддерживает код.
    • Произвольный доступ к символу в текстовом буфере может занимать константное время (в случае ASCII) или линейное время (UTF-8), в зависимости от кодировки символов.
    • Если вы попытаетесь считывать текстовые данные при помощи не той кодировки, то на выводе могут образоваться непонятные символы.

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

Если раньше вы не слышали об этих темах, то можете даже не понимать, когда попадёте в их ловушки.

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

При изучении незнакомой платформы/языка/инструмента/библиотеки/технологии или работе с ними:

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

  • Читайте посты в блогах.
  • Читайте исходный код.
  • Развивайте своё понимание абстракций, с которыми вам нужно работать.
    • Изучайте фичи, недавно добавленные в ваш язык программирования.
    • Исследуйте все публичные функции библиотек, а не только те, которыми пользуетесь.
    • Просмотрите все флаги на странице man CLI-инструмента.

  • Изучите хотя бы на один слой абстракции ниже необходимого.
    • Узнайте об оптимизациях вашего компилятора.
    • Если вы управляете сервисом, изучите его платформу оркестровки (например, Kubernetes).
    • Если вы работаете с Java, изучите JVM.
    • Если вы работаете с Python, изучите интерпретатор Python.

▍ Заключение


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

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

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

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (1)


  1. ZontikFromRuSO
    02.07.2024 07:14

    Любая скомпилированная программа на Java (например, файл jar) должна иметь возможность запускаться на любой машине с установленным Java Runtime Environment (например, JVM).

    JVM — часть JRE, а не её частный случай, разве нет?