Pull to refresh

Comments 105

Интересно увидеть топик этот в .NET сообществе, что вас так подтолкнул.
К сожалению, я его не нашел. В оправдание могу сказать, что читал я его прошлым летом.
Долго же Вы катились к этому посту ;)
Простите, а как связан принцип инверсии зависимостей и юнит тесты? Имхо, грамотную архитектуру нужно строить опираясь на SOLID, а не на возможные тесты.
Я и не писал, что тесты это самоцель. Тесты просто стимулируют соблюдать solid, не позволяя высокой связанности проникать в код.
Ну, на самом деле, наоборот. В истории, описанной в вашем посте, тесты — это самоцель, потому что пост написан с позиции разработчика тестов: «Джонни претендовал на вакансию разработчика модульных тестов и был расстроен низким качеством кода, который ему вменялось тестировать. „

Другое дело, что это совсем не так плохо, как кажется.

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

Это цитата автора SOLID.
1) Рекомендую забыть про Moq и использовать NSubstitue. Намного удобнее.
2) в одном тесте 4 Assert. Не очень хорошая идея. Первый же Fail и вы не узнаете результаты других проверок. По факту у вас должно быть 3 теста как я понял из спецификации.
3) Название SendMessageFullTest. Оно ужасно. Название должно полностью показывать что тест должен делать и что произошло в случае провала. лучший вариант — строить название теста по схеме MethodName_Scenario_ExpectedBehavior
т.е. если разделить те приемлемым названием будет как то так AddMessage_PlainMessageSended_SavedOnce
Недорефакторили. У вас много единообразного сетапа, который прекрасно выносится в общий метод.
В общий метод сетап не вынесешь. Можно, конечно, создать несколько методов, по отдельности для каждого мока, id и т.д. Но лучше от этого код не станет.
Почему не вынесешь? Создание всех моков и целевого объекта у вас идентичны, а это уже четыре строчки кода. Им место в тест-сетапе. Создание параметров — тоже. Единственное отличие — это то, как сетапится IsUserOnline, и вот ему место в тесте. Кстати, если бы сократили код таким образом, то увидели бы, что у вас в первом тесте тоже есть этот сетап, а его там быть не должно.
Вы предлагаете сделать мок объекты приватными членами класса, содержащего тестовые методы, и устанавливать им значения в приватной функции?
Вообще-то, это стандартное решение.
Для рабочего проекта — да. В качестве примера к топику — нет. Лично меня бы раздражало в чужом топике сначала искать приватный метод тремя строчками, а потом возвращаться к месту где он был вызван. Лучше уж так.
А не надо его описывать как приватный метод, просто сделайте TestSetup в начале кода, и все будет легко и читаемо. Этот паттерн сделан специально для повышения читаемости тестов (ну и чтобы избежать дублирования кода).
На самом деле, статья — прекрасная иллюстрация к регулярно возникающему непониманию «что для чего». В данном случае, на самом деле, пост написан от лица тестировщика, которому неудобно тестировать код — окей, прекрасная позиция, мы очень любим модульное тестирование, от него много пользы, тестировщик имеет право быть возмущенным и настаивать на модификации кода под его нужды. Но зачем выдавать нужды тестировщика за нужды приложения?

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

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

А вот теперь вернемся к цитате из поста:

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

Архитектура приложения не может оцениваться вне зависимости от требований к приложению. Не бывает универсально хороших архитектур. И вои тут мы приходим к допущениям. И первое допущение, которым оперируют люди, пропагандирующие IoC — это то, что нам когда-нибудь понадобится заменить одну реализацию на другую. Так вот, это допущение верно для не такого уж большого количества проектов, а для всех остальных — это антипаттерн preemptive generalization, который ничем не лучше предварительной оптимизации.

