Методы борьбы со сложностью

У разработчиков – и это включает меня – полным полно отмазок на тему того, почему та или иная задача не сделана. Обычно виной тому служит компилятор, библиотека, 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! ■

19 responses to “Методы борьбы со сложностью”

  1. “Не существует никаких объективных причин связывать логическую и физическую структуру кода.”
    тоже самое, что и
    “Не существует никаких объективных причин делать форму окна в ОС прямоугольной.”

    1. Прямоугольная форма окна – это очень спорный момент, и поскольку я не юзабилист, экспертного мнения по этому вопросу у меня нет. Может раскроете суть комментария?

  2. Дмитрий, во многом с вами согласен, но хотел бы отметить два нюанса.
    Первый, касательно разбиения алгоритма на псевдошаги с последующим созданием сущностей для каждого из них, – преимущество такого подхода может быть раскрыто только в случае, когда нам нужно покрыть некую часть алгоритма комментариями, т.е. разъяснить программисту что же происходит в каждый момент времени. Как результат все упирается в проблему выбора имен для классов, методов, свойств, которые будут инкапсулировать логику каждого шага. Ну а проблема выбора правильных имен стара как и само программирование, каким мы знаем его сейчас. Поэтому эту методику нужно использовать очень осторожно, ибо неправильный выбор имен превратит изначально понятную последовательность операций в спагетти из десятка классов.
    Второй, касательно проблемы использования файлов и чтения кода сверху вниз, – так уж сложилось, что человечество уже как пару тысячелетий читает печатные(рукописные) документы сверху вниз. Я думаю, что за такой срок эволюция уже бы заставила нас изобрести какой-либо более совершенный способ передачи читабельной информации. Ан нет, мы все еще пользуемся таким подходом. Но это значит только то, что восприятие информации посредством зрительных образов уже оптимизировано и нужно искать другие методики, более эффективные, для передачи данных от человека к человеку.

    1. Alexey DAloG Demedeckiy Avatar
      Alexey DAloG Demedeckiy

      Ну вот по поводу чтения я с вами не согласен. Тексты котолрые привыкло читать человечество связаны намного сильнее чем то что читает программист. Я представляю себе что это список публичных методов. при клике не каждый открывается его код в маленьком окошке, и так далее. А там тягая эти окошки вправо влево. Можно показать связи до все что угодно. Фокусировка на задаче будет в разы выше.

      С алгоритмами я тоже с Дмитрием согласен, давно уже так пишу. Проблема именования тут скорее открывает непонимание решения. Если сущность выдялеятся “по шаблону” то придумать ей имя тяжело. А если это продиктовано необходимостью хорошее имя само выскакивает в мозгу) И делать такие классы нужно даже не для гибкости и прочих плюшек ООП, сколько для разгрузки основного класса, удаление частных переменных, состоянийи, методов. Всего того что существует только для 1 вызова.

  3. Всегда нравилось когда сложную концепцию иллюстрируют простым примером в стиле DoSomething, Foo, blablabla

    1. Борьба со сложностью ПО структурированием кода напоминает анекдот:

      Офис. С утра офисные сотрудники в спешке передвигают мебель с места на место, выравнивают все по сантиметру, компасу и т.д. Посредине всего этого хаотичного движения стоит старенькая уборщица в обнимку со шваброй, испуганно смотрит на все это действо.
      Бормочет про себя :”Только помыла, сейчас все опять затопчут, ироды и т.д.”.
      Стояла долго смотрела на все это, потом спрашивает:
      – Милые, а что вы тут делаете? Переезжаете?
      – Да нет, бабуля,
      мы сейчас мебель по фен-шую передвинем и у нас сразу продажи взлетят до небес.
      – Сынки, я тут давно уже работаю, еще до революции полы в этом здании мыла. Так вот, до революции тут публичный дом был. Так там, когда выручка падала, кровати не двигали. Там сразу .6%#дей меняли.

      1. Стоп, так если не структурированием бороться, то чем?

      2. Бороться со сложностью – это как? Писать ERP систему сложно, давайте лучше напишем калькулятор?

      3. Борьба со сложностью, обычно проходит в муках. И методы борьбы это не какие-то элементарные технические решения, по перетаскиванию кода из файла в файл, а решения как правило решения идеологические.
        Сложность кода не в том, что мне читать его в одном файле или в 10 разных, а сложность в его содержании. И если год говно, его никакими перетаскиваниями и сингелтонами не улучшить.

        Борьба со сложностью начинается построения иерархий и абстракций. Функционал, который относится к одной области выносится в определенный слой, между слоями определяются четкие и строгие интерфейсы. Далее идет правильный дизайн. Это значит, что при небольших изменениях требований, происходят такие же небольшие изменения кода, т.к. сказать линейная зависимость.
        Получить качественный такой код в большом проекте, который удовлетворяет, данному условию на протяжении всего жизненного цикла проекта, очень сложно и зависит исключительно от опыта разработчиков и архитектора, которые взаимодействуют как команда.
        То что вы пишите ” Алгоритм отныне будет классом. ” это называется переход к процедурному программированию. Предложенный процедурный стиль перечеркивает все плюсы (можете их перечислить например?). Отсюда напрашивается вывод, что вы плохо понимаете парадигму ООП.
        Вот это “return MyAlgoResult.Ok” еще одно зло, современные языки, это языки императивного программирования. И если мы вызываем метод class MyAlgo.Step1(), то если не происходит исключительной ситуации, значит он отработал. Но самое большое зло кода возврата, это то, что тот слой в котором происходит ошибка, в большинстве случаев не знает как поступить с этой ошибкой. Простой пример: есть программа с таблицей и фильтрами, пусть запросы фильтров кешируются в хранилище. Т.е. имеем три слоя: это бизнес логика, хранилище(формирует структуры для хранения), физический носитель (хранит данные). Как вариант хранилище может быть представлено локальным файлом, а может быть и удаленным диском, ftp и т.д. При чтении локального файла, происходит ошибка, и слой хранилища пересоздает файл. Данная ошибка возникает в слое носителя, а обрабатывается в слое хранилища. Ошибка проходит 1 слой, если же недоступен сетевой диск, то ошибка, доходит уже до слоя бизнес логики, тем самым проходит 2 границы между слоями. А теперь стоит только прикинуть количество switch’ей и if’ов в таком коде.

        С уважением, Корка.

      4. Ну во-первых я не соглашусь что перетаскиваниями код не улучшить, т.к. разбиение кода на более мелкие фрагменты, имхо, улучшает восприятие. Что касается перечеркивания парадигмы ООП, то тут мне кажется следует понимать, что ооп – это полиморфизм, инкапсуляция и наследование, и ни один из этих принципов напрямую не относится к отдельно взятому алгоритму. К набору алгоритмов – возможно, но не к единичному. Тем самым, тот факт что я предлагаю использовать больше классов с ООП не связан вообще, это чисто подход по структурированию кода.

        Что касается кодов возврата, это просто одна из двух практик – либо мы программируем с использованием исключений, либо стараемся все проверять и исключения не бросаем и не ловим. Лично я не фанат исключений т.к. они вполне естественным образом нарушают control flow программы.

  4. >> Я сколько не думал, так и не мог привести вменяемых аргументов в пользу этого правила.
    Это дело вкуса поэтому, ИМХО, тут не может быть вменяемых аргументов за или против. Мне удобно когда я могу без лишних движений открыть в студии два файла-класса, и переключаться между ними с помощью ctrl+tab. Моему коллеге удобно прыгать по одному файлу между классами с помощью pgup/down, ctrl-n или ставить букмарки. Вам удобно третье.

    1. Да, но по аналогии, если классов 50 то и файлов должно быть 50. Не уверен что с перставлением 50-ти файлов в solution explorer удобно работать.

      1. С одним файлом на 10000 строк кода, в котором лежит несколько вложенных типов, будет работать не намного удобнее

      2. Мой аргумент в том, что файлы в конечном счете вообще не нужны. Единственное что важно так это чтобы данные хранились где-то, и чтобы область видимости нужной части алгоритма можно было ограничить, и тем самым не просматривать “простыню”.

      3. Какая разница в чем физически хранится код? В редакторе кода QuickBasic код хранился в одном файле, но можно было переключаться между отдельными процедурами так, что на экран выводился код только той процедуры с которой идет работа.
        В редакторе кода Visual Basic 6 был аналогичный режим – к редакторе кода выводилась только текущая процедура, для переключения между процедурами нужно было “проскроллить” вверх или вниз.
        http://msdn.microsoft.com/en-us/devlabs/debuggercanvas Под Visual Studio .NET тоже есть концепт с похожей функциональность.

      4. Так не пишите простынями :)

      5. Были одни такие “нелюбители простыней”, слабавшие PowerBuilder. Ублюдочнее среды я ещё не видел!! Да, всё раскидано по структурам и комбобоксам, но до чего же это неудобно! Запустите, поработайте и вся спесь пройдёт :)

  5. В защиту “кода как простыни текста” есть единственный и главнейший аргумент: системы контроля версий.
    А то, что в редакторе мы видим ту же простыню – куча причин, от лени программастов до ограниченности устройств отображения. Будет у меня монитор с рабочий стол – тогда можно говорить о структурах.

  6. большие сквозные структуры – вот самое зло… иерархии и структурно-функциональная декомпозиция – это метод борьбы с ним… про именование – верно подмечено, есть проблема – иногда чисто аналитически получаешь некую странную функцию, которую не знаешь как назвать – гамма, лямбда – множество примеров в академической математике, а не только в программировании…
    монитор с рабочий стол – тоже остро, а если еще 3D:) увы, пока еще, в среднем, программист в этом смысле больше ограничен, чем конструктор или разработчик рэа… зато воображение развивает:)

Оставить комментарий