Страницы

Наследие в развивающейся системе без раздувания сложности

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

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

Далеко не всегда можно обойтись только добавлением, что может быть вызвано причинами:

  1. Ошибки в проектировании, с самого начала делающие использование системы далёким от оптимального или даже опасным.
  2. Устаревание, то есть изменение внешних условий использования, делающих изначально приемлемую систему неоптимальной, обнажая ранее некритичные недостатки.
  3. Появление непредусмотренных точек развития, возможность которых не позволяет в общем случае заложить нужную расширимость заранее за приемлемое время проектирования.
  4. Избыточность и несогласованность, возникающая при добавлении якобы независимых дополнений, что приводит к лишнему усложнению. Накопленная сложность, превышающая определённый порог, становится существенной проблемой как для поддержания наследия, так и для создания нового в рамках технологии.

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

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

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

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

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

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

Чтобы способствовать лёгкости переходов, исходная технология должна обладать определёнными качествами.

  1. Обобщённое требование простоты. Чем богаче и изощрённей язык, чем крепче и сложней взаимосвязи между его элементами, тем сложней и объёмней может потребоваться преобразователь. Так, сохранение простоты — это не только цель, но и необходимое условие. Следует отметить, что выполнить его не так уж и просто, так как в алгоритмически полной системе можно незаметно для себя перекидывать сложность с одного уровня на другой, совершенно не достигая простоты в целом, а то и делая только хуже (Forth, LISP и т.д.).
  2. Изначальная большая высокоуровневость. Эта особенность контринтуитивна, потому что при совместимом расширении, обычно, следует поступать прямо противоположно, но при наличии трансляции в исходной форме проще переходить с более высокого уровня на низкий, чем наоборот. Если при проектировании языка или библиотеки есть неясность между каким уровнем следует выбрать, лучше отдать предпочтение более высокоуровневому, потому что он может относительно безболезненно транслироваться в низкий уровень. Наоборот же — сложней. То разнообразие, с которым можно моделировать на низком уровне приводит к тому, что накопленный код может быть невозможно привести к единой высокоуровневой форме с приемлемым результатом.
  3. Избегание встроенного метапрограммирования, многоуровневого представления программ — препроцессора, рефлексии, компиляторных плагинов и так далее. Если программа использует свою часть как входные данные, то в общем случае корректное преобразование становится алгоритмически неразрешимой задачей. Как минимум, язык не должен предоставлять рефлексивность по умолчанию, а использовать явные пометки, чтобы упростить определение потенциально опасных мест и не провоцировать злоупотребление из любви к искусству. Препроцессор должен быть крайним средством, а не стандартной затычкой для дыр в основном языке. Любые расширения для предметной области должны быть обусловлены значительным объёмом применения и значительным выигрышем от использования расширения.
  4. Избегание исключений как нормы. Этот механизм превращает некорректное состояние в потенциально корректное, взрывным образом увеличивая возможное количество ветвлений и создавая параллельный каркас для переходов. Это усложняет преобразование таких программ (смотрите суперкомпиляцию Java).
  5. Избегание неструктурных переходов. Такие переходы увеличивают сцепку кода, усложняя декомпозицию — важную разновидность преобразований. При наличи ресурсов всё можно преодолеть, например, здесь сотрудник Jetbrains с гордостью рассказывает о том, как благодаря кропотливому труду удалось сделать рефакторинг методов с множественными return. Но если хочется без уличной магии, лучше не надо.
  6. Чёткость границ модулей. Если язык позволяет трудноразличимо смешивать составные части единиц сборки, то при преобразованиях требуется более сложное отслеживание связей и дополнительный контроль над общей корректностью. Механизмы вроде перекрытия и перегрузки имён, псевдонимов, автовывода и автопреобразования типа способны ещё сильней осложнить преобразование. Если исходная форма неудобна в этом отношении, возможно, стоит обеспокоиться об изоморфном промежуточном представлении, в котором отслеживание принадлежности тривиально.
  7. Синтаксическая различимость разных сущностей. Если разные элементы языка будут различаться даже при поверхностном анализе, например, без необходимости построения таблицы символов, то это повысит шансы на простое воплощение преобразования.
  8. Уменьшение косвенности. Косвенность способна вносить неясность о принадлежности сущностей. Где возможно косвенность не должна поощраться по умолчанию. Как отрицательный пример — в Oberon любые процедуры уровня модуля можно присваивать процедурным переменным.

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

Комментариев нет:

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