Поэтому, мне кажется, уже давно пора признать: IoC в его нынешнем понимании и применении чаще всего необходим только для модульного тестирования. Я не хочу сказать, что это плохо, поскольку тестируемость — это тоже один из атрибутов архитектуры; я просто хочу, чтобы мы не лукавили, говоря, что «помимо тестов, мы улучшили еще и архитектуру». Не улучшили. Просто сделали так, чтобы нам было удобно писать тесты.
Приведите пожалуйста хоть один пример, почему высокая связанность может оказаться в итоге лучшим решением. Я таковых не знаю. А чем плоха высокая связанность знаю на собственном опыте очень хорошо. Из недавнего — повторное использование кода. Сейчас мы переписываем проект, оказавшийся погубленным отсутствием этого самого dependency injection. Проект состоит из нескольких модулей и часть из них нам кажется неплохой. Но использовать повторно их никак не получится, потому что все модули так крепко связаны между собой, что перетащить можно только все целиком, но никак ни один кусочек.
Приведите пожалуйста хоть один пример, почему высокая связанность может оказаться в итоге лучшим решением.

Так, стоп. Давайте вы не будете подменять IoC на связность (высокую или низкую). IoC — это один из путей к низкой связности, но не единственный.

Сейчас мы переписываем проект, оказавшийся погубленным отсутствием этого самого dependency injection.

И отдельно не надо путать IoC и DI. Второе — частный случай первого, но не наоборот.

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

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

Я ничего не говорил про IoC. Вопрос следующий, в каких случаях прямое взаимодействие между объектами лучше, чем взаимодействие через интерфейс.
И отдельно не надо путать IoC и DI. Второе — частный случай первого, но не наоборот.

Как вы в фразе
Сейчас мы переписываем проект, оказавшийся погубленным отсутствием этого самого dependency injection.
нашли что то про IoC?
когда вы понимаете, что интерфейсы нужно оставлять так, чтобы они были видимы везде

А вот это в корне не верное рассуждение. Представьте, что у вас приложение разбито на несколько слоев — слой приложения, слой бизнес логики и слой доступа к данным. Так вот, слой приложения видит только интерфейсы слоя бизнес логики и ничего не знает об интерфейсах слоя доступа к данным. О существовании этих интерфейсов знает только слой бизнес логики.
Я ничего не говорил про IoC.

В статье — а я изначально писал комментарий именно к статье — говорили.

В его коде начинают появляться фабрики и IoC контейнер, а на столе книга gof про паттерны.[...] И, самое главное, все зависимости мы теперь можем подменить, передав в конструктор заглушки для теста.

Это — IoC.

Вопрос следующий, в каких случаях прямое взаимодействие между объектами лучше чем взаимодействие через интерфейс.

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

А вот это в корне не верное рассуждение.

Под «везде» я понимал «во всех изначально связанных модулях». Речь шла о вашем конкретном примере.
Уважаемый lair, мне кажется вы меня просто троллите. Засим откланиваюсь)
А мне кажется, что у вас радость неофита модульных тестов (причем, как видно из допускаемых вами ошибок в тестировании — именно неофита), и вы пытаетесь продать эту траву дальше под стандартными аргументами.
Как-то уж совсем толсто
Хотите, кстати, наглядный пример излишней абстракции?
А дайте пример, интересно.

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

К сожалению, нет.

Рассмотрим следующий дизайн: есть прикладной слой, есть некий data facade (упрощенный репозиторий), есть ORM. Каждый слой знает только о следующем, как и полагается. Смысл датафасада в том, чтобы упростить работу с БД до тривиальных операций, отвлекшись от конкретной структуры БД, выраженной в ORM. Собственно, датафасад и вырос из TDD, поскольку для тестирования прикладного слоя было слишком накладно описывать весь ORM, особенно учитывая, что структуру БД мы еще не определили. В общем, все хорошо, спасибо (без издевки) TDD.

При этом из требований мы точно знаем, что у нас никогда не будет рантайм (и даже компайл-тайм) подмены ни дата-фасада, ни ORM: жестко фиксированная СУБД, предсказуемый цикл разработки. Иными словами, для приложения жесткая зависимость «прикладной слой знает о конкретном и единственном датафасаде, датафасад знает о конкретном и единственном ORM» ничем не плоха, все требования выполняются. Даже если пофантазировать, и решить, что мы очень сильно хотим работать с другой СУБД (и при этом настолько гениальны, что заранее учли это в датафасаде) — нужно разорвать зависимость прикладного слоя и датафасада, но каждый конкретный датафасад может продолжать знать о конкретном ORM без каких-либо потерь для функциональности приложения и его расширяемости.

