У разработчиков – и это включает меня – полным полно отмазок на тему того, почему та или иная задача не сделана. Обычно виной тому служит компилятор, библиотека, IDE или внешние факторы вроде выход Diablo 3. На самом же деле, самая существенная, значимая, серьезная и в то же время неизученная причина, по которой мы «тормозим» – это отсутствие вменяемого механизма для борьбы со сложностью.
В каком-то смысле, наша неспособность понимать и управлять витиеватыми конструктами мышления является ненавистной самой природе, которая позволяет функционировать структурам, намного превышающим по сложности какую-то там ERP-систему или сайт, который мы пишем. Наши собственные тела, обусловленные через ДНК, являются хорошим тому примером.
Конечно, проводить полноценные аналогии между конструкцией программного обеспечения и конструкций вроде разумных биологических существ столь же корректно, как пытаться сравнивать архитектуру ПО с архитектурой строительной. С таким подходом, можно смело редуцировать все к антропоцентризму и тем самым утверждать, что борьба со сложностью является борьбой с эго, коллективным подсознательным, или каким-нибудь еще Фрейдопорожденным психологическим конструктом.
Тем временем, на планете Земля, сложность реально мешает разработчикам жить и творить в полную силу. Я думаю можно смело утверждать, что конкретно языки программирования не несут сколь либо значимой вины, т.к. на сегодняшний день нам доступны различные механизмы построения собственных языков, если нам таковые нужны. Нет, я осмелюсь утверждать, что проблема сложности влияет на нас на более высоком, структурном уровне. Иначе говоря, я считаю, что причина нашей неспособности «объять необъятное» кроется в неэффективном представлении кода, вне зависимости от языка.
Проблемы структуризации
В .NET есть ряд весьма неприятных «канонов» в плане взаимодействия с кодом. Во-первых, у нас есть негласное правило под названием «one class per file», т.е. идея о том, что в файле может быть только один класс. Я сколько не думал, так и не мог привести вменяемых аргументов в пользу этого правила. Должен признаться, что я уже давно его не придерживаюсь, разве что для конкретных задач (например, динамического прототипирования, где это критично).
Вторая, сопряженная, проблема заключается в том, что нынче не модно делать «композицию», т.е. вложенные классы. Опять же, мне вспоминается пример с машиной и колесами: если у вас в программе колеса есть только у машины, почему не сделать класс Wheel
вложенным в класс Car
? Потому что не модно. Нынче агрегирование якобы лучше чем композиция.
Аналогичная проблема встречается в алгоритмах, состоящих из нескольких частей. Если все эти части – внешние функции, то у нас получился не перевариваемый API. Хочется сделать функции внутренними? Пожалуйста, только вывод типов тут работать не будет. Да и то, как выглядит функция на 100 строк, где 50 из них – это какой-нибудь Action – не дает никаких особых преимуществ. Как было нечитабельно, так нечитабельно и осталось.
Фундаментальная проблема всего этого вовсе не в том, что мы не умеем читать код. Если бы проблема была в этом, мы бы уже все поголовно использовали идею грамотного программирования Кнута. Проблема не в том, что код нечитабелен, а в том что мы неспособны поделить код на такие куски, которые можно было воспринимать в совокупности.
Так как описать сложный алгоритм?
С одной стороны, мы не можем отказаться от принципа «разделяй и властвуй», т.е. единственный шанс понять что-то сложное – разбить его на несколько кусочков, потом разбить эти кусочки, и так далее ad nauseam. Проблема лишь в том, как потом заставить эти 1000 кусочков не лезть нам в глаза со строк тысячестрочного файла?
Я предлагаю начать с простого – пересмотреть наше определение алгоритма. Мы привыкли, что алгоритм – это функция (метод). Давайте выкинем это определение в помойку. Алгоритм отныне будет классом. Если он нам вдруг понадобился, то мы инстанциируем этот класс (никакой статики, но можно положить синглтон в контейнер) и вызываем какой-то метод. Соответственно, мы уже изолировали алгоритм от той формочки или контроллера, который его использует.
Следующий шаг – это разбить алгоритм на отдельные классы и методы внутри этого класса, но ни в коем случае не позволять классам иметь shared state. Общее состояние – одна из причин, по которым нам не угнаться за сложностью системы. Соответственно, если наш алгоритм состоит из 2-х частей и эти части используют одни и те же данные, мы пишем что-то вроде этого:
class MyAlgo { MyAlgoResult Step1() { Foo foo = new Foo(); // change foo, then if (foo.Baz < 0) return MyAlgoResult.Fubar; return Step2(foo); } MyAlgoResult Step2(Foo foo) { var bar = foo.DoStuff(); return MyAlgoResult.Ok; } enum MyAlgoResult { Ok, Fubar }; }
В качестве визуального примера этого подхода хочу показать диаграмму вызовов, сгенерированную Visual Studio. Диаграмма хоть и неидеальная, но суть подхода показывает:
К сожалению, разбить алгоритм на методы – мало. Обычно нужны не только внутренние методы, но также внутренние классы. Более того, некоторые алгоритмы не делятся ровно. Например, всякие if-ы, switch-и и прочие частично ломают наш подход. Иногда их можно оставить в покое, но иногда их можно эмулировать с помощью различных ООП структур.
Я должен пояснить: я не предлагаю перерабатывать алгоритмы в некие аналоги Workflow Foundation. Я предлагаю лишь переписать их так, чтобы, с одной стороны, для потребителя алгоритма был один унифицированный интерфейс, но чтобы сам алгоритм являлся гибкой композиционной структурой. Это в свою очередь означает, что алгоритм может быть классом, у которого лишь один метод публичный, а остальные – приватные.
Столпотворение структур
У нашего подхода есть и обратная, вполне очевидная сторона: он плодит сущности. И если нам нужно в каком-то месте воспользоваться 10-ю различными сущностями, это становится проблемой – не знаю как вам, а мне не очень-то импонирует конструктор с 10 параметрами. Чтобы решить эту проблему, можно использовать т.н. «бандлы» – т.е. мы создаем класс-хранилище, который существует только для того, чтобы через конструктор инициализировать некий набор сервисов. Теперь, все что остается сделать – это инъецировать именно этот класс в нужное место и вуаля! Конечно, вложенные объекты придется вызывать через точку, что тоже не способствует экономии пространства.
Еще одна структура (а точнее может одна, а может у вас их несколько) – это классы для хранения методов расширения. Методы расширения нужны по ряду причин, и я уже публиковал пост на тему паттернов которые встречаются чаще всего. Методы расширения способны существенно улучшить читаемость. Например, что проще понять – Math.Round(Math.Abs(x))
или x.AbsRound()
?
Еще одна полезная практика – это перегружать оператор индексирования []
для всех типов, в которых есть одна основная перечисляемая коллекция. Разница между info.Points[i]
и info[i]
весьма заметна, особенно если это нужно писать в 10 разных местах.
Что дальше?
Мне все больше и больше кажется, что использование файлов для хранения кода – это эволюционный тупик. Не существует никаких объективных причин связывать логическую и физическую структуру кода. Более того, мне кажется, что вертикальность файлов, т.е. тот факт, что весь код написан сверху вниз, тоже является далеко не лучшей идеей, т.к. люди имеют возможность воспринимать информацию и латерально. По вертикали, например, можно иметь основную ветку мышления, а по горизонтали – ответвления и зависимости.
Другой неразрешенной проблемой является то, что по-хорошему нужно уходить от текстового редактора к более гибкому. Меня всегда удивляло, например, почему в программировании не появилась синтаксическая нотация из математики? Ведь математики изначально и двигали эту индустрию… с перфокартами… а, ну да, ну да.
ОК, получился еще один «поток сознания», comments welcome! ■
Оставить комментарий