Борьба со сложностью является постоянной темой в области создания программного обеспечения, которое я встречаю снова и снова. Это нечто, что я постоянно вижу в обсуждениях на всех уровнях, ну например, как много комментариев должно сопровождать методы и функции? Каково идеальное "количество" абстракции? Когда фреймворк начинает содержать "слишком много магии"? Когда в компании используется слишком много языков программирования?
Мы пытаемся избавиться от сложности, обуздать её и ищем простоты. Я думаю, что представлять себе суть вещей таким образом, — это ложный путь. Сложность обязательно должна где-то обитать.
Есть одна вещь из дисциплины построения устойчивых систем (resilience engineering), которая научила меня концепции необходимой вариативности (requisite variety) из кибернетики: только сложность может держать под контролем другую сложность.
Когда вы имеете дело с инструментами сборки, проявляются и становятся очевидными несколько моментов:
- если вы делаете инструменты сборки простыми, они не могут покрыть все странности граничных случаев, возникающих на практике
- если вы хотите обработать эти странные случаи, вам приходится отходить от соглашений и норм, которые вы бы хотели установить
- если вы хотите облегчить использование типичных случаев, правила для них должны быть созданы как в самом инструменте, так и хорошо известны пользователям, которые подгоняют структуру своих систем в соответствии с ожиданиями системы сборки
- если вы позволяете расширенное конфигурирование или поддержку скриптов, вы даёте пользователям путь для определения новых правил, которые в свою очередь должны быть распространены на остальных пользователей и инструмент сохранит совместимость с их системами
- если вы хотите сохранить инструмент простым, вы вынуждены заставить пользователей играть только по определённым правилам в пределах установленных параметров, так чтобы инструмент остался простым
- если пользовательские варианты использования не отображаются на ваше представление о простоте, пользователи будут создавать обёртки вокруг вашего инструмента, чтобы добиться решения своих задач
И этого никак не избежать. Сложность должна обитать где-то. Она всегда сопровождает разработчиков в процессе решения проблем, независимо от того, осознаёте вы это или нет.
К сожалению, если мы боремся со сложностью подобно последнему пункту в списке (мы всегда боремся с ней тем или иным образом), обёртки становятся частью "ландшафта" нашей системы. Сложность не ложится в спячку. Она становится частью общего приобретаемого опыта, и разработчики с пользователями вынуждены приспосабливаться к ней.
Они создают временные решения, когда видят несоответствия между парой конфликтующих концепций. Эта необходимая сложность может быть перемещена куда-то — назад в инструмент (или новый инструмент) — или выдавлена путём реконструкции частей. Каждое такое структурное изменение требует определённых усилий и дополнительных подгонок для того, чтобы люди увидели сложность, поняли сложность и научились справляться со сложностью. И в некоторых случаях сделанные изменения не упрощают сущности, они усложняют их создавая новые несоответствия между представлениями в головах разных людей, и это несоответствие порождает новые обёртки. Непреднамеренная сложность (accidental complexity) является просто необходимой сложностью (essential complexity), в которой проявился её возраст. Сложность невозможно избежать, она всё время меняется. Сложность должна обитать где-то.
В книге Дизайн привычных вещей, Дон Норман упоминает о концепциях "Внутренних знаний" (Knowledge in the head) и "Внешней информации" (Knowledge in the world) (подобные концепции в академическом исследовании представлены в работе Roesler & Woods Designing for Expertise). Внутренние знания — это факты и связи между ними, которые вы узнали, изучили и которые находятся в вашей памяти. Внешняя информация — это всё остальное: информация в виде текста, знаки в дизайне (вы узнаёте кнопку включения питания просто смотря на её символ и знаете, что она может быть нажата, поскольку выглядит как кнопка) и прочее. Одна нетривиальная вещь состоит в том, что интерпретация внешней информации зависит от культуры контекста, и, вдобавок, она основана на внутренних знаниях (вы знаете, что кнопка включения питания может быть нажата потому, что вы вообще знаете о кнопке в принципе).
В некотором смысле, экспертиза это обладание такими внутренними знаниями, которые позволяют лучше понимать информацию из внешнего мира.
Типичная ловушка, с которой мы сталкиваемся в процессе проектирования ПО, проявляется в фокусировании на том, насколько "простым" является для нас чтение и понимание конкретного фрагмента кода. Однако фокусирование на простоте чревато множеством затруднений, поскольку сложность не может быть удалена: она может быть только перемещена. Если вы её перемещаете из своего кода, куда она тогда денется?
Когда мы проектировали Rebar3, мы ощущали что инструмент может быть сделан простым в использовании. Отправной точкой для этой простоты являлось общее соглашение о предполагаемой структуре проектов на Erlang/OTP. Пока вы следуете этим правилам, всё отлично работает. Мы вынесли некоторую часть сложности на внешний уровень. Правила всегда были бы изучены (как мы думали), но теперь инструмент зависел от понимания и принятия всеми этих правил. В погоне за упрощением использования инструмента для разработчиков уже освоивших эти правила, мы усложнили использование для новичков и тех, кто ещё находился в процессе изучения. Другие инструменты самого разного назначения в других экосистемах — подобно нам — также выбирают свои компромиссы.
Эта ловушка скрыта и в архитектуре ПО. Когда мы перенимаем какую-то технологию, скажем, микросервисы, мы пытаемся сделать их так, что каждый сервис в отдельности был бы достаточно прост. Но как только эта простота станет чересчур ограничивающей, а ваше приложение будет стремиться вырваться за принудительные рамки простоты, оно должно развиваться куда-то. Если оно не уложится в каждый из микросервисов, то куда оно денется?
Сложность должна обитать где-то. Если вам повезло, она живёт в хорошо очерченных местах. В код, куда вы решили поместить часть сложности, в документацию, которая описывает поведение кода, в обучающие материалы для ваших инженеров. Вы выделяете место без попыток скрыть всю её. Вы создаёте способы управления сложностью. Вы знаете куда нужно смотреть, чтобы разобраться с ней, когда вам это понадобится. Если вам не так повезло и вы просто попытались притвориться, что сложности можно полностью избежать, ей будет некуда деться в этом мире. Но это не значит, что перестанет существовать.
Не имея какого-то определённого места, она расползётся по всем уголкам внутри вашей системы, и в вашем коде и в головах ваших коллег. А поскольку люди меняют интересы, перемещаются и уходят из проекта, понимание сложности разъедается со временем.
Сложность должна обитать где-то. Если вы принимаете её и отводите для неё подобающее место, соответственно ей проектируете вашу систему и отношения внутри организации, сосредотачиваетесь на приспособлении к ней, зная, что она существует, сложность может однажды стать сильной чертой вашей системы.
mospan-n
Если под «сложностью» понимать именно сложность присущую самой задаче (inherent complexity), то, безусловно, такую сложность надо распределять между абстракциями, где абстракции более высокого уровня полагаются на корректную реализацию соответствующего низкоуровнего API.
Когда речь идёт об упрощении, часто имеют в виду так называемую accidental complexity — то есть искусственно привнесённая сложность в силу разного рода причини: недостаток знаний технологии, сжатые сроки и.т.д. Эти «сложности» могут накапливаться, не принося никакого нового функционала продукту. Их пытаются описать в терминах технического долга и убедить владельцев продукта на выделение времени на их устранение (рефакторинг).
nlinker Автор
В-общем согласен, только замечу, что непреднамеренная сложность (accidental complexity) является обычной необходимой сложностью, которую вовремя не обнаружили и не разложили по необходимым абстракциям (или не создали необходимые) и не задокументировали.
Люди не стараются себе специально усложнить жизнь и привнести побольше сложностей, обычно то, что мы называем непреднамеренной сложностью появляется из-за того, что они слишком упростили штуки в других местах, нет?
funca
Есть два способа борьбы со сложностью — инновации и переиспользвание. На днях переписал свой старый пет проект на современный лад и закрыл несколько ишью на гитхабе. Бандл по объему уменьшился раз в пять.
Благодаря инновациям мир с каждым днём становится стандартнее и проще, превращая когда-то essential complexity в accidental. А с этим бороться все умеют. Нужно лишь периодически собираться силой воли и устраивать генеральную уборку.
Hardcoin
Нет, не является. Простой пример — римские числа. Их сложно умножать (без перевода предварительно в арабские), но эта сложность не является необходимой. Просто эти числа реализованы криво.
nlinker Автор
Я правильно понимаю, что вы считаете, что римские числа обладали некоей сложностью, а при переходе к арабским позиционным числам часть сложности просто исчезла?
Hardcoin
Работа с римскими числами имела лишнюю сложность, потому что они неудачно сделаны.
Да, эта лишняя сложность просто исчезла. Она не была внутренней сложностью домена "умножение чисел", она была случайной, ненужной. Вы, вероятно, имеете ввиду какой-то "закон сохранения сложности", однако его нет. Сложность легко сделать на пустом месте, из ничего, специально.
mvv-rus
«Здесь не всё так однозначно»(с).
Римские числа неудачно сделаны для одной задачи — для умножения, но зато удачно — для другой: для быстрой оценки порядка величины числа.
В римской записи порядок виден сразу (если сначала идет M, то сразу понятно что порядок величины — тысячи), а в позиционной для надо посчитать цифры (понимаю, что нам до четырех посчитать несложно — мы привычные, но тем не менее).
andreyverbin
С римскими числами верно подмечено. Вы думаете нельзя придумать откровенно неудачную реализацию? Такую, сложность которой невозможно оправдать никак.
mvv-rus
Можно. Даже ничего особо самим придумывать не надо — достаточно читать Хабр: Enterprise-версия программы FizzBuzz с правильной архитектурой
aml
Вот кстати это как раз хороший пример получился того, как зачастую разработчики решают не ту проблему (более общую, более сложную, просто другую), и в итоге решение накапливает в себе сложность предметной области, которой оно касаться не должно.
Например, нужно было придумать систему для простого умножения, а разработчик зачем-то добавил туда возможность лёгкой оценки порядка величины. Или наоборот.
В итоге решение получилось более сложным, чем могло быть.
Hardcoin
Верно. А если C — то сотни. Например, CMXXXVII — сразу видно, что порядок около сотни, не так ли?
На самом деле нет, это 937. То есть даже вашу удачную задачу римские цифры решают так себе, кое-как.
Kroid
Формально говоря, 937 — это сотни. Меньше одной тысячи ведь.
mayorovp
Формально говоря, десятичный логарифм 937 округляется до трёх, а не до двух.
Kroid
А кто говорит про логарифм и округление? Есть два слова: «сотни» и «тысячи». Вы считаете, что число 937 лучше характеризует второе?
mayorovp
Да.
Kroid
Занятно.
Hardcoin
Если вам нужно узнать, число меньше тысячи или нет, то вы правы. 999 тоже меньше тысячи.
На практике, оно из частых использований чисел — это цены. Вы согласитесь с фразой "это стоит несколько сотен" при цене 937? Или если скажут "они примерно одинаковые по цене, CXXXVII и CMXXXVII". Так что для нормальной оценки вам совершенно точно не хватит первой цифры.
Kroid
Я смотрю, тут никто не заметил, что дается определение во множественном числе. Да, 999 — это ближе к тысяче, а не к сотне. Но это же число ближе к сотням, чем к тысячам. Ну да ладно, естественные языки никогда не были абсолютно точными, чтобы всерьез спорить об этом.
Hardcoin
Суть не в лингвистике, а а цели, которую вы перед собой ставите. Если "определить разрядность небольшого числа", то, по всей видимости это действительно юзкейс, когда римские цифры проще (не нужно считать количество цифр).
Не могу представить ситуацию, что бы именно это было настолько важно, что бы на самом деле использовать римские числа, но забавно.
vlti
Для арабских цифр придумали хак для быстрой оценки порядка — экспоненциальную форму записи числа. И тут вроде умножается хорошо и порядок виден, но чтобы сложить — числа нужно нормализовать.
Получается есть сохраняющаяся сложность — это полезная информация. А сложность, «которую невозможно оправдать», — это помеха. И методики чистки кода пытаются снизить уровень «помехи». Неудачное применение методик может привести не к снижению уровня помехи, а просто к преобразованию, в лучшем случае, «помехи» из одного вида в другой.
nlinker Автор
Со случаем римских чисел согласиться не могу, в данном случае часть сложности, которая ушла от перехода от римских чисел к позиционным арабским переместилась в систему образования, когда школьники до 4-го класса учатся делать простые арифметические операции с ними (в то время как в детском саду легко складывают/вычитают на палочках) при этом правильно учитывая переносы в разрядах. То есть сложность частично переместилась на уровень документации и обучения.
Но есть в каком-то смысле экстремальный случай — это, например, обфускация. И здесь я соглашусь, что это похоже на создание большой сложности из воздуха, которую мы затем можем с определёнными усилиями постепенно уменьшать, распутывая шаг за шагом этот обфусцированный код. Но бесконечно упрощать мы всё равно не сможем, это просто противоречит теории информации.
Я представляю этот процесс примерно как у нас есть система определённой сложности, обладающей некоторой энтропией. С помощью рефакторинга мы можем спуститься в локальный минимум энтропии. Но чтобы опускаться ещё ниже, нам нужно переделать и, например, перестроить код на других абстракциях (скажем, перейти от коллбэков к асинкам). При этом, как говорит автор статьи, часть сложности перейдёт на уровень документации и обучения, но код мы можем разгрузить довольно сильно, рефакторингами снова скатившись в локальный минимум энтропии.
Но всегда есть нижняя граница сложности, ниже которой мы не опустимся. Поэтому её (сложность) нужно будет куда-то растолкать. Как-то так.
Hardcoin
Вы утверждаете, что третьекласснику научиться складывать римские цифры будет проще, чем арабские? Римские цифры — это не просто палочки пересчитать, там есть ещё пара правил, не таких уж простых для третьеклассника. XXIV + XXI простым подсчетом палочек не посчитаешь.
Я ничего не говорил про бесконечность. Мой тезис был — в некоторых ситуациях мы можем упростить. Не переместить, не обменять сложность, а убрать.
+1. Если вы уверены, что текущий код более-менее близок к локальному минимуму, тогда пора распределять сложность. Но не ранее.
qw1
Новички часто могут писать излишне сложно, например
вместо
Римские цифры из той же серии. Неудачное решение, которое можно заменить на простое, не перераспределяя сложность куда-то ещё.
bogolt
если в си кто-то напишет
это будет очень плохо. сравнение двух указателей почти всегда окажется ложью. Не всегда более длинная запись признак новчика, иногда напротив человека который слишком часто обжигался.
qw1
смотря в каком языке.
это точно не Си, как вы могли бы заметить по названию ф-ции length
VolCh
Некоторые предпочитают на разных языках писать плюс-минус одинаково, пускай и избыточное для некоторых из них.
qw1
bogolt
костыль понятный людям из любого языка, без оглядки на бэкграунд. Примерно как наследние си/си++ заставляет людей писать
вместо казалось бы более логичной
просто чтобы случайно в условии не совершить ошибку и не присвоить значение вместо сравнения.
qw1
Тем не менее, это ли не пример привнесённой сложности?
Hardcoin
Length — это сложный костыль? Нет никакого преимущества в записи str != "". Есть желание, пишите так, не проблема. Но и пользы нет.
qw1
Синтаксическое дерево сложнее (больше операций) — значит, труднее читать код человеку. Умный компилятор, конечно, может считать, что это одно и то же.