(ура, вступление закончено)

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

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

Дальше вопрос как раз в том, оправдывает ли (в каждом конкретном случае) выигрыш от тестового покрытия (причем на самых ранних этапах) оверхед от дополнительного уровня абстракции.
Можно, в общем, сократить.
Введение каждого слоя абстракции должно соответствовать принципу KISS.
Если после введения абстракции код стал проще (снизилась цикломатическая сложность или там стало банально меньше строк кода) — это хорошая абстракция. Иначе — плохая.
И да, на одних только модульных тестах свет клином не сошелся. Они позволяют отловить далеко не все баги, а те, что все-таки позволяют — не всегда оптимальным образом.
В приведённом в статье примере главный источник потенциальных проблем — это функция SaveMessage, вызов которой может сфейлиться по миллиону причин — от некорректных передаваемых агрументов до сдохшей базы. Однако, всем пофиг, что там она возращает. Главное интерфейсики выделить, и сразу архитектура станет хорошей.
Пример хороший, однако стоит заметить, что сейчас уже существуют стандартные методы подмены запечатанных и даже статических(!) классов и методов, например Microsoft Fakes.

Поэтому, хотя я в целом согласен с вашей точкой зрения, но аргумент о технологических ограничениях платформы не состоятелен.
Microsoft Fakes — это прекрасно, только вот они не общедоступны (в отличие от Moq или NSubstitute), да и интерфейс пока оставляет желать лучшего. Так что пока для массового применения остаются подмена на основе интерфейсов.
Пример притянут за уши. Автор топика нигде и не утверждает, что надо модульно тестировать все и вся, и не вводит никаких «нефункциональных требований на расширяемость и поддерживаемость», а говорит только о контроле над связанностью.

Насчет Вашего примера — Вы сами только что описали случай, где TDD помогла — при разработке прикладного слоя в изоляции от слоя персистентности. А слой персистентности (ORM + датафасад) — это другой случай. Если этот слой тривиален, его компоненты отлично согласуются друг с другом и больше ни от чего не зависят, там нет никакой логики и там нечего глубоко проверять — ну и не надо его разделять на два независимых слоя. Это вполне нормальное решение. KISS никто не отменял, интеграционные тесты дадут уверенность в том, что все ок.

А вот если уже есть что тестировать в изоляции — значит и сложность повышена, и увеличение числа слоев для уменьшения их сложности — оправданный шаг.

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

Так я и ничего не имею против TDD (или модульного тестирования).
Пример притянут за уши. Ваш датафасад, скрывающий структуру БД, это интерфейс. А его реализация — это объект, инкапсулирующий в себе работу с ORM контейнером. Зачем отделять ORM от вашего датафасада слоем для меня загадка.То есть примера нет.
Зачем отделять ORM от вашего датафасада слоем для меня загадка.

Незачем. Собственно, никакого слоя там и нет. А вот интерфейс вводить приходится — потому что иначе это не удается протестировать.
Зачем вводить интерфейс между датафасадом и ORM-ом то?.. В вашем случае датафасад инкапсулирует в себе ORM контейнер. Это не разные модули, их не нужно друг от друга изолировать.
Чтобы протестировать датафасад, очевидно.

А если вы считаете, что это избыточное тестирование, то поднимитесь на уровень выше, где ситуация строго аналогична — мне низачем, кроме тестирования, не нужен интерфейс между датафасадом и прикладным кодом, но он там есть.
Архитектурно, реализация датафасада содержит в себе ORM контейнер. ORM контейнер является неотъемлемой частью реализации, а не внешней зависисмостью. Понимаете? Никакого собственного функционала, который можно протестировать в отрыве от ORM контейнера, у датафасада нет. Ваш пример надуман.
Никакого собственного функционала, который можно протестировать в отрыве от ORM контейнера, у датафасада нет.

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

Ваш пример надуман.

