Как понять общую картину в слабосвязанном приложении?

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

Многие классы в стиле «сервис» имеют конструктор и один метод, реализующий интерфейс. Каждый отдельный класс очень легко понять по отдельности.

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

Нелегко перейти к соавторам, использующим Eclipse, потому что вам нужно пройти через интерфейсы. Если интерфейс Runnable, это не поможет определить, какой класс на самом деле подключен. На самом деле необходимо вернуться к определению контейнера DI и попытаться выяснить, что там происходит.

Вот строка кода из класса обслуживания с внедренной зависимостью: -

  // myExpiryCutoffDateService was injected, 
  Date cutoff = myExpiryCutoffDateService.get();

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

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

  ExpiryDateService = new ExpiryDateService();
  Date cutoff = getCutoffDate( databaseConnection, paymentInstrument );

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

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

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

Кто-нибудь еще испытывает эту проблему? Какие у вас есть решения? Это просто что-то, чтобы приспособиться? Существуют ли какие-либо инструменты для визуализации того, как классы связаны друг с другом? Должен ли я сделать классы больше или более связанными?

(Намеренно оставил этот вопрос контейнерно-агностическим, так как меня интересуют ответы на любые).


person WW.    schedule 13.01.2012    source источник


Ответы (10)


Хотя я не знаю, как ответить на этот вопрос в одном абзаце, вместо этого я попытался ответить на него в блоге: http://blog.ploeh.dk/2012/02/02/LooseCouplingAndTheBigPicture.aspx

Подводя итог, я считаю, что наиболее важными моментами являются:

  • Для понимания слабосвязанной базы кода требуется другое мышление. Хотя «перейти к соавторам» труднее, это также должно быть более или менее неуместным.
  • Слабое связывание — это понимание части без понимания целого. Вам редко нужно понимать все это одновременно.
  • Выявляя ошибку, вы должны полагаться на трассировку стека, а не на статическую структуру кода, чтобы узнать о соавторах.
  • Разработчики обязаны писать код, чтобы убедиться, что его легко понять, а не разработчик, читающий код.
person Mark Seemann    schedule 02.02.2012
comment
+1. Часть вашей проблемы с пониманием целого может быть просто в том, что ваша кодовая база плохо организована. Ваши соавторы, вероятно, должны находиться в одной и той же папке/ближайших папках. если ваша архитектура верхнего уровня выглядит как /models/views/controllers, неудивительно, что вы запутались, поскольку у вас, вероятно, есть грузовик файлов в каждой папке. Кластеризуйте модули вместе по их отношению к проблемной области, а не по «типу файла». Подробнее по этой теме: blog.8thlight.com/ дядя-боб/2011/09/30/ - person timoxley; 03.02.2012
comment
Возможно, наши абстракции слишком абстрактны. В основном мы используем 4 интерфейса, каждый из которых содержит по одному методу: run(), ‹T›get(), appendXML(XMLElement e),modify(‹T›thing). - person WW.; 07.02.2012
comment
Звучит так, как будто они идеально подходят для RAP, но сами по себе они мало сообщают о ролях, которые они играют. Возможно, вы можете использовать для этого имена параметров/переменных. - person Mark Seemann; 07.02.2012
comment
Единственная проблема, связанная с использованием трассировки стека, заключается в том, что у вас ее нет, и вы не можете легко ее получить, и вы находитесь на стадии попытки сформировать гипотезы, объясняющие сбой, прежде чем вы начнете (потенциально навязчивые) исследования для проверки. их. Если вы диагностируете производственную ошибку с помощью асинхронного кода, часто отсутствует значимая трассировка стека. Эти моменты делают диагностику слабосвязанных систем особенно болезненной. - person Ian Griffiths; 05.09.2014

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

Я использую Visual Studio и специально созданный фреймворк, поэтому проблема, которую вы описываете, — это моя жизнь. В Visual Studio SHIFT+F12 — мой друг. Он показывает все ссылки на символ под курсором. Через некоторое время вы привыкаете к обязательно нелинейной навигации по вашему коду, и становится второй натурой думать о том, «какой класс реализует этот интерфейс» и «где находится сайт внедрения/конфигурации, чтобы я мог видеть, какой класс используется для удовлетворения этой зависимости интерфейса».

