О-Нотация В Разработке Программного Обеспечения

Разбираясь в SOLID, я часто сталкивался с тем, что несоблюдение этих принципов может привести к проблемам.

Проблемы известны, но плохо формализованы.

Данная статья написана с целью формализовать типичные ситуации, возникающие в процессе написания кода, возможные решения и вытекающие отсюда последствия.

Мы поговорим об опасностях плохого кода и о том, как проблемы растут по мере роста программы.

К сожалению, в большинстве случаев оценка сводится к вариантам «много» и «один», что намекает на несостоятельность О-нотации, но даже такой анализ позволит лучше понять, какой код действительно опасен для дальнейшего развития.

системы и какой код является допустимым.



Определение

Будем говорить, что изменение в программе требует «О» из f(n) действий, если программисту необходимо внести в программу не более f(n) логически отдельных изменений для реализации изменения с точностью до постоянного коэффициента, где n относится к размеру программы.



Метрики

Давайте рассмотрим некоторые конструктивные свойства по Роберту Мартину и оценим их с точки зрения О-нотации.

Проект считается жестким, если одно изменение вызывает каскад других изменений в зависимых модулях.

Чем больше модулей приходится менять, тем жестче конструкция.

Разница между изменениями O(1) и O(n) значительна.

Те.

Независимо от того, допускает ли наш дизайн постоянное количество изменений, или по мере роста программы количество изменений будет увеличиваться.

Далее следует рассмотреть сами изменения — они тоже могут оказаться жесткими с некоторой своей асимптотикой.

Таким образом, жесткость может иметь сложность до O(нм).

Параметр m будем называть глубиной жесткости.

Даже приблизительная оценка глубины жесткости в конструкции, допускающей хотя бы жесткость O(n), слишком сложна для человека, поскольку каждое из изменений необходимо проверять на наличие еще более глубоких изменений.

Хрупкость — это способность программы повреждаться во многих местах при внесении одного-единственного изменения.

Часто новые проблемы возникают в частях, не имеющих концептуальной связи с той, которая была изменена.

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

Поэтому с точки зрения обозначения О нет разницы между хрупкостью и жесткостью, и рассуждения, справедливые для жесткости, применимы и к хрупкости.

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

Риски и усилия в этом определении можно интерпретировать как непостоянное количество изменений, происходящих в модуле по мере роста размера модуля при попытке абстрагировать его от исходной системы.

Однако, как показывает практика, абстрагироваться все же стоит, так как это наводит порядок в самом модуле и позволяет перенести его в другие проекты.

Очень часто после первой необходимости перенести модуль в другой проект появляются другие подобные.

Вязкость.

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

Некоторые сохраняют дизайн, другие нет (то есть по сути являются «хаком»).

Если подходы, сохраняющие дизайн, реализовать труднее, чем хаки, то вязкость дизайна высока.

Неправильно решить задачу легко, но решить ее правильно сложно.

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

Следующую вязкость можно назвать близорукостью с точки зрения обозначения О.

Да, на первых порах у разработчика есть возможность внести изменение в O(1) вместо O(n) (из-за жесткости или хрупкости), но зачастую такие изменения приводят к еще большей жесткости и хрупкости, т.е.

увеличивают глубину жесткость.

Если проигнорировать такой «звоночек», то последующие изменения, возможно, уже не получится решить «хаком» и придется вносить изменения при жестких условиях (возможно, О дольше, чем до «звонка») или привести систему в исправное состояние.

То есть при обнаружении вязкости лучше сразу переписать систему соответствующим образом.

Происходит это так: Ивану нужно написать какой-то код, который обманет маленького парня.

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

Но Иван не знает, что код, который он извлек мышкой, поместил туда Петр, который взял его из модуля, написанного Светой.

Свете первой пришлось завивать бокренок, но она знала, что этот процесс очень похож на завивание бармаглотов.

Она где-то нашла код этого бармаглота, скопировала его в свой модуль и модифицировала.

Когда один и тот же код появляется снова и снова в немного разных формах, разработчики теряют абстракцию.