Так я вам специально предложил подняться на уровень повыше, на границу между прикладным кодом и датафасадом. Там тоже нечего отдельно тестировать?
То есть вы собираетесь процесс обращения к базе данных размазать по нескольким обьектам чтобы добиться факта, что тестировать ваши объекты по отдельности смысла не имеет. Изящно.
Вы, похоже, в упор не хотите увидеть того, что вам предлагают отвлечься от того, сколько именно объектов занимается БД, и обсудить границу между прикладным слоем и DAL. Да?

Повторю пример еще раз: есть прикладной слой, есть DAL. По техническим требованиям DAL статичен, замены на другой не будет никогда. Вопрос: зачем кроме тестирования мне вводить интерфейс, описывающий DAL (или объявлять абстрактный класс, или вводить виртуальные методы)? Почему (кроме тестирования) я не могу сделать DAL статическим фасадом, который бы полностью скрыл от меня все его детали?
В случае, если мы сейчас говорим о взаимодействии deal и прикладного слоя, вы вправе сделать dal статическим, если вы
не собираетесь отдельно тестировать прикладной слой. Но вы лишаетесь гибкости и можно придумать кучу вариантов при которых использование статических классов начнёт вас смущать
Вот мы и пришли к тому тезису, который озвучивался изначально: если бы не тестирование, вся эта красота с дополнительными интерфейсами оказалась бы никому не нужна. Что, как бы, наводит нас на сомнения в «правильности» архитектуры.
Да, всё так! Но статический фасад — всё-таки перебор, с аргументацией из области шестого чувства :))
Ваше шестое чувство растет из того, что мы все привыкли использовать IoC. Потому что так написано в книжках умными людьми, которые, в общем-то, нигде вокруг не ошибаются. Собственно, мое шестое чувство говорит мне то же самое — «ааа, статический фасад, паника-паника». А потом, когда начинаешь сидеть с конкретикой, становится понятно, что объективных причин для этой паники и нет. Кроме тестирования. И вот тогда задумываешься — может, мы что-то не так делаем? Может, я что-то не то в книжке прочитал?
Конечно же мы что-то не так делаем. Умные дядьки из книжек ошиблись, не написав во введениях к своим книгам крупными буквами: «Not a silver bullet. Применять с умом». Отсюда ИМХО и все проблемы (с ООП), обсасываемые в последнее время на Хабре

Но всё же статический фасад — паника-паника :-) Ну всякое же может быть, чёрт его знает! Я руководствуюсь мыслью, что при разработке функционала с нуля, налабать статический класс по трудозатратам и кол-ву буковок — сопоставимо с передачей ссылки на него же извне. А при таком раскладе и раз в год какой-нибудь несложный реюз будет возможен или наследника передать, раз в пол года.
С другой стороны, конечно, статический класс может делегировать вызовы к какой-нибудь сторонней реализации и мы получим примерно те же яйца, может чуть другого размера…
Я руководствуюсь мыслью, что при разработке функционала с нуля, налабать статический класс по трудозатратам и кол-ву буковок — сопоставимо с передачей ссылки на него же извне.

Не-а. Если не тестировать — то статический фасад дешевле, а если тестировать — то экземпляр дешевле.
Требования к приложению:
программного обеспечения для предприятий финансового сектора.

Если бы Вы знали, что это за приложения такие для финансового сектора, Вы бы не писали все эти глупости.
Вас не затруднит пояснить свою мысль? Я не знаю, что такое «приложения для финансового сектора», я знаю, что такое «приложения для крупного банка России», и там эти «глупости» прекрасно работают.
Зато появился новый уровень абстрации, не обусловленный никакой семантикой.. Этот уровень абстрации усложняет код (удорожает рефакторинг, ...

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

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

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

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

Не улучшили. Просто сделали так, чтобы нам было удобно писать тесты. Я не хочу сказать, что это плохо, поскольку тестируемость — это тоже один из атрибутов архитектуры; я просто хочу, чтобы мы не лукавили, говоря, что «помимо тестов, мы улучшили еще и архитектуру».

Еще как улучшили, так как увеличили ее гибкость.

Я предполагаю, что Вы просто не разделяете понятия «хорошая архитектура» и «простая архитектура». Это далеко не одно и то же.

Да и нет в сложности нет ничего плохого, если она контролируема.
это так же гибко и правильно, как если в квартире каждый светильник включать через вилку-розетку. и теперь весь сыр-бор вокруг подключения потолочной люстры — когда тестировщикам ужас как хочется заместо неё втыкать свои имитаторы, а хозяевам нафиг не нужно что-то вместо неё подключать и вообще портит интерьер. :)
И к чему это вообще? Вы просто привели пример неправильной архитектуры.