Также доступны расширения для VS, которые предоставляют улучшения пользовательского интерфейса, чтобы помочь в этом, такие как Productivity Power Tools. . Например, вы можете навести указатель мыши на интерфейс, появится информационное окно, и вы можете нажать «Реализовано», чтобы увидеть все классы в вашем решении, реализующие этот интерфейс. Вы можете дважды щелкнуть, чтобы перейти к определению любого из этих классов. (Я все равно обычно просто использую SHIFT+F12).

person Igby Largeman    schedule 13.01.2012
comment
+1. Для этого мы используем ReSharper. Это имеет большое значение. - person TrueWill; 14.01.2012
comment
Я не мог жить без ReSharper. Я убедил три последние команды, в которых я работал, инвестировать в него, и это также изменило их жизнь. - person scottm; 06.02.2012

У меня только что была внутренняя дискуссия по этому поводу, и в итоге я написал эту статью, которая, я думаю, слишком хороша, чтобы ею не поделиться. Я копирую его сюда (почти) неотредактированным, но, несмотря на то, что это часть более крупного внутреннего обсуждения, я думаю, что большая его часть может стоять отдельно.

Обсуждается введение пользовательского интерфейса под названием IPurchaseReceiptService и вопрос о том, следует ли его заменить на использование IObserver<T>.


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

public interface IPurchaseReceiptService
{
    void SendReceipt(string transactionId, string userGuid);
}

Если мы сохраним его как Интерфейс заголовка, которым он является в настоящее время, он будет иметь только этот единственный метод SendReceipt. Это классно.

Что не так круто, так это то, что вам пришлось придумать имя для интерфейса, и другое имя для метода. Между ними есть некоторое совпадение: слово Receipt появляется дважды. IME, иногда это совпадение может быть еще более выраженным.

Кроме того, имя интерфейса — IPurchaseReceiptService, что тоже не особо полезно. Суффикс Service — это, по сути, новый Manager и, по моему мнению, запах дизайна.

Кроме того, вам нужно было не только назвать интерфейс и метод, но вы также должны назвать переменную при ее использовании:

public EvoNotifyController(
    ICreditCardService creditCardService,
    IPurchaseReceiptService purchaseReceiptService,
    EvoCipher cipher
)

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

Теперь сравните это с использованием хорошо известного интерфейса, такого как IObserver<T>:

public EvoNotifyController(
    ICreditCardService creditCardService,
    IObserver<TransactionInfo> purchaseReceiptService,
    EvoCipher cipher
)

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


Когда дело доходит до обсуждения «отключенности», я не питаю иллюзий, что использование IObserver<T> волшебным образом решит эту проблему, но у меня есть другая теория на этот счет.

Моя теория состоит в том, что причина, по которой многим программистам так сложно программировать интерфейсы, заключается именно в том, что они привыкли к функции Visual Studio Перейти к определению (кстати, это еще один пример того, как инструментарий разлагает разум). Эти программисты постоянно находятся в состоянии, когда им нужно знать, что находится «по ту сторону интерфейса». Почему это? Может быть, это потому, что абстракция плохая?

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

Однако, если вы применяете RAP, я надеюсь, что постепенно программисты узнают, что за конкретным интерфейсом может скрываться любая реализация этого интерфейса, и их клиентский код должен быть в состоянии обрабатывать любую реализацию этого интерфейса. этот интерфейс без изменения правильности системы. Если эта теория верна, мы только что внедрили принцип подстановки Лисков в базу кода, никого не напугав. с высокоинтеллектуальными понятиями они не понимают :)

