Как понять общую картину в слабосвязанном приложении? - PullRequest
42 голосов
/ 14 января 2012

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

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

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

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

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

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

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

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

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

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

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

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

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

(Умышленно оставили этот вопрос независимым от контейнера, так как меня интересуют ответы на любые вопросы).

Ответы [ 10 ]

34 голосов
/ 03 февраля 2012

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

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

  • Понимание слабосвязанной базы кода требует другого мышления . Хотя «прыгнуть к соавторам» сложнее, это также должно быть более или менее неактуально.
  • Слабая связь - это все о понимании части без понимания всей . Вам редко нужно понимать все это одновременно.
  • При обнаружении ошибки вы должны опираться на трассировки стека , а не на статическую структуру кода, чтобы узнать о соавторах.
  • Это ответственность разработчиков написание кода, чтобы его было легко понять - разработчик не читает код.
12 голосов
/ 14 января 2012

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

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

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

8 голосов
/ 04 сентября 2014

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

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


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

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

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

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

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

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

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, я надеюсь, что программисты постепенно поймут, что за конкретным интерфейсом может быть любая реализация этогоинтерфейс, и их клиентский код должен быть в состоянии обрабатывать любую реализацию этого интерфейса без изменения правильности системы.Если эта теория верна, мы только что ввели принцип подстановки Лискова в кодовую базу, не пугая никого концепциями высокого броуна, которые они не понимают:)

7 голосов
/ 14 января 2012

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

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

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

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

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

Также ознакомьтесь с тем же обсуждением в Весенний форум

UPDATE:
Читая ваш вопрос еще раз, я не думаю, что это настоящий вопрос.
Я имею в виду это следующим образом.
Как и все вещи, loose coupling не является панацеей и имеет свои недостатки как таковые.
Большинство, как правило, фокусируются на преимуществах, но, как и любое решение, имеют свои недостатки.

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

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

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

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

5 голосов
/ 14 января 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% моего бизнес-уровня, в большинстве случаев я могу очень быстро перейти с уровня представления на бизнес-уровень и даже легко перемещаться по бизнес-уровню.

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

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

4 голосов
/ 05 февраля 2012

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

Это принцип раскрытия намерений , как указано Дизайн, управляемый доменом (http://domaindrivendesign.org/node/113).

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

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

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

2 голосов
/ 07 февраля 2012

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

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

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

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

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

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

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

2 голосов
/ 02 февраля 2012

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

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

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

2 голосов
/ 14 января 2012

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

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

0 голосов
/ 07 февраля 2012

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

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

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

Аминь.

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

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

The hierarchy view in Eclipse after pressing F4

...