Весь смысл проектирования — правильное распределение кода. Что-то должно быть расположено близко друг к другу, что-то — далеко. Внутри частей — связно. Между частями — несвязанно. Без одного не может быть другого.
Пример со светильниками неудачен. Проводку тестируют индикаторной отверткой, вполне себе аналог юнит-теста. И люстру вы тоже можете отдельно в магазине протестировать. Оба модуля протестированы, соединяем и делаем интеграционный тест. Все счастливы.
аналогично, MessagingService и NotificationsSerivce (в примере из статьи), можно тестировать отдельно, а для метода SendMessage зафигачить интеграционный тест. однако лирический герой ратует за то, чтобы непременно тестить освещение не зависимо от реализации конкретных компонентов, для чего наваял DI с конструктором и пачкой новых интерфейсов "… не обусловленных ни какой семантикой".
Не совсем. В вашем примере с проводкой все можно протестировать отдельно. Messaging service отдельно не протестируешь. Так что ваш пример неудачен.
Нет, он упрощает код и удешевляет рефакторинг, потому что теперь вместо одного толстого уровня можно независимо модифицировать и рефакторить два тонких, которые знают и делают принципиально меньше.

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

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

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

Не единственный. Еще как минимум Service Locator и фабрики.

Еще как улучшили, так как увеличили ее гибкость.

Гибкость не обязательно является достоинством.

Я предполагаю, что Вы просто не разделяете понятия «хорошая архитектура» и «простая архитектура».

Как раз напротив — потому что простоту архитектуры определить достаточно легко, а вот «хорошесть» — намного сложнее.

Да и нет в сложности нет ничего плохого, если она контролируема.

Как раз контроль сложности — это самое ресурсоемкое занятие программиста. Поэтому лично я стараюсь сложности избегать.
Содержание и толщина слоев изменилась очень сильно. Хотя бы потому, что если слой занимается инстанцированием зависимостей, это плюс одна ответственность на каждую зависимость. Эта ответственность может быть очень непростой — например, нужно передать все необходимые параметры конструктору и обработать ошибки. И теперь Ваш код контролирует время жизни зависимости, она без него не существует, а он — без нее. Где же здесь упрощение?

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

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

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

Где же здесь упрощение?

Смотря с чем сравнивать. С вариантом, где введено три дополнительных интерфейса — ощутимо меньше кода и степеней свободы.

Сервис локатор и фабрику тоже надо как-то передать, и обычно это все равно DI, так что сути не меняет.

Ээээ. Вы путаете. Сервис-локатор — это альтернатива DI, его не передают, он глобально доступен. Фабрики бывают разные, я имел в виду глобальные. Еще бывают статические фасады, контексты и много других интересных способов выстрелить себе в ногу.
Хотя бы потому, что если слой занимается инстанцированием зависимостей, это плюс одна ответственность на каждую зависимость. Эта ответственность может быть очень непростой — например, нужно передать все необходимые параметры конструктору и обработать ошибки. И теперь Ваш код контролирует время жизни зависимости, она без него не существует, а он — без нее. Где же здесь упрощение?

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

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

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


А что ещё? В комментариях на поверхности пока лишь плавает «уменьшить связанность ради уменьшения связанности».
И первое допущение, которым оперируют люди, пропагандирующие IoC — это то, что нам когда-нибудь понадобится заменить одну реализацию на другую. Так вот, это допущение верно для не такого уж большого количества проектов

Если используем модульное тестирование, то это допущение становится верным. Подмена реализации зависимостей на стаб/мок — один их главных методов модульного тестирования.
я просто хочу, чтобы мы не лукавили, говоря, что «помимо тестов, мы улучшили еще и архитектуру». Не улучшили. Просто сделали так, чтобы нам было удобно писать тесты.