person Mark Seemann    schedule 04.09.2014
comment
Я не говорю, что интерфейс лучше, но OnNext (значение TransactionInfo) сообщает we're observing transactions that happened, тогда как SendReceipt сообщает this will cause a receipt to be sent. «purchaseReceiptService» не говорит мне об этом в любом случае. Мои 2 цента. - person Yves Reynhout; 04.09.2014
comment
Политически корректный ответ Эти программисты постоянно находятся в состоянии ума, когда им нужно знать, что находится «по ту сторону интерфейса». Почему? вероятно, потому, что отсутствуют модульные тесты. Я большой поклонник документации, когда это необходимо, и удобочитаемого именования, чтобы уменьшить количество экземпляров документации, поэтому нет, может быть, это потому, что абстракция плохая? Я думаю, гораздо более вероятно, что конкретные имена являются бедными, затемняющими семантику абстракции. - person Andy Dent; 04.09.2014
comment
На этой основе вы будете создавать интерфейсы, основанные только на подписи. В сочетании с дженериками вам нужно всего несколько. Выполнить, Процесс‹T›, Поставщик‹T›, Калькулятор‹X,Y›. Названия интерфейсов перестают нести какой-либо смысл. Но управление сложностью заключается в поиске полезных абстракций. Эти абстракции настолько абстрактны, что становятся бесполезными. - person WW.; 05.09.2014
comment
@WW. Абстракции по-прежнему называются. Они просто названы по именам своих переменных, а не по их типам. Код, написанный на многих языках программирования (то есть на всех языках с динамической типизацией), работает так. - person Mark Seemann; 05.09.2014

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

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

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

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

Вы также можете использовать средства, предоставляемые вашей IDE.
Поскольку вы ссылаетесь на Eclipse, у Spring есть подключаемый модуль для него, а также визуальная вкладка, отображающая настроенные вами bean-компоненты. Вы проверили это? Разве это не то, что вы ищете?

Также ознакомьтесь с тем же обсуждением на Spring Forum

ОБНОВЛЕНИЕ:
Еще раз прочитав ваш вопрос, я не думаю, что это настоящий вопрос.
Я имею в виду следующее.
Как и все остальное, loose coupling не является панацея и сама по себе имеет свои недостатки.
Большинство склонны сосредотачиваться на преимуществах, но, как и у любого решения, у него есть свои недостатки.

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

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

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

Это все равно, что спросить:
Эй, я использую шаблон с именем Singleton. Он отлично работает, но я не могу создавать новые объекты! Ребята, как мне обойти эту проблему????
Ну, вы не можете; но если вам нужно, возможно, синглтон не для вас....

person Cratylus    schedule 13.01.2012
comment
+1. Может быть, интерфейсы просто нуждаются в лучшей документации, чтобы вы знали, чего можно ожидать от внедренных классов? - person Ocelot20; 02.02.2012
comment
В моем вопросе я, вероятно, ничего не сказал о реализации классов вокруг него. Интеллектуально я знаю, что это не должно иметь значения, но это все равно замедляет чтение и понимание кода при поиске правильного способа добавить новую функцию или исправить ошибку и т. д. - person WW.; 03.02.2012
comment
Ваше обновление интересно. Вы, кажется, говорите, что это очевидно. Ничто из того, что я читал о слабой связи, не упоминало об этом как о недостатке, и поэтому мы обнаружили это после написания кода. Я думаю, стоит понять, как улучшить проблемные места или, возможно, мы сделали связь слишком слабой. - person WW.; 07.02.2012
comment
@WW.: Как уже упоминалось, у всех подходов есть свои плюсы и минусы. Даже ООП не везде подходит, например. микропрограммирование. У всего есть компромисс. Отражение, DI, MVC, шаблоны проектирования и т. Д. Для всего этого вы знаете преимущества, но есть и недостатки. вы получаете гибкость с шаблонами проектирования, но также получаете действительно большое количество классов. Отражение = меньшая скорость и т. д. Все это действительно в некоторой степени проблемы. Когда вы проектируете, вы выбираете компромисс. В вашем случае вы прочитали только плюсы. Если вы начнете гуглить минусы, то увидите, что они упоминаются. Вы просто не читали всю историю - person Cratylus; 07.02.2012
comment
@WW.: Просто начните искать полный обзор и прочитайте минусы и недостатки. Вы только что наткнулись на один из них. Надеюсь, преимущества больше, чем ваши проблемы. В противном случае прочитайте мою аналогию для Синглтона. - person Cratylus; 07.02.2012

