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


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

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

Энтропия и ценность программных компонент


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

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

Соответственно, ценность программных компонент можно оценивать как разницу в энтропии, которую они вносят. Тонкие клиенты более надёжны и гибки, но дают меньше ценности. Сложные абстракции долги в написании, неповоротливы, требуют постоянного обслуживания и мониторинга, но могут создать гораздо больше ценности. Если у вас в руках большой сложный внутренний компонент, упрощающий жизнь другим разработчикам, будь то MapReduce-движок или распределённая очередь, вы можете воспринимать себя как этакий маленький бизнес внутри бизнеса, и он тем более ценен, чем больше хлопот берёт на себя. Больше ответственности – более значительный бюджет на поддержание и разработку – более высокий грейд у руководителя (шутка).

Энтропия и качество кода


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

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

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

type Product struct {
    ID    ProductID
    Name  string
    Image *image.Image
    ...
}

type ProductManager interface {
    GetProduct(ProductID) (*Product, error)
}

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

type Product struct {
    ID     ProductID
    ...
    Hidden bool 
}

С точки зрения энтропии проекта в целом это катастрофическое изменение. Везде, где происходит работа с товарами (то есть практически везде вообще) неопределённость удвоилась: к состоянию, когда обрабатывается «обычный» товар, добавляется состояние со скрытым товаром. Теперь везде нужно проверять этот флаг, даже там, где, на первый взгляд, это не нужно – потому что очень скоро на скрытые товары перестанут раскатывать новые фичи, состояние объектов в базе данных в целом будет устаревать, инварианты – переставать соблюдаться и так далее. Вы перестали пускать в продажу товары без картинок и написали код, подразумевающий, что картинка всегда не nil? Устаревшим товарам плевать.

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

type Product struct {
    ID    ProductID
    Name  string
    Image *image.Image
    ...
}

type HiddenProduct struct {
    ID        ProductID
    LastName  string
    LastImage *image.Image
    ...
    HiddenAt  time.Time
    HiddenBy  AdminID
}

type ProductManager interface {
    GetProduct(ProductID) (*Product, error)
    GetHiddenProduct(ProductID) (*HiddenProduct, error)
}

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

Энтропия и оптимизация кода


По самому своему определению информационная энтропия – это показатель того, насколько эффективно можно сжать данные (например, алгоритмом Хаффмана). А насколько эффективно можно обработать данные?

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

Взглянем на такой код.

func (dao *DAO) FindProductsByIDs(ids []ProductID) ([]*Product, error) {
    dbQuery := {"_id": {"$in": convertProductIDsToStrings(ids)}}
    documents, err := dao.mongoCollection.find(dbQuery)
    if err != nil {
        return nil, err
    }
    return convertMongoDocumentsToProducts(documents), nil
}

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

    if len(ids) == 0 {
        return nil, nil
    }

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

То же самое с любыми эвристиками. Эвристика – это всегда полагание на некоторую существующую определённость, в противовес ситуации полной неопределённости, когда эвристика ничего не улучшает.

Вместо заключения. Энтропия и стоимость разработки


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

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

Самый простой код можно будет оформить в виде обычной функции в файле utils. Для более сложного потребуется менеджер с конструктором. В какой-то момент потребуется внешний источник конфигурации. Потом – хранить состояние в персистентном хранилище. Дальше – микросервис, сервера, сетевые доступы, on call дежурный инженер и так далее. Chaos reigns.

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

  • Насколько сильно снижает неопределённость компонент, который я пишу? Могу ли я дать больше гарантий без дополнительных затрат? Могу ли я дать чуть меньше гарантий со значительным падением затрат?
  • Соответствует ли моя оценка времени на разработку этого компонента пользе, которую мы от него ожидаем? Если слишком долго – стоит ли его делать, как вообще эта проблема решается в других местах? Если слишком быстро – точно ли я всё учёл?
  • Насколько сильно растёт количество состояний программы от моего изменения? Могу ли я локализовать этот урон?
  • Насколько ясно я представляю себе источник определённости, позволяющий сделать эту оптимизацию? Будет ли эта определённость с нами завтра? Можно ли выдавить из неё больше?

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

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


  1. xcono
    07.06.2022 15:59
    +1

    Добавление проверки на длину documents в фукнции convertMongoDocumentsToProducts, снимет необходимость лишней проверки на ошибку. Например, так:

    func (dao *DAO) FindProductsByIDs(ids []ProductID) ([]*Product, error) {
        if len(ids) == 0 {
            return nil, nil
        }
        dbQuery := {"_id": {"$in": convertProductIDsToStrings(ids)}}
        documents, err := dao.mongoCollection.find(dbQuery)
        return convertMongoDocumentsToProducts(documents), err
    }

    если:

    func convertMongoDocumentsToProducts(documents []Document) []*Product {
        if len(documents) == 0 {
            return nil
        }
    }

    Конечно, если dao.mongoCollection.find возвращает zero-value при возврате ошибки.

    Энтропия на том же уровне, а пользы больше.


    1. navferty
      07.06.2022 16:26
      +1

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


      1. xcono
        07.06.2022 16:36

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

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


        1. Deosis
          08.06.2022 07:12

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


  1. zloddey
    07.06.2022 16:50
    +1

    Правильная статья, спасибо!

    Но всё же хотел бы немного поспорить относительно этого места:

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

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

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


    1. saluev Автор
      07.06.2022 17:32

      Много кода пишется на автомате и это ок, так что мест, где реально принимается какое-то решение, чаще всего не так много. Так что это не буквально 100% рабочего времени ????

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

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


      1. zloddey
        08.06.2022 08:48
        +1

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

        Но вот несколько лет назад приходилось мне работать над java-монолитом в ~1M SLOC, с десятками взаимосвязанных модулей внутри, с несколькими командами, которые эти модули развивают (человек 50 примерно), с историей изменений лет в 10-15. Прикольно было - но пришлось серьёзно задуматься над тем, как же так работать, чтобы задачи закрывались за разумное время. Тогда и открыл для себя малые решения: мы их принимаем очень много, и каждое имеет свою цену. И если мы хотим поддерживать высокий темп разработки, то вариантов не так уж много. Надо уменьшать либо стоимость принятия решений, либо их число.

        Потому-то такой наброс-намёк и написал в изначальном комментарии.


        1. saluev Автор
          08.06.2022 10:59

          Я работаю над проектом > 1M LOC на Go)


      1. zloddey
        08.06.2022 09:22

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

        Подвожу к ключевому моменту. Люди. Очень. Разные. В одних и тех же ситуациях мы будем принимать совершенно разные решения, опираясь на собственный предыдущий опыт (который существенно отличается). Даже один и тот же человек может легко менять свои привычки и подходы. Например, если прочитает Вашу статью и проникнется важностью борьбы с энтропией. А другой, кто её не читал, будет проникнут важностью чего-то другого - и свои малые решения будет принимать иначе. Представим теперь, что эти два гипотетических человека работают над одним общим кодом. Вопрос: что происходит с энтропией в этом коде?


        1. saluev Автор
          08.06.2022 11:07
          +1

          Что угодно может произойти! Поэтому в проекте должен быть СТО/техлид, одной из онсовных задач которого будет выработка и enforcement общих подходов. А отдельно взятому разработчику нужно стараться обращать внимание на то, какие привычки и подходы доминируют в кодовой базе вокруг него, и либо принимать их, либо убеждать всех вокруг, что нужно делать по-другому.


  1. DmitryKoterov
    08.06.2022 08:57
    +3

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

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

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


  1. aSimpleMan
    09.06.2022 17:49

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


    1. saluev Автор
      09.06.2022 17:50

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