Так улучшили. Сделали архитектуру лучше тестируемой. Если начали писать тесты, то тестируемость системы становится одним из требований к ней.
Вы не находите, что аргументация получилась несколько циклической: модульные тесты сделали архитектуру лучше, потому что она стала более пригодной для тестов?
Не нахожу. Появилось новое требование к системе: тестируемость (простота написания тестов, больше скорость их выполнения и т. п.). Архитектуру изменили что бы она лучше соответствовала этому требованию. Тесты ведь можно было написать и для первого варианта кода, но со вторым это делать проще
Эм. А откуда появилось это требование?

Речь-то в посте изначально шла о том, что мы добавили тесты — и архитектура стала лучше. Не просто тесты стало проще писать, а вообще архитектура лучше.

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

«Вообще» по-моему нет, есть, утрируя, сумма различных параметров. Архитектура стала лучше как минимум по двум из параметров (тестируемость, расширяемость), не став (допущение) хуже по другим.

Автор не очень удачно донёс эту мысль, наверное.
Плюс бесплатный бонус — простота расширения/изменения по некоторым направлениям.

Как минимум этот бонус не бесплатен, как максимум — это не бонус.

Архитектура стала лучше как минимум по двум из параметров (тестируемость, расширяемость)

По одному (тестируемость). Для расширяемости там нужно еще работать.

не став (допущение) хуже по другим.

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

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

Бонус бесплатен — целью изменения архитектуры было облегчении тестируемости, а не расширяемости.

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

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

Вынесение зависимостей — не усложняет. А вот введение дополнительных абстракций (а именно оно показано в примере к статье) — усложняет.

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

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

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

Из практики, IoC контейнер нужен в основном для легкого пробрасывание зависимостей. Если классу нужна новая зависимость, то достаточно просто добавить ее интерфейс(или даже реализацию) в конструктор требуемого. И всё, ничего не надо переписывать, не надо протаскивать требуемый сервис по всей цепочке классов в n-мест, только для того чтобы использовать в создании требуемого.

А дмнамическая xml-конфигурация или статическая конфигурация(изменяемая) — да, баловство, очень редко требуется именно изменять. Здесь согласен. Ну и облегчение тестирования — некое следствие, да. Во главу угла ее, вероятно, ставить не стоит, иначе получится программа из 1000 классов в каждом из которых один метод из трех строк :) Хотя TDD именно как способ проектирования еще Макконел советовал, что-то в этом есть. Да и пропогандируемый дядей Бобом «extract Till you drop» брр.
Из практики, IoC контейнер нужен в основном для легкого пробрасывание зависимостей.

Вы рассказываете, зачем нужен инструмент, реализующий практику. А я ставил под сомнение необходимость практики.
Я всего лишь констатирую, что «ваша практика» не равна «всеобщей практике» тем более в «нынешнем понимании».
Пока вам удалось констатировать только тривиальный факт, что DI-контейнер полезен, когда нужно реализовать IoC. С этим я не спорил.
Вы либо специально пытаетесь «передергивать», либо у нас не совпадает терминология.

DI CONTAINERS are also known as Inversion of Control (IoC) Containers or (more rarely) Lightweight Containers.
Mark Seeman

1) Вы выше написали, что IoC «в нынешнем применении»(с) необходим разве что для модульного тестирования.

2) Я вам написал, что ваша практика не равна всеобщей, приведя пример из своей, когда IoC применяется в основном не для тестирования, а для сборки графа объектов.

3) Вы пытаетесь бравировать схожими терминами IoC/DI, зачем-то вставляя слово «тривиальный».

Так что я склонаюсь к мнению, что вы специально «передергиваете», и дискуссия неконструктивна.
Я вам написал, что ваша практика не равна всеобщей, приведя пример из своей, когда IoC применяется в основном не для тестирования, а для сборки графа объектов.

Вот здесь ваша ошибка. Для сборки графа объектов применяется контейнер (вы называете его IoC-контейнером, правильнее говорить DI-контейнер, как, собственно, и написано у Симана).