Одна вещь, которая мне помогла, — это разместить несколько тесно связанных классов в одном файле. Я знаю, что это противоречит общему совету (иметь 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% моего бизнес-уровня, в большинстве случаев я могу очень быстро переходить с уровня представления на бизнес-уровень и даже легко перемещаться внутри бизнес-уровня.

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

Если вы хотите узнать больше о том, как я это разработал, я написал об этом две статьи:

person Steven    schedule 14.01.2012

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

Это принцип Интерфейса, выявляющего намерения, изложенный в Domain Driven Design ( http://domaindrivendesign.org/node/113 ).

Вы можете переименовать метод get:

// intention revealing name
Date cutoff = myExpiryCutoffDateService.calculateFromPayment();

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

person Dimitri    schedule 05.02.2012

Я обнаружил, что The Brain полезен при разработке в качестве инструмента сопоставления узлов. Если вы напишете несколько скриптов для разбора вашего исходного кода в XML, который принимает Brain, вы сможете легко просмотреть свою систему.

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

person Mark Robbins    schedule 14.01.2012

В зависимости от того, сколько разработчиков работает над проектами и хотите ли вы повторно использовать некоторые его части в разных проектах, слабая связь может вам очень помочь. Если ваша команда большая и проект должен длиться несколько лет, слабая связанность может помочь, так как работу легче распределять между разными группами разработчиков. Я использую Spring/Java с большим количеством DI, а Eclipse предлагает несколько графиков для отображения зависимостей. Использование F3 для открытия класса под курсором очень помогает. Как говорилось в предыдущих постах, вам поможет знание ярлыков для вашего инструмента.

Еще одна вещь, которую следует учитывать, — это создание пользовательских классов или оболочек, поскольку их легче отслеживать, чем обычные классы, которые у вас уже есть (например, Date).

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

person Bran    schedule 01.02.2012

Документация!

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

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

Документация по API
APIDoc с удобной функцией поиска. Что каждый файл и --почти-- каждый метод имеют четкое описание.

Документация по общей картине
Я думаю, хорошо иметь вики, объясняющую общую картину. Боб сделал прокси-систему? Как это работает? Обрабатывает ли он аутентификацию? Какой компонент будет его использовать? Не весь туториал, а просто место, где можно 5 минут почитать, разобраться, какие компоненты задействованы и как они между собой связаны.

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

... Опять же: APIDoc и немного вики для разработчиков.

person FMaz008    schedule 06.02.2012

Я поражен тем, что никто не написал о тестируемости (конечно, с точки зрения модульного тестирования) слабосвязанного кода и нетестируемости (в тех же терминах) сильносвязанного дизайна! Нет ничего сложного в том, какой дизайн выбрать. Сегодня со всеми фреймворками Mock и Coverage это очевидно, ну по крайней мере для меня.

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

Вы думаете, что вам нужно перемещаться по всем зависимостям из вашей IDE? Забудь об этом! Это та же ситуация, что и в случае компиляции и выполнения. Вряд ли какая-либо ошибка может быть обнаружена во время компиляции, вы не можете быть уверены, работает ли она, если не протестируете ее, то есть не выполните ее. Хотите знать, что скрывается за интерфейсом? Поставь точку останова и запусти проклятое приложение.

Аминь.

...обновлено после комментария...

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

Представление иерархии в Eclipse после нажатия F4

person Jagger    schedule 06.02.2012
comment
Тестируемость в нашем коде, написанном таким образом, практически идеальна. Но я пытаюсь решить проблемы. Ваш ответ - использовать отладчик, что означает, что все снова становится конкретным. - person WW.; 07.02.2012
comment
@WW.: Я не знаю, какую IDE вы используете, но для понимания общей картины может быть полезно представление Eclipse Hierarchy View. - person Jagger; 07.02.2012