Сильная связанность.
Сильная связанность — антипаттерн, при котором модули настолько зависят от внутреннего устройства друг друга, что изменение одного требует менять другие.
В программной инженерии связанность (англ. coupling) — это степень взаимозависимости между программными модулями: мера того, насколько тесно связаны две подпрограммы или модуля и насколько сильны отношения между ними. Сама по себе связанность — это метрика, а не дефект: она есть в любой работающей системе, и она не бинарна, а многомерна. Антипаттерном является сильная связанность (её также называют высокой или жёсткой): модули опираются на внутреннее устройство друг друга, разделяют глобальное состояние или всегда меняются вместе, так что ни один из них нельзя понять, протестировать или переиспользовать отдельно.
Связанность обычно противопоставляется связности. Низкая связанность часто коррелирует с высокой связностью, и наоборот. Низкая связанность часто считается признаком хорошо структурированной компьютерной системы и хорошего проектирования, и в сочетании с высокой связностью способствует достижению общих целей — высокой читаемости и сопровождаемости.
Несколько антипаттернов — это частные формы сильной связанности: божественный объект, от которого зависит всё остальное, циклические зависимости между модулями, последовательная связанность вызовов методов и действие на расстоянии через общее глобальное состояние. На уровне отдельных классов она проявляется в запахах кода Неуместная близость, Завистливые функции и Цепочка вызовов и часто делает код трудно тестируемым.
История
Метрики качества программного обеспечения — связанность и связность — были предложены Ларри Константайном в конце 1960-х годов в рамках структурного проектирования на основе характеристик «хороших» практик программирования, снижавших затраты на сопровождение и модификацию. Структурное проектирование, включая связность и связанность, было опубликовано в статье Stevens, Myers & Constantine (1974) и книге Yourdon & Constantine (1979), и введённые в последней термины впоследствии стали стандартными.
Связанность и связность
Связанность и связность — термины, которые очень часто встречаются вместе. Связанность относится к взаимозависимостям между модулями, тогда как связность описывает, насколько связаны между собой функции внутри одного модуля. Низкая связность означает, что данный модуль выполняет задачи, слабо связанные друг с другом, и поэтому может создавать проблемы по мере увеличения размера модуля.
Степень
Связанность может быть «низкой» (также «слабой» и «нестрогой») или «высокой» (также «тесной» и «сильной»). Некоторые типы связанности, в порядке убывания от наивысшей к наименьшей, перечислены ниже:
Процедурное программирование
Под модулем здесь понимается подпрограмма любого рода, то есть набор из одного или нескольких операторов, имеющий имя и, желательно, собственный набор имён переменных.
- Связанность по содержимому (высокая)
- Связанность по содержимому возникает, когда один модуль использует код другого модуля, например ветвление. Это нарушает сокрытие информации — базовую концепцию проектирования программного обеспечения.
- Общая связанность
- Общая связанность возникает, когда несколько модулей имеют доступ к одним и тем же глобальным данным. Однако при внесении изменений это может приводить к неконтролируемому распространению ошибок и непредвиденным побочным эффектам.
- Внешняя связанность
- Внешняя связанность возникает, когда два модуля совместно используют навязанный извне формат данных, протокол связи или интерфейс устройства. По сути, это связано со взаимодействием с внешними инструментами и устройствами.
- Связанность по управлению
- Связанность по управлению — это когда один модуль управляет ходом выполнения другого, передавая ему информацию о том, что делать (например, передавая флаг с указанием действия).
- Связанность по образцу (связанность по структуре данных)
- Связанность по образцу возникает, когда модули совместно используют составную структуру данных и применяют лишь её части, возможно разные (например, передача целой записи в функцию, которой нужно лишь одно её поле).
- В этой ситуации изменение поля, которое модулю не нужно, может привести к изменению того, как модуль читает запись. Чтобы проиллюстрировать концепцию связанности по образцу, рассмотрим сценарий с компонентом
UserProfile. Этот компонент спроектирован так, чтобы в ответ на запросы возвращать всю информацию профиля пользователя, даже когда потребителям нужен лишь определённый атрибут. Такая практика является примером связанности по образцу, которая может приводить к существенным проблемам с пропускной способностью, особенно при масштабировании. При изменении любого атрибута внутри компонентаUserProfileвсе взаимодействующие с ним потребители могут потребовать тестирования, даже если они не используют изменённый атрибут. - Связанность по данным
- Связанность по данным возникает, когда модули обмениваются данными, например через параметры. Каждый элемент данных является элементарным, и обмениваются только этими данными (например, передача целого числа в функцию, вычисляющую квадратный корень).
Объектно-ориентированное программирование
- Связанность подклассов
- Описывает отношение между потомком и его родителем. Потомок связан со своим родителем, но родитель не связан с потомком.
- Временная связанность
- Временная связанность возникает, когда один модуль можно использовать только в определённый момент или в определённом порядке относительно другого — например,
open()обязательно вызывать доread(), — хотя интерфейс этого никак не обеспечивает; см. последовательная связанность. Объединение действий в один модуль лишь потому, что они выполняются в одно и то же время, — другое понятие, временная связность.
В недавних работах исследовались и использовались различные другие концепции связанности в качестве показателей для разных принципов модуляризации, применяемых на практике.
Динамическая связанность
Цель определения и измерения этого типа связанности — обеспечить оценку программной системы во время выполнения. Утверждается, что статические метрики связанности теряют точность при интенсивном использовании динамического связывания или наследования. В попытке решить эту проблему были учтены меры динамической связанности.
Семантическая связанность
Этот вид метрики связанности учитывает концептуальное сходство между программными сущностями, используя, например, комментарии и идентификаторы и опираясь на такие методы, как латентно-семантическое индексирование (LSI).
Логическая связанность
Анализ логической связанности (или эволюционной связанности, или связанности по изменениям) использует историю выпусков программной системы для поиска паттернов изменений среди модулей или классов: например, сущностей, которые, вероятно, будут изменяться вместе, или последовательностей изменений (за изменением класса A всегда следует изменение класса B).
Измерения связанности
По мнению Грегора Хопе, связанность многомерна:
- Зависимость от технологии
- Зависимость от местоположения
- Зависимость от топологии
- Зависимость от формата и типа данных
- Семантическая зависимость
- Зависимость от диалога
- Зависимость от порядка
- Временная зависимость
Недостатки сильной связанности
Сильно связанные системы, как правило, обладают следующими характеристиками разработки, которые часто рассматриваются как недостатки:
- Изменение в одном модуле обычно вызывает эффект домино — каскад изменений в других модулях.
- Сборка модулей может требовать больше усилий и/или времени из-за возросшей межмодульной зависимости.
- Конкретный модуль может быть труднее повторно использовать и/или тестировать, поскольку приходится включать зависимые модули.
Проблемы производительности
Независимо от того, слабо или сильно связаны модули, обмен сообщениями между ними требует затрат во время выполнения на создание, передачу, преобразование (например, маршалинг) и интерпретацию этих сообщений. Простые сообщения — например, ссылка на строку, массив или структуру данных — обходятся гораздо дешевле, чем сложные, такие как сообщение SOAP, а на формирование более длинных сообщений уходит больше процессорного времени и памяти. Эти накладные расходы бывают трёх видов:
- Накладные расходы на передачу сообщений и производительность
- Поскольку сообщение должно передаваться целиком, чтобы сохранить свой полный смысл, передачу сообщений необходимо оптимизировать. Более длинные сообщения требуют больше ресурсов процессора и памяти для передачи и приёма. Кроме того, при необходимости получатели должны собрать сообщение в его исходное состояние, чтобы полностью его принять. Следовательно, для оптимизации производительности во время выполнения длину сообщения следует минимизировать, а смысловую нагрузку — максимизировать.
- Накладные расходы на преобразование сообщений и производительность
- Протоколы сообщений и сами сообщения часто содержат дополнительную информацию (то есть сведения о пакете, структуре, определениях и языке). Поэтому получателю часто приходится преобразовывать сообщение в более очищенную форму, удаляя лишние символы и сведения о структуре и/или преобразуя значения из одного типа в другой. Любое преобразование увеличивает накладные расходы на процессор и/или память. Для оптимизации производительности во время выполнения форму и содержание сообщения следует сокращать и очищать, чтобы максимизировать его смысл и уменьшить объём преобразований.
- Накладные расходы на интерпретацию сообщений и производительность
- Все сообщения должны интерпретироваться получателем. Простые сообщения, такие как целые числа, могут не требовать дополнительной обработки для интерпретации. Однако сложные сообщения, такие как сообщения SOAP, требуют синтаксического анализатора и преобразователя строк, чтобы отобразить заложенный в них смысл. Для оптимизации производительности во время выполнения сообщения следует очищать и сокращать, чтобы минимизировать накладные расходы на интерпретацию.
Решения
Один из подходов к снижению связанности — функциональное проектирование, которое стремится ограничить обязанности модулей в соответствии с функциональностью. Связанность между двумя классами A и B возрастает, если:
Aимеет атрибут типаB(ссылающийся на него).Aобращается к услугам объектаB.Aимеет метод, ссылающийся наB(через возвращаемый тип или параметр).Aявляется подклассом классаB(или реализует его).
Низкая связанность подразумевает отношение, при котором один модуль взаимодействует с другим через простой и стабильный интерфейс и не должен заботиться о внутренней реализации другого модуля (см. сокрытие информации).
Системы, такие как CORBA или COM, позволяют объектам взаимодействовать друг с другом, не зная ничего о реализации другого объекта. Обе эти системы даже позволяют объектам взаимодействовать с объектами, написанными на других языках.
Связанность и коннасценция
Связанность описывает степень и природу зависимости между программными компонентами, акцентируя внимание на том, что они разделяют (например, данные, поток управления, технологию), и насколько тесно они связаны. Она оценивает два ключевых измерения: силу, которая измеряет, насколько трудно изменить зависимость, и область видимости (или видимость), которая показывает, насколько широко зависимость раскрыта между модулями или границами. Традиционные типы связанности обычно включают связанность по содержимому, общую связанность, связанность по управлению, связанность по образцу, внешнюю связанность и связанность по данным.
Коннасценция, введённая Мейлиром Пейдж-Джонсом, предоставляет систематическую методику для анализа и измерения зависимостей связанности. Она оценивает зависимости по трём измерениям: сила, которая измеряет усилия, необходимые для рефакторинга или изменения зависимости; локальность, которая учитывает, насколько физически или логически близки зависимые компоненты в кодовой базе; и степень, которая измеряет, сколько компонентов затрагивает зависимость. Коннасценцию можно разделить на статическую (выявляемую во время компиляции) и динамическую (выявляемую во время выполнения) формы. Статическая коннасценция относится к зависимостям времени компиляции, таким как сигнатуры методов, тогда как динамическая коннасценция относится к зависимостям времени выполнения, которые могут проявляться в таких формах, как коннасценция момента времени, значений или алгоритма.
Каждая разновидность связанности может проявлять несколько типов коннасценции, какой-то конкретный тип или, в редких случаях, не проявлять ни одного — в зависимости от того, как реализована зависимость. Распространённые типы коннасценции включают коннасценцию имени, типа, позиции и смысла. Некоторые типы связанности естественным образом соответствуют конкретным типам коннасценции; например, связанность по данным часто включает коннасценцию имени или типа. Однако не каждое сочетание связанности и коннасценции имеет практический смысл. Зависимости, опирающиеся на порядок параметров в сигнатуре метода, демонстрируют коннасценцию позиции, которая хрупка и трудна для рефакторинга, поскольку изменение порядка параметров нарушает интерфейс. Напротив, коннасценция имени, которая опирается на имена полей или параметров, как правило, более устойчива к изменениям. Сами типы коннасценции образуют естественную иерархию по силе, при этом коннасценция имени обычно считается слабее, чем коннасценция смысла.
Зависимости, пересекающие границы модулей или распределённых систем, обычно имеют более высокие затраты на координацию, что увеличивает сложность рефакторинга и распространения изменений через отдалённые границы. Современные практики, такие как внедрение зависимостей и программирование на основе интерфейсов, часто применяются для снижения силы связанности и улучшения сопровождаемости зависимостей.
В то время как связанность определяет, что разделяется между компонентами, коннасценция оценивает, как ведут себя эти зависимости, как распространяются изменения и насколько трудно их рефакторить. Сила, локальность и степень взаимосвязаны; зависимости с высокой силой, широкой областью видимости и пересекающие отдалённые границы значительно труднее рефакторить и сопровождать. В совокупности связанность даёт высокоуровневый обзор отношений зависимости, тогда как коннасценция предлагает детализированную методику для анализа силы, локальности, степени и устойчивости зависимости к изменениям, поддерживая проектирование сопровождаемых и надёжных систем.
Связанность модулей
Одна из опубликованных метрик сводит эти величины к единому значению связанности модуля.
Для связанности по данным и по управлению:
d_i: число входных параметров-данныхc_i: число входных управляющих параметровd_o: число выходных параметров-данныхc_o: число выходных управляющих параметров
Для глобальной связанности:
g_d: число глобальных переменных, используемых как данныеg_c: число глобальных переменных, используемых для управления
Для связанности с окружением:
w: число вызываемых модулей (коэффициент разветвления по выходу, fan-out)r: число модулей, вызывающих рассматриваемый модуль (коэффициент объединения по входу, fan-in)
Тогда связанность модуля равна:
Coupling(C) = 1 − 1 / (d_i + 2×c_i + d_o + 2×c_o + g_d + 2×g_c + w + r)
Чем сильнее связан модуль, тем больше значение. Оно лежит в диапазоне примерно от 0,67 (слабая связанность) до 1,0 (сильная связанность).
Например, если у модуля всего один входной и один выходной параметр-данные:
C = 1 − 1 / (1 + 0 + 1 + 0 + 0 + 0 + 1 + 0) = 1 − 1/3 = 0.67
Если у модуля 5 входных и 5 выходных параметров-данных, столько же управляющих параметров, он обращается к 10 глобальным данным, а его fan-in равен 3 и fan-out — 4:
C = 1 − 1 / (5 + 2×5 + 5 + 2×5 + 10 + 0 + 4 + 3) = 0.98
Пример
Этот Report сильно связан со всем, к чему прикасается: с глобальным синглтоном базы данных, со схемой SQL, с позицией столбца в результате запроса и с консолью.
class Report {
void run() {
List<Object[]> rows = Db.connection().query("select * from orders");
for (Object[] row : rows) {
System.out.println(row[3]); // в столбце 3 лежит сумма — пока что
}
}
}
После рефакторинга он зависит только от узкого интерфейса и типа-значения, которые передаются ему извне:
interface OrderSource {
List<Order> orders();
}
record Order(String id, BigDecimal total) {}
class Report {
private final OrderSource source;
private final PrintStream out;
Report(OrderSource source, PrintStream out) {
this.source = source;
this.out = out;
}
void run() {
for (Order order : source.orders()) {
out.println(order.total());
}
}
}
Теперь базу данных, схему и вывод можно менять или подменять в тестах, не трогая Report.
Адаптировано по статье Википедии “Coupling (computer programming)” (версия 1355254577), распространяется по лицензии CC BY-SA 4.0; текст переведён и отредактирован, добавлены примеры.