А IoC (inversion of control, без слова «контейнер») — это методология, при которой каждый объект использует не конкретные зависимости, а их абстракции (например, интерфейс вместо конкретного класса).

Я не спорю с тем, что DI-контейнер — полезная вещь. Но я последнее время стал задумываться о том, где границы полезного применения IoC как методологии.
Я знаю про глобальный «inversion of control», столько же глобальный «dependency inversion principle», который не тоже самое что «dependency injection» и т.д.

Однако: «IoC в нынешнем применении»(с) = DI/IoC контейнер в 99% случаев.

Отсюда и уверенное утверждение, что не только для тестов.

Однако: «IoC в нынешнем применении»(с) = DI/IoC контейнер в 99% случаев.

Для меня — и, что важнее, в контексте моего комментария — это не так.
Ваша неправда. использование DI/IoC контейнера — отдельно, принципы DI и IoC — отдельно. Тот факт, что кто-то ввиду недопонимания их отождествляет, не должно учитываться. Здесь г-н lair прав.
Еще раз:
1. Есть «принцип IoC».
2. Есть «IoC контейнер» как общепринятое применение, использование, реализация принципов IoC в виде множества фреймворков. Если взять только .Net, это будут: Unity, Autofaq, NInject, StructureMap, Castle Windsor и т.д. еще n-цать штук.

Я правда не понимаю, как здесь можно спорить и писать что «общепринятый опыт использования IoC другой и не связан с контейнерами».
То что лично и конкретно у lair он другой — ок.
Я говорю о том, что:
1) Dependency Inversion / IoC можно делать и без контейнера, собирая зависимости руками.
2) при помощи контейнера решаем задачу Dependency Injection, т.е. автоматически собираем зависимости и не более того

Т.е. использование контейнера совершенно не значит, что архитектура использует принципы обращения зависимостей. Для этого нужно предпринять отдельные шаги.
А IoC (inversion of control, без слова «контейнер») — это методология, при которой каждый объект использует не конкретные зависимости, а их абстракции (например, интерфейс вместо конкретного класса).

Отнюдь. Смысл IoC — это когда более конкретные сущности зависят от более общих, но не наоборот. И уже это расшифровывается в общепринятое определение, в котором действительно есть тот признак, который Вы упомянули.
[Под впечатлением от образовавшегося холивара на тему добавления абстракций ради тестируемости кода]
… и вот именно поэтому мне так нравятся интеграционные тесты.

Код не усложняется, тесты есть, при рефакторингах в 90% случаев тесты не меняются, и именно тогда они нужнее всего (попробуйте при тестировании «в изоляции» поменять логику взаимодействия/интерфейсы двух классов).
У интеграционных есть ряд минусов:
1) Долгое время исполнения
2) Плохое покрытие редких случаев
3) Плохая обратная связь для разработчика — чтобы понять, что произошло, нужно читать логи.

А так да. По простоте написания/поддержки и code coverage интеграционные тесты жгут :)
Не очень силён в С#, но есть ли реальная необходимость заводить именно интерфейсы (или абстрактные классы)? Почему в сигнатуре конструктора не передавать конкретные типы?
Можно. Но тогда придётся пометить методы этих типов как virtual
Это ухудшает архитектуру?
Нисколько. Объект с виртуальными функциями это тоже в своем роде абстракция.
Зачем тогда вводить новые сущности, а не изменять свойства существующих (если это изменение остальным требованиям не противоречит)?
Выбирать из вариантов приятнее. Основная разница в том, что констуктор не может быть виртуальным. И вы его не переопределите.
Скажем так, это добавляет в нее открытости, которая может быть как к добру, так и ко злу.
Мне кажется этот вариант устроил бы вас и вашего оппонента как компромиссный. С одной стороны вы используете DI, с другой — не вводите ненужные абстракции
Мне не нравится слово оппонент. Я рассматриваю дискуссию как способ улучшить топик, так он как раз пытается его улучшить. Жаль, конечно, что его желание самоутвердиться немного мешает процессу, но я отношусь к этому философски.
Проблема в том, что это так не работает. Либо класс обладает минимальной достаточной расширяемостью (что может означать sealed), и тогда его не переопределишь для фейка, либо же в нем остается слишком много точек расширения, чтобы его можно было сломать.
У каждого решения есть свои trade-offs. Черт с ней с тестируемостью, но ваше решение не очень хорошее с т.з. читаемости — нельзя сразу понять какие у класса внешние зависимости. Решение с интерфейсами мне нравится еще меньше — вводить интерфейсы «чтоб было» противоречит YAGNI.
Лучшим решением здесь я считаю внедрением конкретных классов. Выделять интерфейсы/абстрактные классы не следует до того, как появится доменная необходимость в них (создание mock/stub не входит в это понятие).
Единственный минус здесь — невозможно определить класс как sealed и необходимость пометки методов как virtual.

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

