ИЗ АРХИВА ЖУРНАЛА PRAGPUB ЯНВАРЬ 2011 ГОДА

Связывание кода: уменьшение зависимости в вашем коде

Тим Оттингер и Джефф Лангр

Когда дело доходит до муфт, в слабости есть сила.

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

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

Определение

Когда мы говорим о связи в этой статье, мы имеем в виду вложения, необходимые из-за зависимостей между модулями в объектно-ориентированной (ОО) системе. Модуль может быть классом или совокупностью классов (пакетом). Термин «связь» может относиться к другим зависимостям, например, между методами, но они нас здесь не интересуют.

Зависимость существует, когда код одного класса ссылается на другой класс — через любой из нескольких возможных механизмов:

  • поле
  • Аргумент
  • построен внутри функции
  • наследование или смешение
  • общие знания

Когда вы не можете скомпилировать или использовать модуль A без непосредственного присутствия модуля B, модуль A зависит от модуля B. Зависимость — это связь. Что еще более важно, если изменение модуля B может привести к поломке модуля A, то A соединен с B.

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

Простая зависимость

Нам нужен класс Branch для представления различных физических зданий, в которых посетители библиотеки могут найти книги или другие материалы. Нам нужен класс Material для представления предмета, который посетители хотят одолжить. Объект Branch содержит набор объектов Material. (Не путайте это с проектированием базы данных,
в этом случае Material может иметь обратную ссылку на таблицу Branch.) На рис. 1 показано, как мы изображаем такие зависимости с помощью UML.

Без Material Branch незачем жить; Branch зависит от Material. Любые изменения в определении Material могут повлиять на Branch. Если вы измените класс Material, вам потребуется повторно протестировать классы Material и Branch перед публикацией изменения. С другой стороны, изменения в Branch не важны и не интересны для Material.. Можно сказать, что эффекты текут против направления стрелок зависимости.

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

Транзитивная зависимость

Больше проблем возникает, когда существует много уровней зависимости (см. рис. 2).

Зависимость транзитивна. Чтобы понять полное влияние зависимости, проследите по цепочке в обратном порядке. Например, небольшое изменение кода в деталях расчета сборов в FeeCalculator может повлиять на LibraryController!

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

Модульное тестирование, одно из наших любимых занятий (и, конечно, в первую очередь, то есть с использованием TDD), также становится намного сложнее с глубокими цепочками зависимостей.
Если вы хотите протестировать класса FeeCalculator вы просто создаете новый экземпляр:

var sut = new FeeCalculator(baseRate);

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

С другой стороны, тесты для Material в какой-то момент потребуют от вас также создания экземпляра FeeBasis, для которого требуется экземпляр FeeCalculator. Тесты Branch потребуют Materials и так далее. К тому времени, когда вы доберетесь до самого зависимого класса, этого неуверенного в себе человека, который не может жить без множества других объектов, вы можете обнаружить, что пишете десятки строк кода для создания и заполнения связанных объектов.

Структурная зависимость

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

Неявные зависимости

Прилежный программист без труда проследит явные зависимости между модулями кода, как в цепочке от LibraryController до FeeCalculator выше. С неявными зависимостями дело обстоит сложнее. Когда разные части программы обмениваются знаниями, в нарушение того, что мы обсуждали в прошлом месяце о согласованности, они будут демонстрировать неявную зависимость.

Скажите, что ваш аудитор хочет, чтобы вы указали конкретный метод расчета вознаграждения по имени. К сожалению, при расчете комиссии имя алгоритма не записывается. Вы можете изменить комиссионную транзакцию, включив в нее имя алгоритма, но это означает изменение основного бизнес-объекта, а также базы данных. К счастью, вы помните, что только большой платежный калькулятор может взимать комиссию до 23 долларов. Вы можете вывести метод расчета!

if Fee.amount > 23: 
    calc_method_name = “large payment”

Оно работает! Теперь вы можете создать свой отчет, не трогая и не повреждая существующий код в остальной части системы! Проблема в том, что теперь у вас есть неявная зависимость от лимитов расчета крупного платежа. Может быть, это работает сейчас, может быть, это будет работать в течение нескольких месяцев или лет, но это не гарантирует, что будет работать всегда. Если расчет когда-либо изменится, этот код будет довольно сломан, даже если отчет о комиссии явно не зависит от калькулятора комиссии за крупный платеж.

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

Fee.amount — это простое целое число в приведенном выше примере. Авторы неявно предполагали валюту. Если они наши соотечественники, то наверняка думают о долларах США. И это могло бы быть хорошо, если бы в системе существовало бизнес-правило, согласно которому все суммы приводятся в виде целых долларов, но это общее предположение.

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

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

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

Решения

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

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

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

Еще лучше полагаться на фундаментальные, неизменные интерфейсы объектов. Например, если мы можем переработать код, чтобы рассматривать FeeCalculator как черный ящик, в который мы передаем арендованный экземпляр и получаем объект оплаты, мы можем использовать факты, которые предоставляет нам калькулятор, независимо от того, как он работает. Эта более слабая зависимость предоставляет нам своего рода «брандмауэр зависимостей». Абстракция (будет рассмотрена позже) позволяет нам терпеть изменения.

Со структурными связями трудно иметь дело, потому что навигация часто необходима, и она должна куда-то идти. Наиболее распространенными способами справиться с этим являются Закон Деметры или использование специальных классов (с именами, включающими такие слова, как Gateway или Repository), которые будут выполнять для нас навигацию с помощью таких методов, как Repository.FeeCalculatorFor(Material). Мы обмениваем зависимость от более широкой структуры системы на зависимость от одного класса, который скрывает от нас эти зависимости.

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

Краткое содержание

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

Об авторах

Познакомьтесь с авторами этой статьи и всемирно известной книги Agile in a Flash из The Pragmatic Bookshelf.



О Тиме

Тим Оттингер — создатель и соавтор книги Agile in a Flash, участник Clean Code и разработчик программного обеспечения со стажем более 40 лет. Тим является старшим консультантом в компании Industrial Logic, где он помогает трансформировать команды и организации посредством обучения, консультирования по процессам и обучения техническим практикам. Он непрекращающийся блогер и неисправимый каламбур. Он по-прежнему пишет код, и ему это нравится.

О Джеффе

Джефф Лангр успешно занимается разработкой программного обеспечения вот уже четыре десятилетия. Вместе с Тимом он стал соавтором Agile in a Flash и участвовал в проектах Clean Code и Clean Agile. Среди других книг Джеффа — Agile Java, Современное программирование на C++ с разработкой через тестирование иПрагматическое модульное тестирование в Java. Как и Тим, он постоянно пишет: более 100 опубликованных статей, сотни постов в блогах и почти 1000 ответов на вопросы на Quora. Джефф является членом технического консультативного совета Pragmatic Bookshelf. Он руководит консалтинговой и обучающей компанией Langr Software Solutions, Inc.