Как понять общую картину в слабосвязанном приложении?
Мы разрабатывали код с использованием слабой связи и внедрения зависимостей.
Многие классы стилей "обслуживания" имеют конструктор и один метод, который реализует интерфейс. Каждый отдельный класс очень легко понять в отдельности.
Однако из-за слабости связи, просмотр класса ничего не говорит вам о классах вокруг него или о том, где он вписывается в общую картину.
Нелегко перейти к соавторам с помощью Eclipse, потому что вам нужно пройти через интерфейсы. Если интерфейс Runnable
, это не помогает найти, какой класс на самом деле подключен. Действительно, необходимо вернуться к определению контейнера DI и попытаться выяснить это оттуда.
Вот строка кода из класса обслуживания, внедренного в зависимость:-
// myExpiryCutoffDateService was injected,
Date cutoff = myExpiryCutoffDateService.get();
Муфта здесь настолько свободна, насколько это возможно. Срок годности должен быть реализован буквально любым способом.
Вот как это может выглядеть в более связанном приложении.
ExpiryDateService = new ExpiryDateService();
Date cutoff = getCutoffDate( databaseConnection, paymentInstrument );
Из тесно связанной версии я могу сделать вывод, что дата отсечения как-то определяется с помощью платежного инструмента с использованием соединения с базой данных.
Я нахожу код первого стиля сложнее для понимания, чем код второго стиля.
Вы можете утверждать, что, читая этот класс, мне не нужно знать, как вычисляется дата отсечения. Это правда, но если я сужу ошибку или выясняю, где нужно добавить улучшение, это полезная информация.
Кто-нибудь еще испытывает эту проблему? Какие у тебя решения? Это просто что-то, чтобы приспособиться к? Существуют ли какие-либо инструменты для визуализации способа соединения классов? Должен ли я сделать классы больше или больше в сочетании?
(Умышленно оставили этот вопрос контейнерно-независимым, так как меня интересуют ответы на любые вопросы).
10 ответов
Хотя я не знаю, как ответить на этот вопрос в одном абзаце, я попытался ответить на него в сообщении в блоге: http://blog.ploeh.dk/2012/02/02/LooseCouplingAndTheBigPicture.aspx
Подводя итог, я считаю, что наиболее важными моментами являются:
- Понимание слабосвязанной кодовой базы требует другого мышления. Хотя "прыгнуть на соавторов" сложнее, это также должно быть более или менее неактуально.
- Слабая связь - это понимание части без понимания целого. Вам редко нужно понимать все это одновременно.
- Обращая внимание на ошибку, вы должны полагаться на трассировки стека, а не на статическую структуру кода, чтобы узнать о соавторах.
- Разработчики, пишущие код, несут ответственность за то, чтобы его было легко понять - разработчик не читает код.
Некоторые инструменты знают о структурах DI и знают, как разрешать зависимости, что позволяет вам перемещаться по коду естественным образом. Но когда это недоступно, вам просто нужно использовать все возможности, которые предоставляет ваша IDE, как можно лучше.
Я использую Visual Studio и пользовательский фреймворк, поэтому проблема, которую вы описываете, это моя жизнь. В Visual Studio SHIFT+F12 - мой друг. Он показывает все ссылки на символ под курсором. Через некоторое время вы привыкаете к обязательно нелинейной навигации по своему коду, и становится второй натурой думать, "какой класс реализует этот интерфейс" и "где находится сайт внедрения / конфигурации, чтобы я мог видеть, какой класс используется для удовлетворения этой зависимости интерфейса ".
Для VS также доступны расширения, предоставляющие улучшения пользовательского интерфейса, такие как Productivity Power Tools. Например, вы можете навести указатель мыши на интерфейс, всплывет информационное окно и нажать "Реализовано", чтобы увидеть все классы в вашем решении, реализующие этот интерфейс. Вы можете дважды щелкнуть, чтобы перейти к определению любого из этих классов. (Я все равно обычно просто использую SHIFT+F12).
Я только что провел внутреннюю дискуссию по этому поводу и закончил тем, что написал эту часть, которую я считаю слишком хорошей, чтобы ею не поделиться. Я копирую это здесь (почти) неотредактировано, но, хотя это и является частью большей внутренней дискуссии, я думаю, что большинство из них может остаться в одиночестве.
Обсуждение о введении пользовательского интерфейса под названием IPurchaseReceiptService
и должен ли он быть заменен с использованием IObserver<T>
,
Ну, я не могу сказать, что у меня есть веские данные по этому поводу - это всего лишь некоторые теории, которые я преследую... Однако моя теория о когнитивных накладных расходах на данный момент выглядит примерно так: IPurchaseReceiptService
:
public interface IPurchaseReceiptService
{
void SendReceipt(string transactionId, string userGuid);
}
Если мы оставим его в качестве интерфейса заголовка, в котором он сейчас находится, SendReceipt
метод. Это круто.
Что не так круто, так это то, что вам пришлось придумать имя для интерфейса и другое имя для метода. Между ними есть некоторое совпадение: слово " Квитанция" появляется дважды. IME, иногда такое совпадение может быть еще более выраженным.
Кроме того, имя интерфейса IPurchaseReceiptService
, что тоже не особо полезно. Суффикс Сервиса - это, по сути, новый Менеджер, и, IMO, запах дизайна.
Кроме того, вы не только должны были назвать интерфейс и метод, но вы также должны назвать переменную при ее использовании:
public EvoNotifyController(
ICreditCardService creditCardService,
IPurchaseReceiptService purchaseReceiptService,
EvoCipher cipher
)
В этот момент вы, по сути, трижды сказали то же самое. Это, согласно моей теории, когнитивные накладные расходы и запах, который дизайн может и должен быть проще.
Теперь сопоставьте это с использованием хорошо известного интерфейса, такого как IObserver<T>
:
public EvoNotifyController(
ICreditCardService creditCardService,
IObserver<TransactionInfo> purchaseReceiptService,
EvoCipher cipher
)
Это позволяет вам избавиться от бюрократии и уменьшить замысел в самом деле. У вас все еще есть намеренное раскрытие имен - вы только смещаете дизайн с Подсказки на роль имени типа на Подсказку на роль имени аргумента.
Когда дело доходит до дискуссии о "разобщенности", у меня нет иллюзий, что использование IObserver<T>
волшебным образом заставит эту проблему уйти, но у меня есть другая теория об этом.
Моя теория состоит в том, что причина, по которой многие программисты находят программирование для интерфейсов настолько сложным, заключается именно в том, что они используются для функции Go to Definition в Visual Studio (кстати, это еще один пример того, как инструменты мешают работе). Эти программисты постоянно находятся в состоянии ума, когда им нужно знать, что находится "на другой стороне интерфейса". Почему это? Может быть потому, что абстракция бедна?
Это связано с RAP, потому что если вы подтверждаете убеждение программистов, что за каждым интерфейсом стоит отдельная, конкретная реализация, неудивительно, что они думают, что интерфейсы только в пути.
Однако, если вы примените RAP, я надеюсь, что программисты постепенно поймут, что за конкретным интерфейсом может быть любая реализация этого интерфейса, и их клиентский код должен быть в состоянии обрабатывать любую реализацию этого интерфейса без изменения правильности система. Если эта теория верна, мы только что внедрили принцип подстановки Лискова в базу кода, не пугая никого концепциями высокого броуса, которые они не понимают:)
Однако из-за слабости связи, просмотр класса ничего не говорит вам о классах вокруг него или о том, где он вписывается в общую картину.
Это не совсем точно. Для каждого класса вы точно знаете, от каких объектов зависит класс, чтобы иметь возможность предоставлять его функциональность во время выполнения.
Вы знаете их, так как знаете, какие объекты предполагается вводить.
Чего вы не знаете, так это фактического конкретного класса, который будет внедрен во время выполнения, который будет реализовывать интерфейс или базовый класс, от которого, как вы знаете, зависит ваш класс (ы).
Поэтому, если вы хотите увидеть, что именно вводится в настоящий класс, вам просто нужно посмотреть файл конфигурации для этого класса, чтобы увидеть конкретные вводимые классы.
Вы также можете использовать возможности, предоставляемые вашей IDE.
Поскольку вы ссылаетесь на Eclipse, у Spring есть плагин для него, а также визуальная вкладка, отображающая настроенные вами bean-компоненты. Вы это проверяли? Разве это не то, что вы ищете?
Также проверьте то же обсуждение в весеннем форуме
ОБНОВИТЬ:
Читая ваш вопрос еще раз, я не думаю, что это настоящий вопрос.
Я имею в виду это следующим образом.
Как и все loose coupling
не является панацеей и имеет свои недостатки как таковые.
Большинство, как правило, фокусируются на преимуществах, но, как и любое решение, имеют свои недостатки.
Что вы делаете в своем вопросе, так это опишите один из его основных недостатков, который заключается в том, что на самом деле не так просто увидеть общую картину, поскольку у вас есть все, что можно настроить и подключить к чему-либо.
Есть и другие недостатки, на которые можно жаловаться, например, что это медленнее, чем в жестко связанных приложениях, и все же это правда.
В любом случае, повторяя, то, что вы описываете в своем вопросе, не является проблемой, на которую вы ступили, и может найти стандартное решение (или любое другое для этого).
Это один из недостатков слабой связи, и вы должны решить, будет ли эта стоимость выше, чем вы фактически получаете от нее, как в любом компромиссном решении.
Это как спрашивать:
Эй, я использую этот шаблон с именем Singleton
, Это прекрасно работает, но я не могу создавать новые объекты! Как я могу обойти эту проблему, ребята????
Ну, вы не можете; но если вам нужно, возможно, синглтон не для вас....
Одна вещь, которая помогла мне, это поместить несколько тесно связанных классов в один файл. Я знаю, что это противоречит общему совету (иметь 1 класс на файл), и я в целом согласен с этим, но в моей архитектуре приложения это работает очень хорошо. Ниже я попытаюсь объяснить, в каком случае это.
Архитектура моего бизнес-уровня разработана вокруг концепции бизнес-команд. Определены классы команд (простой DTO с только данными и без поведения), и для каждой команды есть "обработчик команд", который содержит бизнес-логику для выполнения этой команды. Каждый обработчик команд реализует общий ICommandHandler<TCommand>
интерфейс, где TCommand - фактическая бизнес-команда.
Потребители зависят от ICommandHandler<TCommand>
и создавать новые экземпляры команд и использовать внедренный обработчик для выполнения этих команд. Это выглядит так:
public class Consumer
{
private ICommandHandler<CustomerMovedCommand> handler;
public Consumer(ICommandHandler<CustomerMovedCommand> h)
{
this.handler = h;
}
public void MoveCustomer(int customerId, Address address)
{
var command = new CustomerMovedCommand();
command.CustomerId = customerId;
command.NewAddress = address;
this.handler.Handle(command);
}
}
Теперь потребители зависят только от конкретного ICommandHandler<TCommand>
и не имеют понятия о фактической реализации (как и должно быть). Однако, хотя Consumer
ничего не должен знать о реализации, во время разработки я (как разработчик) очень заинтересован в реальной бизнес-логике, которая выполняется просто потому, что разработка выполняется в вертикальных слоях; Это означает, что я часто работаю над пользовательским интерфейсом и бизнес-логикой простой функции. Это означает, что я часто переключаюсь между бизнес-логикой и логикой пользовательского интерфейса.
Так что я сделал, поставив команду (в этом примере CustomerMovedCommand
и реализация ICommandHandler<CustomerMovedCommand>
) в том же файле, сначала с помощью команды. Поскольку сама команда является конкретной (поскольку она является DTO, нет смысла абстрагировать ее), перейти к классу легко (F12 в Visual Studio). Поместив обработчик рядом с командой, переход к команде означает также переход к бизнес-логике.
Конечно, это работает, только когда команда и обработчик могут жить в одной сборке. Когда ваши команды необходимо развернуть отдельно (например, при повторном их использовании в сценарии клиент / сервер), это не будет работать.
Конечно, это всего лишь 45% моего уровня бизнеса. Тем не менее, еще одним большим умиротворением (скажем, 45%) являются запросы, которые спроектированы аналогично, с использованием класса запросов и обработчика запросов. Эти два класса также помещаются в один и тот же файл, что позволяет снова быстро перейти к бизнес-логике.
Поскольку команды и запросы составляют около 90% моего бизнес-уровня, в большинстве случаев я могу очень быстро перейти от уровня представления к бизнес-уровню и даже легко перемещаться по бизнес-уровню.
Я должен сказать, что это единственные два случая, когда я помещаю несколько классов в один файл, но значительно упрощает навигацию.
Если вы хотите узнать больше о том, как я это разработал, я написал об этом две статьи:
На мой взгляд, слабосвязанный код может вам сильно помочь, но я согласен с вами по поводу его читабельности. Настоящая проблема заключается в том, что имена методов также должны содержать ценную информацию.
Это принцип Интерфейса раскрытия намерений, заявленный в "Управлениидоменами" ( http://domaindrivendesign.org/node/113).
Вы можете переименовать метод get:
// intention revealing name
Date cutoff = myExpiryCutoffDateService.calculateFromPayment();
Я предлагаю вам внимательно прочитать принципы DDD, и ваш код может стать намного более читабельным и, следовательно, управляемым.
В зависимости от того, сколько разработчиков работают над проектами и хотите ли вы повторно использовать некоторые его части в разных проектах, слабая связь может вам сильно помочь. Если ваша команда большая, и проекту нужно охватить несколько лет, может помочь слабое связывание, так как работа может быть легче распределена между различными группами разработчиков. Я использую Spring/Java с большим количеством DI, и Eclipse предлагает некоторые графики для отображения зависимостей. Использование F3 для открытия класса под курсором очень помогает. Как указывалось в предыдущих постах, знание ярлыков для вашего инструмента поможет вам.
Еще одна вещь, которую следует учитывать, - это создание пользовательских классов или оболочек, поскольку их легче отслеживать, чем общие классы, которые у вас уже есть (например, Date).
Если вы используете несколько модулей или уровень приложения, вам может быть непонятно, что именно представляет собой поток проекта, поэтому вам может потребоваться создать / использовать какой-то пользовательский инструмент, чтобы увидеть, как все связано друг с другом. Я создал это для себя, и это помогло мне легче понять структуру проекта.
Я обнаружил, что Мозг может быть полезен в разработке как инструмент отображения узлов. Если вы напишете несколько сценариев для анализа вашего источника в XML, который принимает мозг, вы можете легко просматривать свою систему.
Секретный секрет заключается в том, что в комментарии к коду добавляются направляющие для каждого элемента, который вы хотите отслеживать, затем можно щелкнуть узлы в мозге, чтобы перейти к этому руководству в вашей среде IDE.
Документация!
Да, вы назвали главный недостаток слабосвязанного кода. И если вы, вероятно, уже поняли, что в конце это окупится, это правда, что всегда будет больше времени, чтобы найти "где" сделать ваши изменения, и вам, возможно, придется открыть несколько файлов, прежде чем найти "правильное место"...
Но вот когда-то действительно важное: документация. Странно, что ни в одном ответе нет явного упоминания о том, что это ГЛАВНОЕ требование во всех крупных разработках.
Документация по API
API Doc с хорошей функцией поиска. Что у каждого файла и - почти у каждого метода есть четкое описание.
Документация "Большая картина"
Я думаю, что хорошо иметь вики, которая объясняет общую картину. Боб сделал прокси систему? Как это работает? Это обрабатывает аутентификацию? Какой компонент будет использовать его? Не целый учебник, а просто место, где вы можете прочитать 5 минут, выяснить, какие компоненты задействованы и как они связаны друг с другом.
Я согласен со всеми пунктами ответа Марка Симанна, но когда вы впервые попадаете в проект, даже если вы хорошо понимаете принципы разъединения, вам нужно либо много гадать, либо что-то вроде помочь выяснить, где реализовать конкретную функцию, которую вы хотите разработать.
... Опять же: APIDoc и маленький разработчик Wiki.
Я поражен тем, что никто не писал о тестируемости (с точки зрения, конечно же, модульного тестирования) слабосвязанного кода и непробиваемости (в тех же терминах) тесно связанной конструкции! Нетрудно понять, какой дизайн вы должны выбрать. Сегодня со всеми структурами Mock и Coverage это очевидно, по крайней мере для меня.
Если вы не проводите модульные тесты своего кода или не думаете, что делаете их, но на самом деле вы этого не делаете... Тестирование в отдельности едва достигается при тесной связи.
Вы думаете, что вам нужно перемещаться по всем зависимостям от вашей IDE? Забудь об этом! Это та же ситуация, что и в случае компиляции и времени выполнения. В процессе компиляции вряд ли найдется какая-либо ошибка, вы не можете быть уверены, что она работает, если только вы не протестируете ее, что означает ее выполнение. Хотите знать, что находится за интерфейсом? Поставь точку останова и запусти проклятое приложение.
Аминь.
... обновляется после комментария...
Не уверен, что он вам пригодится, но в Eclipse есть нечто, называемое иерархическим представлением. Он показывает вам все реализации интерфейса в вашем проекте (не уверен, что рабочая область также). Вы можете просто перейти к интерфейсу и нажать F4. Затем он покажет вам все конкретные и абстрактные классы, реализующие интерфейс.