Какаду воспринимают тезис про увеличение энтропии снаружи слишком буквально.
Что такое энтропия в контексте программного кода? Википедия приводит восемь разных определений энтропии в разных науках и все они ограниченно применимы в нашем случае, так что я не берусь дать формализованный ответ на этот вопрос. Но при принятии архитектурных решений и написании кода я всё чаще задумываюсь о последствиях в терминах энтропии, так что приглашаю и вас в свой чудный мир сомнительных аналогий.
Энтропия и ценность программных компонент
Окружающий мир страшен и непредсказуем, и мы постоянно придумываем решения, чтобы уменьшить эту непредсказуемость. Мы готовы платить за это, и многие 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.
Но на каждом этапе продумывания архитектуры и написания кода мы принимаем бесчисленное количество маленьких решений, способных повлиять на энтропию внутри и снаружи проекта. Как понять, какое правильное? Вот несколько вопросов, которые я лично стараюсь задавать сам себе в поисках ответа:
- Насколько сильно снижает неопределённость компонент, который я пишу? Могу ли я дать больше гарантий без дополнительных затрат? Могу ли я дать чуть меньше гарантий со значительным падением затрат?
- Соответствует ли моя оценка времени на разработку этого компонента пользе, которую мы от него ожидаем? Если слишком долго – стоит ли его делать, как вообще эта проблема решается в других местах? Если слишком быстро – точно ли я всё учёл?
- Насколько сильно растёт количество состояний программы от моего изменения? Могу ли я локализовать этот урон?
- Насколько ясно я представляю себе источник определённости, позволяющий сделать эту оптимизацию? Будет ли эта определённость с нами завтра? Можно ли выдавить из неё больше?
Так, постоянно думая о том, породит ли очередная строка кода дополнительный хаос или поможет его сдержать и принимая осознанные, вносящие минимум хаоса решения, мы, разработчики, сможем внести свою лепту в преодоление человечеством прирождённой беспорядочности мира.
Автор: Тигран Салуев