В этой ситуации становится очевидным, что когда возникает необходимость изменить основу копируемого действия, это изменение необходимо произвести в n местах.

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

В этом случае есть вероятность просто забыть об изменении в другом месте, поскольку на этапе компиляции защиты нет. Это означает, что это также может превратиться в итерации тестирования O (n).



Применение обозначения к SOLID

SRP (принцип единой ответственности).

У объекта программного обеспечения должна быть только одна причина для изменения (ответственность).

Другими словами, например, класс не должен следить за бизнес-логикой и отображением, т.к.

при изменении одной ответственности мы должны следить за тем, чтобы не повредилась другая ответственность.

То есть несоблюдение принципа SRP приводит к жесткости и хрупкости.

Также следование этому принципу помогает избавиться от жесткости и переносить модули из одной программы в другую с потенциально меньшим количеством зависимостей.

Асимптотика изменений остается в целом такой же, как и без соблюдения принципа, но постоянный множитель существенно уменьшается.

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

Аналогичные рассуждения, сведенные к рассуждениям об интерфейсах, справедливы и для ISP (принцип разделения интерфейсов).

OCP (принцип открытия и закрытия).

Программные объекты (классы, модули, функции и т. д.) должны быть открыты для расширения и закрыты для модификации.

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

Поскольку нарушение принципа LSP является нарушением OCP, а DIP является средством поддержания OCP, то и к LSP, и к DIP можно применить следующее.

Несоблюдение принципа открытости-закрытости вынуждает производить изменения во всех сущностях, которые не являются закрытыми относительно этого изменения.

Довольно тривиальной ситуацией является, например, наличие среди списка дочерних классов цепочки if, определяющих тип переменной.

Такие структуры могут встречаться в программе более одного раза.

И всякий раз, когда добавляется новый дочерний класс, в каждой такой цепочке следует вносить соответствующие изменения.

Подобные ситуации могут возникнуть не только с дочерними классами, но и при любом рассмотрении не всех возможных частных случаев [Речь идет о случаях не на момент написания статьи, а вообще.

Дела могут появиться позже].

Теперь рассмотрим ситуацию, когда мы делаем m однотипных изменений, которые из-за несоблюдения принципа открытости-закрытости требуют от нас n операций.

Тогда, если оставить все как есть, сохранив архитектуру для рассмотрения частных случаев и не обобщая, мы получим общую сложность для всех изменений O(mn).

Если мы закроем все m мест относительно данного изменения, то последующие изменения займут у нас O(1) вместо O(m).

Это снижает общую сложность до O(m + n).

Это означает, что никогда не поздно начать следовать OCP. Мартин говорит о подобной ситуации, что не стоит гадать (если точно не знаешь, конечно), как закрыть с первого изменения, но после первого изменения стоит закрыть, так как первое изменение было маркером того, что система не будет обязательно останется в своем нынешнем состоянии.

Это логично, поскольку мы делаем O(1*n) действий из-за незамкнутости, а затем O(m) действий, чтобы закрыться от последующих изменений.

В сумме мы получаем общую сложность O(n + m), но при этом все действия делаем именно тогда, когда они станут необходимыми и ничего не делаем заранее, не зная точно, понадобится ли это.



Шаблоны и O-нотация

Еще одну аналогию можно провести между О-нотацией в теории вычислений и О-нотацией в проектировании.

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

Влияние паттернов можно оценить в контексте принципов SOLID и, как следствие, в контексте О-нотации.

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

Шаблон адаптера позволяет нам использовать (читай добавлять) объекты с разными интерфейсами, которые мы будем использовать для общей цели.

Используя этот паттерн, вы можете ввести в систему новый объект с неразделяемым интерфейсом за ряд операций, не зависящий от размера системы.

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



Разумные пределы

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

Необходимо понимать, что пузырьковая сортировка массива из 3 элементов будет работать быстрее пирамидальной сортировки, несмотря на то, что пирамидальная сортировка имеет лучшую асимптотику.

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

O-нотация работает в дизайне таким же образом.

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

Теги: #Системный анализ и проектирование #проектирование и рефакторинг #solid #O-нотация

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.