А зачем? Вот опять-таки, если мы не говорим о модульности (нет требований) и не говорим о тестируемости — то зачем нам видеть внешние зависимости?

невозможно определить класс как sealed и необходимость пометки методов как virtual.

Вот это как раз большое зло.

обязанность разработчика подкласса писать его так, чтобы он не нарушал LSP

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

Если это сервисный класс (например, контроллер в ASP.NET MVC), то наверное и правда незачем. Если доменный, то чем четче вы очертите в коде его ответственности и зависимости, тем лучше для поддержки такого кода, т.к доменный слой — это ядро системы. В-общем, думаю вопрос действительно спорный и неоднозначный, много зависит от привычек конкретных разработчиков. Каких-то явный преимуществ кроме некоторого субъективного повышения читаемости я назвать сейчас не могу.

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

Вы в любом случае не сможете оградить вашу систему от того, чтобы разработчики дописали ее так, чтобы все сломалось. Эти вещи не решаются техническими средствами — только общением.
Вот именно, что мы уперлись в субъективные привычки (которые у меня, собственно говоря, такие же). Просто полезно иногда задумываться, а откуда же они — привычки — растут, и не надо ли их менять.
Спасибо всем поучаствовавшим в обсуждении. Я на хабре неделю и мне очень неприятно, что мой второй по счету топик скатился в холивар. Но нет худа без добра, благодаря живому обсуждению у топика появилось заключение.

Поиск идеальной архитектуры — бесполезное занятие.
Юнит тесты — это отличная проверка вашей архитектуры на низкую связанность между модулями. Но всегда ли это необходимо конкретному приложению? На этот вопрос правильного ответа нет. Проектирование сложных технических систем — это всегда поиск компромисса. Идеальной архитектуры не бывает, так как учесть все возможные сценарии развития приложения при проектировании невозможно. Качество архитектуры зависит от множества параметров, часто друг друга взаимоисключающих. Есть старая шутка, что любую проблему дизайна можно решить путём введения дополнительного уровня абстракции, кроме проблемы слишком большого количества уровней абстракций. Поэтому не стоит рассматривать как догму, что взаимодействие между объектами должно быть построено только на основе интерфейсов, главное чтобы выбор, совершенный вами, был осознанным и вы понимали, что код, допускающий взаимодействие между реализациями, становится менее гибким и, как следствие, теряет возможность быть протестированным модульными тестами.
Всё же не теряет возможность быть протестированным, а просто тестирование усложняется. Например, вместо простой передачи фэйковых зависимостей нужно будет мудрить с другими определениями классов, динамической их генерацией, отражениями и прочими низкоуровневыми хаками (в зависимости от платформы и/или языка). Использование продвинутых тест-фреймворков может низкуровневые хаки спрятать от разработчика, но они всё равно могут усложнять тестирование, например, из-за низкого быстродействия, что демотивирует прогонять тесты при каждом изменении.
Полностью согласен. Я, кстати, когда писал топик боялся, что он скатится в холивар-сравнение легковесных Isolation Framework c тяжеловесами, способными подменять не виртуальные и статические методы. Но вон как оно вышло. Никогда не угадаешь заранее.
Холивар, конечно, гораздо интереснее самого поста вышел:)
Это же сколько человеко-часов погребено под этими комментариями?..
Sign up to leave a comment.

Articles