Как стать автором
Обновить

Комментарии 141

Спасибо за перевод названия первого антипаттерна, я давно так не смеялся )
По поводу ServiceLocator-a согласен с автором, что в домене его не должно быть и не понимаю в чем тут может быть дисскусия. В остальных слоях — пожалуйста (но там где нельзя использовать DI).
Достаточно смело назвать анти-паттерном то, что много уважаемых в IT-сфере людей, включая того же Фаулера, считают паттерном, не находите?
Я не называл его анти-паттерном, прочтите ещё раз мой комментарий.
PS: цитата из Фаулера:
With dependency injector you can just look at the injection mechanism, such as the constructor, and see the dependencies. With the service locator you have to search the source code for calls to the locator.

Брр… просматривать весь код на предмет — какие же всё-таки зависимости используются, когда можно просто раз глянуть на конструктор — по-моему, выводы очевидны.
Вы не называете, Марк Симанн же называет.
Вы спросили в чем может быть дискуссия, я вроде бы ответил. :)
Плюс действительно бывают случаи, когда без SL в бизнес-логике не обойтись.

lair ниже все правильно написал.
Заинтересовало… Может привести конкретный пример того, когда без SL в бизнес-логике не обойтись? До этого мне казалось что DI охватывает практически все варианты внедрения зависимостей.
Например, мне довелось использовать ORM-фреймворк, который не позволяет переопределять конструкторы вообще никак. Единственным вариантом здесь является сервис-локатор, который удалось привязать к Сессии и дергать через extension-метод получение сервисов. Получилось эдакое псевдо-property-injectrion, где локальная пропертя возвращает внешнюю зависимость, полученную через сервис-локатор.
Ну кстати да, чтобы далеко не ходить — Entity Framework (как минимум до пятой версии, на пятую мы еще не апгрейдились). Правда, там есть обходной путь — событие «on entity created», на котором можно сделать property injection. Но нифига не удобно и не аккуратно.
Согласен с автом статьи — более того считаю, что синглтон и сервис локатор никакие не антипаттерны и никогда ими не станут — просто в пик их популярности нужно поумерить пыл тех, кто ими злоупотребляет.
У них есть свои области применения, которые пока не имеют четкой теории.

А по поводу цитаты Фаулера и Вашего комментария — это чисто технический вопрос. Вообще не понимаю при чем тут концепция.
Здается мне он был сильно не прав, когда это писал.
Во первых совершенно не обязательно, что риски появления необходимости менять интерфейсы из сервис локатора сколько нибудь значительны.
Во вторых используя развитые инструменты (например VS+Resharper) вы точно также получите в один клик все зависимости списком и можете их скопом изменить.

Если у вас таковых инструментов не имеется — то так и говорите, что из-за таких то технологических проблем использовать сервис локатор пока не можем, а не то, что он в принципе плох или концепция неверна.
Во вторых используя развитые инструменты (например VS+Resharper) вы точно также получите в один клик все зависимости списком и можете их скопом изменить.

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

Я чего то не понял, зачем Вам получать все зависимости класса, использующего сервис локатор.

Если мы говорим о необходимости найти все зависимости, переданные через сервис локатор, то кликаете правой кнопкой на инстансе сервис локатора (вероятно синглтоне) и нажимаете find usages.

Если нужно точки спользования конкретного сервиса через локатор — то тоже самое над методом CreateInstance локатора (или как он у Вас там называется).

Там группировака сразу будет по местам использования.

Я что то неправильно понял?
Я чего то не понял, зачем Вам получать все зависимости класса, использующего сервис локатор.

Затем, что это типичное действие для любого класса в системе: посмотреть, от кого он зависит, чтобы его можно было или выделить в отдельную подсистему, или протестировать, или еще что. Именно об этом пишет Фаулер: «With dependency injector you can just look at the injection mechanism, such as the constructor, and see the dependencies»

Я что то неправильно понял?

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

В чем проблема?

Меня все таки не покидает ощущение — что я вас правильно понял. Или я опять ошибся?
Вы знаете, что все его зависимости выражены в виде вызовов сервис локаторов.

Откуда я это знаю?

Начинаете поиск по инстансу сервис локатора

Как мне получить инстанс сервис локатора под курсор, учитывая, что я стою в определении класса?

дальше в результате поиска сморите вызовы для своего класса

В проекте с парой сотен классов найти глазами нужный, не помня, где он находится?

В чем проблема?

В количестве кликов, которые для этого нужны. Или вы-таки можете мне сказать, как выполнить описанную выше последовательность действий в один клик (напоминаю ваше утверждение: «используя развитые инструменты (например VS+Resharper) вы точно также получите в один клик все зависимости списком и можете их скопом изменить.»)?
Нет — в один клик нельзя — тут я ошибся. Можно в несколько кликов.

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

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

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

Становится. Наличие подобной проблемы — это объективный минус конкретного паттерна, который нужно учитывать при его внедрении.

Хотя, конечно же, дело не в том, что в IDE не видно, а в том, что зависимости кода не очевидны при взгляде на его public surface. Это как скрытые побочные эффекты.
Симан тоже достаточно уважаемый в IT-сфере человек. При этом Фаулер писал достаточно давно, а некоторые вещи в отрасли, все-таки меняются. Синглтон тоже описали «много уважаемых людей», но это не мешает ему потихоньку переходить в категорию анти-паттернов.

Симан честно предупреждает в книге, что его точка зрения спорна:

Calling SERVICE LOCATOR an anti-pattern is controversial. Some people consider it a proper design pattern, whereas others (me included) consider it an anti-pattern. In this book, I’ve decided to describe it as an antipattern
because I think its disadvantages are greater than its advantages; but don’t be surprised if you see it endorsed in other places. The important thing is to understand the benefits and shortcomings enough to be able to make an informed decision for yourself.


И его же статья по этому поводу: blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
«Control Freak». Руководитель-наркоман.

Руководитель-наркоман — это в Битриксе, а тут скорее Диктатор.
В статье про зависимости без наркомана никуда.
Или в статье про инъекции :).
Чем так уж плох второй метод, если все мои зависимости лежат в одной сборке, и я хочу использовать DI паттерн, но не хочу использовать DI-контейнер? кстати такой код я видел не только во фреймворке, но и а каких -то крайне популярных околодотнетских книжках.
Если вы хотите использовать DI без контейнера, то правильно использовать «Poor Man DI», когда зависимости явно вбрасываются вызывающим в момент создания объекта. А Bastard DI плох тем, что связность сохраняется, так что не очень понятно, зачем (кроме тестирования) это делать.
Если зависимости гарантированно в одной сборке (или еще и internal class), то вместо Constructor Injection хорошо подойдет Property Injection. Т.е. создаются local defaults варианты, а при необходимости подкидываем замену через properties.
О, видимо, я нарываюсь на холивар. Да, наверное, именитый автор и книга, но что-то здесь не нравится.

А как на счет антипаттерна «избегание зависимостей»? Зависимости просто могут быть исходя из логики. Нельзя их избегать в разрыве от задачи.
Первый пример, который «уменьшает зависимость». Но теперь любой код, создающий экземпляр, должен знать почему-то, что внутри этого класса используется ProductRepository repository. И передавать его в конструктор. Просто взять и подумать, сколько раз будут повторения у создающего кода?.. Да, создание внутри класса чего-то внешнего через new — плохо. Два выхода из ситуации:
1. Делать так, как у вас в правильном примере, но создавать объект через фабричный метод и запретить его создавать иначе. В фабричном методе вы ньюкаете репозиторий и передаете конструктору. Один раз будет такой код, внешний код не будет знать.
2. Немного хуже. Репозиторий создается только через фабричный метод и хранится в классе интерфейс, который в конструкторе через фабричный метод создается. Хуже такой способ в плане тестирования. Придется через отражение устанавливать фабрику-мок, а не передавать через конструктор.

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

Зависимости просто могут быть исходя из логики.

Покажите мне хотя бы один пример, где оправдана зависимость от реализации?

Но теперь любой код, создающий экземпляр, должен знать почему-то, что внутри этого класса используется ProductRepository repository. И передавать его в конструктор. Просто взять и подумать, сколько раз будут повторения у создающего кода?..

Вот именно поэтому и используются DI-контейнеры (а все зависимости описываются в composition root). Надо ведь понимать, что «код, создающий экземпляр» — это одно единственное место, поскольку DI применяется планомерно до самого верха.

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

Вот это плохо:
public ProductService(ProductRepository repository)

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

Интерфейсы, хоть и не являются реализацией, тоже чем-то говорят. Говорят о смысле, зачем они используются.

Будет немного лучше так:
public ProductService(IProductRepository repository)

Но тогда любой внешний код при создании ProductService, должен знать то, что должно быть скрыто, а именно: что класс ProductService использует IProductRepository. С чего вдруг? Это кишки сервиса. И что он кушает, нам не важно, важно что он для нас делает. И банально, если этот сервис часто создается, кругом будет дублирование кода.
Вот чтобы этого избежать, надо не только репозиторий передавать как интерфейс, но и внешний код не должен знать о классе ProductService, а использовать интерфейс IProductService.

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

class ProductServiceFactory
{
    class ProductService: IProductService
    .......
    public static IProductService Create()
    {
         IProductRepository repository = ProductRepositoryFactory.Create();
         return new ProductService(repository);
    }
}


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

А вообще, это больше религиозно. Оно вроде и красиво, но накладно каждый раз писать фабрики. Шарп не предрасполагает — приходится писать дополнительный код.

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

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

Для меня «паттерны» — это больше ругательство. Смотреть надо на код, на смысл, а не на красоту и количество паттернов. Хотите пример, недавно нашел в коде? Код — конечный автомат. Принимает сообщения и обрабатывает. Правильно ли он сделан, не обсуждаю, но вот метод, принимающий сообщения, приблизительно такой:

public void ProcessMessage(Message message)
{
    if (handledMessageFuncs.Contain(message.Type) && IsValid(message))
    {
          hadledMessageFuncs[Message.Type](message.Parameters);
    }
}


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

Я очень издалека объясняю, конечно, но суть в том, что на код надо смотреть локально. Т.е. видите, чувствуете лажу, значит скорее всего так оно и есть. И паттерны как раз могут увеличивать лажу. Важно, чтобы код был понятен «локально». А если вы разбрасываете логику, пытаясь разделить зависимости просто ради разделения — то это может быть и плохо.
Ну это… Ваш ProductServiceFactory — и есть сильно упрощенный DI-контейнер. Только добавьте в него еще возможность определять через внешний конфиг или предварительную настройку того, какой конкретно тип создается для IProductService и IProductRepository и будет в принципе минимальный набор функциональности.

UPD Про интерфейсы согласен, в примере из статьи косяк.
Только DI-контейнер не надо писать каждый раз, его надо только сконфигурировать, а так он как правило уже есть в библиотеке ;)
Вот это плохо: public ProductService(ProductRepository repository) Потому что класс ProductService получает ссылку на конкретный класс, т.е. на конкретную реализацию.

Это плохо только в том случае, если ProductRepository — это конкретная реализация.

Но тогда любой внешний код при создании ProductService, должен знать то, что должно быть скрыто, а именно: что класс ProductService использует IProductRepository. [...] И банально, если этот сервис часто создается, кругом будет дублирование кода.

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

С чего вдруг? Это кишки сервиса.

Нет. Сервис не должен знать, какой конкретно реализацией IProductRepository он пользуется.

нужно приблизительно так:

class ProductServiceFactory
{
    class ProductService: IProductService
    .......
    public static IProductService Create()
    {
         IProductRepository repository = ProductRepositoryFactory.Create();
         return new ProductService(repository);
    }
}




Вы заменили зависимости от сервисов на зависимости от фабрик — тем самым, не убрав конкретные зависимости, но добавив лишний уровень абстракции. Лучше не стало.

Оно вроде и красиво, но накладно каждый раз писать фабрики.

Вот именно поэтому и используются DI-контейнеры.

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

А не надо нарушать SRP — и все будет хорошо. Не зря эти принципы идут вместе.

(и заметьте, ни слова о паттернах, которые вам так не нравятся)
В общем почти всё верно, я как раз о том и писал, что в статье явно не указано то, что вы расписали.

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

Я когда-то работал с фанатом настроек (до меня проект писали). Ситуация часто была такой (через день):
— Опять не работает система! Эти сисадмины тормоза! Они снова не поняли, как надо правильно настраивать и поставили некорректные настройки!

Т.е. надеюсь, вы поняли. Погоня за гибкостью — плохая идея. Код должен быть жестким. Когда я даю переменной тип int, а закрепляю логику, что там целое число. т.е. я мог бы написать и double и даже object, и была бы неимоверная гибкость. Но тем не менее, я считаю лучшим способом написать хардкод — int, потому что это соответствует смыслу того, что я хочу написать и я хочу ограничить программу, используя статическую типизацию.
Кроме последней идеи — «не убрал конкретных зависимостей». К счастью не убрал. Кой какие зависимости быть обязаны, потому что кода не бывает ради кода, как фильмов ради режиссеров. Код должен описывать смысл. И зависимости — не что иное, как статическая типизация. Иначе получится холодец, а не код. Очень гибкий, настраиваемый, но любая настройка делает его ненадежным.

А при чем тут настройки? Связывать зависимости можно и в коде, просто не надо это делать в фабрике (потому что слишком много кода). Про composition root я уже писал.

У меня сейчас несколько, наверное, уже десятков проектов со сквозным DI, и ни в одном из них для зависимостей не используется конфигурационный файл.
Сергей, поделись, пожалуйста — как инстанциировать wcf-сервис через di-контейнер не пихая конкретную настройку unity в кастомный бехейвиор и где его настраивать кроме конфигурационного файла?
Создать фабрику и инициализировать контейнер там. См Unity WCF.
Видел.

Плодить по фабрике на каждый сервис не сильно менее «грязно», чем плодить бехейвиоры, имхо.

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


Кэп ;)

А то, что фабрика своя на каждый проект — так это нормально, это composition root так выражен.


Да, понял. Все равно, интуитивно чем-то напрягает идея composition root внутри именно фабрики. Хочется вынести его куда-нибудь в более «семантически правильное» место типа global.asax.
Внутренности фабрики — вполне себе семантически правильное место, согласно тому же Симану.
Как только у меня появляется что-нибудь с ответственностью «фабрика» у меня тут же появляется желание сконфигурировать зависимость и от нее. И тут мы внезапно втыкаемся, ага, опять в конфиг или svc. Поэтому не вижу сильной разницы между вариантом с конфигурированием зависимости от фабрики или конфигурированием сразу контейнера в конфиге.
Зависимость от фабрики ты конфигурируешь в svc. А вот фабрику целиком — внутри нее.

Это не идеально, но достаточно прозрачно.
А я как сказал?
На всякий случай уточню, что согласно тому же Симану, класть DI-контейнер внутрь фабрики можно только в том случае, если фабрика объявлена в том же месте, где находится Composition Root, иначе будет как раз Service Locator anti-pattern.
В случае с WCF factory оно так и есть (фабрика и есть composition root). По крайней мере, в тех реализациях, которые мне удалось найти.
Почему много кода в фабриках? Давайте так, прямо из вики:

Service service = (Service)DependencyManager.get(«CarBuilderService»)

1. По всей видимости эта штука возвращает object. Это значит, что бага проявится только в рантайме, если вызовут этот код. А это ухудшает надежность. Этот код может и через год завалить систему. И компилятор тут не причем.
2. Имя сервиса в стринге — то же самое — не статическая типизация.

Допустим у нас есть код:

public interface IAClass
{
    string Foo();
}

public class A:IAClass
{
    public string Foo()
    {
        return "In instance A";
    }
}

public interface IBClass
{
    string Foo();
}

public class B : IBClass
{
    public string Foo()
    {
        return "In instance B";
    }
}



Сейчас код, на фабрики-женерик (только что набросал, воспринимать только для фана!):

public class GenericFactory<T>
{
    sealed class ConstructorCreator
    {
        static ConstructorCreator()
        {
            ConstructorCreator<IAClass>.BindType<A>(); // Здесь по одной строке мы объявляем, какой тип соответствует интерфейсу
            ConstructorCreator<IBClass>.BindType<B>();
        }

        public static Func<Ti> GetDefaultConstructor<Ti>()
        {
            return ConstructorCreator<Ti>.defaultConstructor;
        }
    }
    sealed class ConstructorCreator<Tc>
    {
        public static Func<Tc> defaultConstructor;
        public static Func<Tc> DefaultConstructor
        {
            get { return ConstructorCreator.GetDefaultConstructor<Tc>(); }
        }

        public static void BindType<TInheritor>() where TInheritor : Tc
        {
            var costructor = typeof(TInheritor).GetConstructor(new Type[] { });

            defaultConstructor = Expression.Lambda<Func<Tc>>(LambdaExpression.New(costructor)).Compile();
        }
    }

    public static T CreateInstance()
    {
        if (ConstructorCreator<T>.DefaultConstructor == null)
        {
            throw new NullReferenceException(string.Format("GenericFactory is not initialized for type {0}", typeof(T).FullName));
        }
        return ConstructorCreator<T>.DefaultConstructor();
    }
}  


Как видите, возможно женерик-фабрику соорудить. Тут, для примера, только для дефолтовых конструкторов. Для недефолтовых либо не подойдет женерик фабрика (разве что с tuple-ами каким-то, что есть плохо). Либо если какие-то коннекшины, привязывать можно здесь.
Привязка типа к интерфейсу идет только по одной строке. Так что кода все равно не больше.

Плюс такого подхода — статическая типизация. Не создадите что-то плохое. И даже, для примера, здесь компилируется конструктор, что значительно может ускорить работу, если объектов генерить надо много.

Пример использования:

Console.WriteLine(GenericFactory<IAClass>.CreateInstance().Foo());
Console.WriteLine(GenericFactory<IBClass>.CreateInstance().Foo());

Как и следует ожидать, выводит:
In instance A
In instance B

И где много кода? Класс пишется довольно быстро. Нигде ни на шаг от шарпа не отошли. Я за то, чтобы пользовать шарп! (Хотя в случаях ORM за честный мапинг в XML)
Что-то посмотрел примеры по ссылкам, где статическая типизация? ))

Да, конечно, я не подарил миру новый паттерн. Конечно, не один я такое делал (не подсматривал, честно). Просто настолько очевидно, как сделать женерик фабрику, что и думать не надо много.

Еще раз повторю — в этом моем коде практически безопасные объявления. Потокобезопасные, типобезопасные.
два узких места: создание конструктора (его может по умолчанию не быть или он закрыт) и вызов статического конструктора с объявлением. Он вызывается раз. При первом вызове фабрики. Но где-то ухудшается типизация, он не обязательно сразу вызовется сначала.
Но всё же, стрингов нет, ХМЛ нет. Не ощущаете разницу? (да да да, не один я такой умный, всё это конечно, есть, но вы рекламируете как раз более худший вариант, когда я вам показываю более безопасный и при этом настолько простой и очевидный, что не нужны библиотеки. Для синглтонов вы не не пользуете библиотеки?)
Давайте так, прямо из вики:
Service service = (Service)DependencyManager.get(«CarBuilderService»)

1. По всей видимости эта штука возвращает object. Это значит, что бага проявится только в рантайме, если вызовут этот код. А это ухудшает надежность. Этот код может и через год завалить систему. И компилятор тут не причем.
2. Имя сервиса в стринге — то же самое — не статическая типизация.


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

var service = ServiceLocator.Get<IService>();


Для DI это вообще не важно, потому что в DI все происходит через типизированные аргументы и свойства.

И где много кода?

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

Инициализация:
var container = new UnityContainer();
container.RegisterType<IAClass, A>();
container.RegisterType<IBClass, B>();

var locator = new UnityServiceLocator(container);
ServiceLocator.SetLocatorProvider(() => locator);


Использование:
Console.WriteLine(ServiceLocator.Current.GetInstance<IAClass>().Foo());


Кода меньше, та же самая функциональность, что и у вас, достигнута, но при этом еще есть:
  • недефолтные конструкторы (привет DI)
  • поддержка синглтонов (обратили внимание на GetInstance вместо CreateInstance?) и вообще управление временем жизни
  • прочие приятные мелочи
Да, принято. Тут я вижу привязки в коде.

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

Вообще, мы отходим от темы. Я не критикую вообще сам паттерн. Я критикую попытки его везде использовать, особенно с конфигами. Это выходит за пределы добра и зла (за пределы кода и компилятора). А если не конфиги, то этот паттерн ничем или очень мало чем отличается от фабрик. Примеры выше. Т.е. даже называть его dependency injection как-то язык не поворачивается.
А если не конфиги, то этот паттерн ничем или очень мало чем отличается от фабрик. Примеры выше. Т.е. даже называть его dependency injection как-то язык не поворачивается.

Вы не грокнули Dependency Injection.

То, что показано выше — это Service Locator. А Dependency Injection — это когда ни один (есть оговорки, но для простоты будем считать так) класс не создает (и не получает по своей инициативе) ни одного экземпляра других классов (за исключением тривиальных).

Т.е. не может быть никакого ServiceLocator.Current.GetInstance<IA>, может быть только параметр в конструкторе с типом IA (вбрасывание в конструктор), или свойство с типом IA (вбрасывание в свойство), или параметр в методе с типом IA (вбрасывание в метод). И никак иначе. Вот это — Dependency Injection, со всеми его достоинствами и недостатками.
И на счет размеров кода. Там привязка идет по одной строке на один тип. Так что кода не больше, чем в XML. Если говорить о коде самой женерик-фабрики, то да, издержки профессии. Но код небольшой. Также и с синглтоном — если его (женерик) писать, размер где-то такой и будет размерами. Но в сумму код женерик фабрики не надо включать. Любой проект имеет тысячи строк кода. а этот женерик использовать можно для разных типов — пишется один раз.
Зачем писать код, когда есть готовый, который делает все то же самое, только лучше?
Незачем. Но смотря какой код. Какие размеры и какая сложность. Этот код сложный? Нет. Он настолько прост, что не стоит утруждать себя чем-то.

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

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

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

А не важно.

Этот код сложный? Нет.

Достаточно сложный, чтобы вы допустили в нем ошибку.

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

Достаточно не конфигурировать unity из файла (и вообще не допускать неокрепшие умы в composition root).

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

Зато гибкий. Т.е. вы сказали, что больше кода в фабриках. Если не использовать женерики, то фабрики надо делать на каждый тип. Но вот, код тогда будет отличаться проверкой компилятором. Я как ни думал сделать женерик-фабрику, чтобы могла всё проверить компилятором — так и не смог.

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

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

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

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

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

Это утверждение никак не доказано.

Оно конечно, не делает гибкой архитектуру (а зачем???).

Дело не в гибкости архитектуры. Первое применение IoC — это тестирование. Попробуйте сделать тестовую подмену в ваших реализациях — узнаете много интересного.

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

Какой «этот паттерн»?

Вы перечитайте комментарий выше, который про Service Locator и Dependency Injection, он полезный.
Это утверждение никак не доказано.

Просто без отражения компилятор будет проверять правильность кода. Разве что, к сожалению, он нулы не отслеживает. Если в фабрике напишете return null; то получите проблему рантайма. Но такой код надо писать сильно нетрезвым.

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

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

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

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

Здесь это тоже ищется в несколько кликов. Обычно — в один.

А заодно и фабрику.

И как вы будете в вашей фабрике обрабатывать тестовые ситуации?

Я успешно в приватные свойства и поля хожу.

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

Понимаете ли, спор — он не «фабрика или контейнер». Он «инвертировать контроль или нет». А по первому пункту спорить бесполезно, контейнеры доказали свое практическое удобство.
Вам уже явно показали, что DI тоже работает со статической типизацией.


Не совсем. В нашем шарпе нельзя четко сказать, статическая или нестатическая. Т.е. язык сам по себе со статической типизацией. Но это не значит, что любой код будет равносильно легко проверяться компилятором. Например, если вы будете передавать ссылки, приведенные к базовым. Приведение к базовому классу не нарушает ничего, а вот приведение назад, наследнику — прощай компилятор. В частности — передавать обжекты — в языке типизация статическая, а в коде появляются ob is int и т.д. Проверки на нал — ложатся на плечи программиста. Компилятор помочь не может. Что это значит? Что в лучшем случае сможете проверить сами и бросить эксепшин в рантайме. Такие проверки — ненадежные.

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

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

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

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

Можно. Тип каждой переменной известен в момент компиляции.

Если есть фабрика, то ест и метод, который возвращает экземпляр.

Внутри этого метода можно написать что угодно. Прощай проверки.

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

Какого паттерна? О каком именно паттерне вы говорите, конкретное его название?

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


Да ну?
А такой код, на шарпе:

void Foo(object someData)
{
     if (someData is TextBox)
     {
          ((TextBox)someData).Text = "This is TextBox";
     }
     else if (someData is ProductService)
     {
         ((ProductService)someData).SendAnyText("Hello, ProductService");
     }
}


И что? Статическая или динамическая? Да, компилятор молодец, он знает, что обжект. Но достаточно ли этого, чтобы он на этапе компиляции обнаружил ошибку? Нет. Так что не надо относится к коду шарпа так легко — сама поддержка статической типизации не делает из компилятора маму, которая всё сделает.

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

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

Тип каждой переменной известен в момент компиляции. Поэтому типизация в C# статическая.

Так что не надо относится к коду шарпа так легко

А при чем тут легко-не легко? Вопрос был «можно ли легко сказать, какая типизация с C#?». Можно. Читайте Скита, C# in Depth.
???
Компилятор в данном случае не знает реального типа данных во время компиляции. Так и называется «динамическая типизация». По смыслу.
Нет, ну если вы к словам придираетесь и логики не понимаете того, что я пишу и как ухудшается надежность программы, то в общем, спорить не о чем.
Компилятор в данном случае не знает реального типа данных во время компиляции.

А я что-то говорил про тип данных? Речь идет о типе переменной. Именно это и называется «статической типизацией».

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

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

А знание о том, что тип не меняется, никак не важно.

object o;


int i = (int)o;

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

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

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

И вот эту проверку можно столькими способами в шарпе куда подальше послать.

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

Нет. Просто что я понимаю под этим, вроде многократно здесь описал. А тема сама по себе скользкая и как бы нет единого мнения. Да и смысл спорить об общественном договоре.

Но давайте посмотрим не на русскую википедию, а на их. У нас могут часто чушь писать.
http://en.wikipedia.org/wiki/Type_system
Раздел Static typing
Обратите внимание на размытую формулировку, в отличии от нашей. Статическая типизация — проверка типов во время компиляции. И всё. И это не спроста.
Есть языки, вроде С, С++, Pascal и т.д. У них есть понятие переменной. По сути, там переменная — это имя, за которым скрывается указатель на кусок памяти, плюс, к переменной прикрепляется некоторый тип — он говорит, какой кусок памяти откусывает переменная и как эту память интерпретировать. Поэтому, говорить, что проверка типов — это проверка типов названий переменных — верно, потому что там вообще нечего больше проверять.
Есть языки, в которых всё наоборот. Там имя не имеет тип. Тип имеет какая-то сущность — данные (если их так можно называть) и функции. Они могут и сами по себе без имен существовать, а имя — всего лишь псевдоним, через который можно к ним обращаться. Даже присваивания нет, а просто именование. Там определение статической типизации из русской википедии не работает. Не смотря на то, что понятие общеязыковое, вроде как.

C# — это вещь не туда и не сюда. В шарпе и имена имеют тип и данные тоже. Т.е. экземпляр в памяти имеет тип и с ним нельзя работать иначе.
Так что вполне корректное замечание у меня, что на момент компиляции, компилятор не знает реальный тип данных, хранящихся в переменной. Поэтому там так обтекаемо и написали «The C# type system performs static-like compile-time type checking».

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

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

Можно.

Поэтому там так обтекаемо и написали «The C# type system performs static-like compile-time type checking».

Не поэтому, а потому, что он проверяет данные еще и в рантайме.

А вообще, непонятно, почему мы спорим.

Потому что вы не грокнули Dependency Injection и продолжаете утверждать, что фабрики имеют большую компайл-тайм устойчивость.
Какого паттерна? О каком именно паттерне вы говорите, конкретное его название?


Прошу прощения. Думал, контекста статьи достаточно. Мы говорим о паттерне Dependency Injection. Или о Unity. Ну или я так назвал «паттерном».
Для паттерна Dependency Injection утверждение «Все эти проверки компилятора в случае паттерна из статьи — уходят в рантайм.» неверно. При применении этого паттерна любой код, использующий какие-либо зависимости, использует совершенно обычные типизированные аргументы. Вот типичный пример:

class Dependent
{
    public Dependent(IDependency dependency)
    {
        ....
    }
}


Единственное, что в этом коде не проверяется компилятором — это вариант, при котором dependency == null (как и в случае с вашей фабрикой). От таких вещей ставят guard condition и забывают про них, как про страшный сон.
Dependency Injection скрывает реализации от нас, а также от компилятора, что явный минус.

Почему минус? Зачем компилятору знать реализацию? Чем ему это мешает? Не понимаю. Где-то в другом месте проекта есть класс-реализация моего интерфейса, компилятор его найдёт и скомпилирует. Зачем ему что-то ещё? А если реализации нет, то не скомпилируется код, где происходит инициализация контейнера.

И еще скрывает от нас в плане читаемости кода. Была бы фабрика, я бы быстро нашел в коде, что за тип используется (ну бывает, надо ходить по коду и смотреть, как он устроен)

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

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

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

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

Где-то так
Ну то есть вы против IoC целиком, как принципа?
Вопросом на вопрос.
Вы хотите перейти на личности в споре или я все-таки не достаточно объяснил свою т.з.? )

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

Я в принципе против «преждевременной гибкости». Это не столько и не только к IoC относится. Я в принципе использую противоположный подход:
Код строится от более ограниченного состояния к менее ограниченному (гибкому). Жесткость кода без боя с заказчиком я не сдаю.

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

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

Code Contracts со включенным static checking. Всё.

(ну и еще ручное создание всех объектов, но вы же этого хотели все равно?)

Я в принципе против «преждевременной гибкости». Это не столько и не только к IoC относится.

IoC — это не «преждевременная гибкость», это «гибкость по месту». Кстати, именно поэтому можно зависеть от конкретного класса, а не от абстракции — если этот конкретный класс является единственной возможной реализацией. Но вот экземпляр этого класса нам должны передать снаружи, чтобы мы не отвечали за его создание (и, как следствие, за его зависимости, которые могут уже быть не настолько однозначными).

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

«Все, что крякает — утка» — это duck typing, к интерфейсам отношения не имеет. А IoC — это один из пунктов SOLID (под псевдонимом Dependency Inversion Principle), и он применим к любым объектно-ориентированным языкам (и в языках со статической типизацией он работает лучше, кстати).
На счет, против ли я вообще, то я разве где-то отказывался от фабрик? Нет. А это один из вариантов. Я лично, отказываюсь их писать сразу, потому что кода становится больше. И отказываюсь пользоваться движками, сокращающими написание кода, потому что там неявно прикрепляется реализация, что ошибки уносит в рантайм («Code Contracts со включенным static checking» — не знаю, что это такое, явно, не шарп).
Отказываюсь также писать фабрики сразу, потому что их не сложно сделать по требованию. Нет необходимости их писать сразу. Негибко? (бинго, я к этому стремлюсь, пусть и компилятор поработает).

IoC — это не «преждевременная гибкость», это «гибкость по месту».

Если вам кажется такая гибкость сразу — нормально, то мне «слишком». Потом, когда будут основания, она вполне может появиться.

«Все, что крякает — утка» — это duck typing, к интерфейсам отношения не имеет.

Конечно, формально не имеет. Да сколько ж мы будем в словах путаться. А суть? Определить что есть кряканье, а потом в рантайме цеплять тех, кто крякает, ну очень мало по смыслу отличается от «просто в рантайме определить кто имеет подходящее поведение».
Цепляясь за формальные названия, вы видимо и мысли не уловили про статическую типизацию. И видимо думаете, что если шарп называется статическим языком, то точка — можно творить что хочешь, на типизацию это не влияет.

В общем, экспериментируйте, а я на такие настраиваемые движки перейду не скоро. (Интерфейс не кодом, а чем-то от реализации отделить… Дык, может вообще не писать на шарпе, а уйти в Хаскель… Чтобы вообще не писать реализации ))
На счет, против ли я вообще, то я разве где-то отказывался от фабрик? Нет.

Окей, именно поэтому я и спросил, отказываетесь ли вы от IoC целиком. Если не отказываетесь от фабрик — значит, не отказываетесь; и, значит, как минимум на нуллрефы вы готовы.

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

Не пользуйтесь. Для IoC это не нужно.

(«Code Contracts со включенным static checking» — не знаю, что это такое, явно, не шарп)

А вы погуглите, это полезно.

Если вам кажется такая гибкость сразу — нормально, то мне «слишком»

Какая «такая»?

Определить что есть кряканье, а потом в рантайме цеплять тех, кто крякает, ну очень мало по смыслу отличается от «просто в рантайме определить кто имеет подходящее поведение».

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

В общем, экспериментируйте, а я на такие настраиваемые движки перейду не скоро.

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

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

Ладно, не буду снова о плохом
Вы уж определитесь, вам фабрики (и нуллрефы) или без нуллрефов (но тогда только plain new).

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

Ничего несбыточного, Code Contracts и прочие статические анализаторы вам в помощь.
я когда и фабрики пишу, нулрефы не использую. Благо есть return из любого места.

Уже был когда-то мне такой вопрос. Не понимаю, почему вдруг решили, что надо держать промежуточную ссылку, пока не создадут класс. Из метода, который возвращает интерфейс, можно в десятках мест написать return new… Причем для разных типов, реализующих интерфейс.

где еще нужны нулы? В полях неинициализированного класса? Тоже, можно инициализировать по месту.

нулы и фабрики никак не связаны. Только особенности шарпа.
я когда и фабрики пишу, нулрефы не использую. Благо есть return из любого места

Вы можете это проверить на этапе компиляции? Нет, не можете. Но используете тем не менее.

Вот и с IoC ровно то же самое — тот факт, что вам не дадут null вместо зависимости гарантируется только доброй волей программиста.

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

нуллреф — это NullReferenceException. Ну, я так привык, по крайней мере.
Вы можете это проверить на этапе компиляции? Нет, не можете. Но используете тем не менее.


Фабрики и null никак не связаны. Null — это неотъемлемая проблема классов в C#. Т.к. такое поведение у классов заложено и убрать его невозможно (нельзя в C# указать ссылку, которая гарантированно не может быть null), то конечно, на этапе компиляции компилятор не проверит. С чего бы ему проверять? Нет таких средств для создания типов, чтобы компилятору это указать.

Фабрики не причем. Если бы была возможность создавать такие типы, то и фабрики без проблем бы можно было бы делать, которые никогда не создавали бы null и не было бы гарантированно NullRefferenceException.

Так что я не понял вашу аналогию: раз на фабрики согласен, значит и на нулреф. (про NullRefferenceException понятно. Просто было бы возможно компилятору указать NotNull тип для класса, женерик, то не было бы и возможности для эксепшина)
Это не аналогия, это дословный факт: если вы пишете код с фабриками, значит, вы (поскольку вы не пользуетесь статическими анализаторами кода) соглашаетесь с тем, что ошибка разработчика фабрики может привести вас к NullReferenceException в рантайме (ну или к срабатыванию guard clause, если он у вас есть, толку-то).

Собственно, это призвано продемонстрировать тот факт, что код с фабриками ничем не более верифицируем на этапе компиляции, нежели код, написанный по правилам IoC (а отсутствие статической верификации — ваша единственная сохранившаяся претензия к IoC).
А, вы про это. Фабрика здесь тоже не причем, потому как null можно оставить и без фабрик легко. Не инициализировать поле класса, например. Без никаких фабрик и интерфейсов.

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

Выше писал. Я бы не стал говорить, что это одинаково. Да, возможность такая есть. Но где это были в моей памяти такие фабрики с методами, которые требовали держать пустую ссылку? Не помню. Всегда return new…

А чтобы писать return null; — надо быть очень пьяным или очень ненавидеть контору, для которой пишешь код.
Да, к сожалению вероятность бага остается. Мусорный код заглушки какой-то, например. Но все же не думаю, что можно говорить об одинаковой вероятности этих багов. Фабрика, когда пишется для интерфейса — она одна. На нее выводит студия, когда ищешь интерфейс или класс. На конфиг ничего не выводит. Там скорее всего списком пишутся типы. Пропустить что-то проще. Точно так же в женерик-фабрике. Создали новый интерфейс, создали класс, но не зарегистрировали. Каким образом это обнаружить?
Фабрика, когда пишется для интерфейса — она одна. На нее выводит студия, когда ищешь интерфейс или класс. На конфиг ничего не выводит. Там скорее всего списком пишутся типы. Пропустить что-то проще. Точно так же в женерик-фабрике. Создали новый интерфейс, создали класс, но не зарегистрировали. Каким образом это обнаружить?

Вообще — юнит-тестом. Это если прямо отвечать на ваш вопрос.

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

(плюс, как верно заметили ниже, ошибка «забыли сконфигурировать зависимость» — не важно, где и как — вылетает при первом же функциональном тесте, и в продакшн ей уйти не суждено)
Мне кажется вы чутка перегибаете. Пускай падает в рантайме, если что. Это будет отловлено ещё на этапе разработки. И падать оно будет только если автор был пьян.
А дело тут всё же не в гибкости, ИМХО. Это экономит кучу времени. В т.ч. и при юнит\интеграционном тестировании, помимо разработки.
Т.е. о том, что пить надо, мы все таки договорились )))

Может быть. Принимается. Ваш опыт про экономию времени — ваш. Я еще не осознал, где же такая экономия получается.
Да, пить надо — это отлично расширяет сознание и помогает в написании кода :-)

А остальное — приложится. Мир!
Мне кажется вам все-таки надо вчитаться внимательнее. Вы в целом все говорите правильно, кроме одного — вы собрали самопальный велосипед, показываете его нам, а мы вам показываем ровным счетом тоже самое, только стандартизированное, «заводской сборки» и с большим функционалом. Не надо писать ваш женерик, он уже написан ;)
Так главная мысль m36 не в том что нужно свои велосипеды писать, а в том, что DI очень не надежен и может вызывать массу проблем при восприятии заложенной логики и написании кода.
В чем именно ненадежность DI? (особенно в сравнении с предложенной m36 фабрикой) Какие конкретно проблемы возникают при восприятии кода (если, конечно, при его написании изначально соблюдались правила проектирования)?
не хочу снова встревать в спор, но как бы начал представлять общую картину того, о чем вы говорите. Тут большая каша в споре. То с Dependency injection перескакиваем на IoC вообще, то на фабрики.

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

Давайте тогда и другую т.з. на тестирование рассмотрим.

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

Кстати, еще одна мысль, почему плохо заранее отвязываться от реализаций. То, что реализации — это плохо — это известно всем. Но не плохо изначально само по себе. Если вы будете пользоваться только декларациями, то у вас возникают дополнительные возможности налажать. Есть, например, неявные связи между реализациями. Скажем, вам надо использовать IDataReader к какой-то вымышленной IDataBase. При этом прикрепили реализацию провайдера к SQL-Server, а датаридер к MySql. Как бы в этом случае нужно было бы пользоваться абстрактной фабрикой, а не фабричными методами. Но это надо — на уровне кода хорошо, когда вы сами это контролируете. А если вы выворачиваете в одну плоскость все классы, делая видимыми для тестирования кишки конструкторов, а потом настраиваете где-то связи типов — налажать в прикреплении нужной реализации вероятность в разы возрастает. И при этом, есть мнение, что читаемость кода не улучшается. Опыта нет, не видел кода, где кругом через интерфейсы создается всё невидимыми фабриками.
Т.е. то, что говорят, что мол реализация нам не важна и это вредно — не совсем верно. Код надо создавать так, чтобы реализации были согласованными. Для клиента класса все равно, что там за интерфейсом. Но сама реализация важна.

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

2. Есть отражение. В общем нет никаких проблем в доступе к закрытыми членам. И есть еще один фокус, про который может быть не все знают. Обратите внимание на такую конструкцию:
System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(T))
Эта штука сделана, очевидно, для сериализации. Она создает чистый обнуленный кусок памяти и возвращает его как экземпляр объекта. Т.е. не вызывает конструктор объекта. Замечательно упрощает тестирование. Все сложности зависимостей при создании объектов уходят. Если надо протестировать какой-то метод, то мне не обязательно инициализировать громоздкий конструктор (у вас с моками в конструктор может быть даже хуже в плане экономии усилий).
Я просто создаю объект и выставляю поля и свойства, нужные только для его окружения и тестирую.
А чтобы не париться каждый раз — написал несложный женерик класс, который упрощает достут к закрытым членам. Тогда класс как на ладони. И тест локализируется только на тестируемом участке и его окружении. Конечно, есть минус, что приходится стрингами обращаться к закрытым членам. При рефакторинге имен, в тестах рефакторинг не происходит и они падают. Но это всё же не обойти, если есть желание тестить закрытые члены.

А кормить рыбу из удочки в целях тестирования… Я сторонник того, что код в первую очередь должен служить требованиям, а не тестированию. Возможно в С++, где нет отражения, люди успешно пользуются таким способом. Но я в шарпе не вижу причин.

Не привязывайтесь к конструкторам. Можно и через проперти инъекцию делать.

А еще я никак не пойму почему вы считаете, что если внутри класса есть ссылка на объект другого класса, то этот другой класс — кишки первого. Нифига, это вполне отдельные модули. И этот модули надо тестировать отдельно.
Тестировать надо всё. И отдельные модули тоже.

Я комментарием выше говорил об иньекции в конструктор. Ну или в проперти. Я только имею ввиду ответственность класса. Самый первый мой комментарий здесь об этом и говорил. Код, который с т.з. ответственности был логичен — класс внутри создавал то, что ему нужно, потому что это детали реализации класса, которые не должны волновать пользователя класса, вдруг перестал создавать, а ему начали толкать через конструктор. Теперь вызывающий класс с т.з. классического ООП, должен знать о реализации данного класса. Чтобы от этого избавиться и привести к нормальному виду, я предложил скрыть за фабрикой.

А далее начался на ровном месте спор, что мол фабрики писать не надо, что есть движки и т.д. и т.п.
На самом деле, с моей т.з. не надо и через конструктор передавать. Разделяем ответственность. И в шарпе нет никакого напряга в тестировании. Делаете поле не классом, а интерфейсом:
private IRepository repository = new Repository();

То используя фокус с неинициализированным объектом, вы получаете для тестов объект с null (конструктор не вызывался). Далее, делаете в тесте прямо нужную вам инъекцию — мок репозитория. И профит
Теперь вызывающий класс с т.з. классического ООП, должен знать о реализации данного класса

Нет, не должен. Вам об этом говорят с самого начала.

Демонстрирую:

interface IDbProvider {}
class DbProvider: IDbProvider {}

interface IRepository {}
class Repository: IRepository {
    public Repository(IDbProvider provider) {}
}

class Controller {}


Контроллер использует репозиторий, репозиторий использует провайдер. Провайдер — зависимость, репозиторий — зависимый компонент, контроллер — (в вашей терминологии) — вызывающий класс.

Вы думаете, что в контроллере пишут так?

public ActionResult SomeAction()
{
    // ....
     (new Repository(new Provider)).GetSomeData()
    // ....
}


Так вот, нет, нифига. Для контроллера репозиторий — это зависимость, а сам он — зависимый компонент, поэтому контроллер тоже получает (уже готовый и инициализированный) экземпляр репозитория:

public Controller(IRepository repository)
{
    _repository = repository;
}

public ActionResult SomeAction()
{
    // ....
    _repository.GetSomeData()
    // ....
}


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


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

Может, конечно, но не должен. Это симптом over-injection, что, в свою очередь, означает нарушение SRP. Так что это типичный code smell, который надо исправлять, а не затыкать.

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

Как вы определяете «что он может создать сам»? Почему вы считаете, что класс, использующий внешнюю зависимость, сам знает, как эта зависимость инициализируется?

И мы снова возвращаемся к тому, что вы (да, именно вы) смешиваете DI и IoC. Давайте уже раз и навсегда определимся, что именно вам не нравится:
— то, что класс не сам определяет свои зависимости,
— или то, каким именно способом он получает зависимости снаружи?

Скажем, вам надо использовать IDataReader к какой-то вымышленной IDataBase. При этом прикрепили реализацию провайдера к SQL-Server, а датаридер к MySql.

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

(более того, в BCL уже есть пример того, как реализуется именно описанная вами функциональность — DbProviderFactories, типичный специализированный сервис-локатор)

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

Вот именно потому, что у вас нет опыта (и вы не то ли не хотите, то ли не пытаетесь понять приводимые примеры), вы и улетаете все время в какие-то выдуманные вами ситуации, которых не бывает в реальности.

Код надо создавать так, чтобы реализации были согласованными.

Это нарушает design by contract и банальную инкапсуляцию.

Передача моков через конструктор не избавляет от проблемы — как тестировать закрытые методы.

А должно? Это как бы совершенно перпендикулярные проблемы.

Я сторонник тестирования и закрытых методов.

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

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

Про автомокеры вы тоже не слышали?

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

Прекрасно. Вот типичный для direct control метод:

public void TransferData()
{
    using(var cn = new SqlConnection(someConnectionString))
    {
        // здесь получаем из коннекшна команду, из нее ридер, из него - строки
        // для наглядности пропущено нафиг
        using(var c = new SomeWebServiceClient(someUri))
        {
            foreach(var row in rows)
            {
                c.PutData(row)
            }
        }            
    }
}


Как вы его будете тестировать вашим методом (естественно, мы говорим об изолированном тесте, БД и вебсервис вам недоступны)?

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

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

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

Я знаю об этом давнем споре, поэтому написал, что я сторонник.
А вообще, здесь ошибка. Юнит-тесты не пишутся на функциональные требования или требования заказчика. Точнее, далеко не только на них. Юнит-тесты более подробные. И пишутся, или могут писаться на любой чих. Т.е. на требования самого программиста. Ему никто не говорил, как надо что-то реализовывать, но в процессе кодирования он постоянно ставит себе мелкие требования и закрепляет их в тесте.

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

Юнит-тесты — серьезная штука для уменьшения вероятностей багов. И если ограничится только видимыми вещами, то вы скорее пропустите много кода мимо.
Отвечу пока только на часть, потому как с вами не поработаешь ))

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

Т.е. на требования самого программиста.

Требования самого программиста тоже могут предъявляться только к публичному контракту класса. Смотрите Design by Contract.

Тесты по хорошему должны тестировать каждую ветку процесса в коде.

Тесты должны тестировать каждую возможную ситуацию (или хотя бы группу эквивалентных ситуаций). Пытаться протестировать каждую ветку — это тестирование прозрачного ящика, вы ставите тест в зависимость от (возможно некорректной) реализации.
Требования самого программиста тоже могут предъявляться только к публичному контракту класса. Смотрите Design by Contract.

Может, а может и нет. Юнит-тесты — для всех, а не только для «контрактников». Если будут контрактники пытаться лучше покрыть тестами, возможно, количество контрактов начнет расти необоснованно.

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

Да. Но и противоречия никакого нет. Программист пишет юнит-тест на требования, исходящие от него самого. высший уровень — тестирует только поставленные функциоанальные требования — низший — каждую ветку. Каждую ветку — это и есть 100 процентное покрытие. Или вы думаете, если программист пишет «if», он не ставит никаких требований к этому ифу, просто потому, что в контракте это не описано? Как раз в ветках и прячутся баги.

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

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

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

Программист пишет юнит-тест на требования, исходящие от него самого.

А откуда берутся эти требования?

Или вы думаете, если программист пишет «if», он не ставит никаких требований к этому ифу, просто потому, что в контракте это не описано?

Должно быть наоборот. Сначала требования (выраженные в тесте), а потом — if.

Ваши контракты, это где-то между

Что такое «мои контракты»?

Накладно через контракты тестить каждую ветку.

Я не понимаю, что такое «тестить через контракты».

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

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

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

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

Сначала покрыть все случаи тестами, а потом код — не соответствует ТДД. Там пишут по одному случаю тест, а потом код. И каждый «иф» — это тоже случай. Код не только в контрактах помещается, он везде. И когда программист собирается сделать ветвление (допустим), это ничто иное, как какое-то требование. Он мыслит, скорее всего так перед кодированием:
«сейчас мне нужно, чтобы система меняла свое поведение в зависимости от такого условия». Это и есть требование к «иф». Иф — это не нечто ненужное и недостойное внимания. Это тоже мысль программиста. И он по ТДД пишет тест, сначала, на это требование. Может и через контракт, может и через только открытые методы. Но это затрудняет создавать предусловия.

Вообще, вы сейчас входите в противоречие, пытаясь навязать, что правильно писать через контракты и т.д. Это некорректно, т.к. юнит-тесты используются в любых техниках. И даже в любых языках, в которых понятие контракт не имеет смысла.
Юнит-тест — это то, что я написал — закрепляет на уровне утверждений поведение кода. Что ж теперь делать людям, которые не пользуются контрактами, не писать юнит-тесты?

А за то, что я не на все вопросы отвечаю — прошу прощения. Мне приятно с вами общаться. Но приходится и работать. Еще вдруг будущий работодатель увидит, чем я в рабочее время занимаюсь ))
Т.е. вы пытаетесь «дать сочетание данных» в контракт так, чтобы покрыть все случае

Что значит «дать данные в контракт»? Что такое по-вашему «контракт»?

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

Нет, это не требование к if, это требование к системе.

Но это затрудняет создавать предусловия.

Почему затрудняет, если это предусловие по определению внешнее по отношению к SUT?

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

Писать через контракты — правильно. Но юнит-тесты тут не при чем (их просто проще писать в условиях явно оформленных контрактов).

И даже в любых языках, в которых понятие контракт не имеет смысла.

Это какие языки, интересно?

Юнит-тест — это то, что я написал — закрепляет на уровне утверждений поведение кода.

Что такое «утверждение» в вашем понимании?
Нет смысла продолжать беседу, особенно в таком тоне. Задавать вопросы «на дурака». Если цепляетесь к словам, можно водить хороводы по кругу до скончания времен.

Что значит «дать данные в контракт»? Что такое по-вашему «контракт»?

Наверное вот это:
http://ru.wikipedia.org/wiki/Контрактное_программирование
А именно: «точные и верифицируемые спецификации интерфейсов для компонентов системы»

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

Нет, это не требование к if, это требование к системе.

А собака — это не животное с хвостом и рычалкой. Собака — это dog.

Любые требования к системе волшебным образом отражаются в коде. Мало того, придирки ваши понятны. Дело в том, что вы плохо относитесь к реализациям. Когда как реализации тоже должны становиться языком описания требований. И «иф» в том числе. А не отказываться от них всяческими способами. Разница между мной и вами (на мой взгляд), только в том, что я считаю, что код всплошную должен быть понятным, включая ифы и форы. Что тело метода — это не только название и сигнатура. Что каждый кусочек должен отражать смысл (и как можно меньше использовать хитрости).
Нет ненужных частей кода.
Почему затрудняет, если это предусловие по определению внешнее по отношению к SUT?

На очевидные вопросы отвечать не буду. Тем более, выше несколько раз написал. Да и пропускал некоторые ваши вопросы иногда не только по причине занятости, а потому что они очевидные и звучат как придирки. Мне не хочется, чтобы общение скатилось до взаимных обвинений. Либо ищем в общении рациональное зерно, либо нет. Стиль Луговского — это смешно и я тоже для фана иногда резко высказываюсь, но всё же, это шутки и никого оскорблять не хотел никогда. Кстати, и здесь, почему-то вы воспринимаете на свой счет изначально, хотя я сразу во втором комментарии описал, что я имею ввиду и что совершенно не собирался что-то критиковать. Просто неточность в статье.
Дальнейший спор был бессмыслен. Хотя и интересен.

Писать через контракты — правильно. Но юнит-тесты тут не при чем (их просто проще писать в условиях явно оформленных контрактов).

Контракты подразумевают открытые члены, смотрим снова по ссылке. Я считаю, что только их тестить — неправильно. Уже написал почему.

Кстати, тоже не нравится сама идея контрактного программирования. Не потому, что это жестко (это как раз хорошо), а потому, что интуитивно человек считает одну часть очень важной — интерфейсы, а другую — плохой и не стоящей внимания. На самом деле баги в реализациях больше всего и прячутся. Есть разница, когда человек фор использует, а когда форич. Второй конструкцией человек явно указывает читателю кода, что ему порядок не имеет значения. Есть разница, вызвать у Enumerable Sum() или форич +=, первым способом человек указывает, что ему не важен алгоритм суммы (они бывают разные). Есть разница, создает интерфейс IList<>, или List<>. В первом случае говорит, что ему важен порядок элементов, но не важна реализация.

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

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

Это какие языки, интересно?

Декларативные. Т.е. если весь код состоит только из деклараций, то странно описывать параллельно к этому коду декларации в виде контрактов. В коде прямо они и описываются. Кстати, все эти моды «правильного проектирования» и проистекают из недостатков шарпа — императивности. Но это не значит, что императивные части не несут смысловой нагрузки и их, например, тестировать не имеет смысла.

Что такое «утверждение» в вашем понимании?

Можно снова не будут отвечать на вопрос?

Прекрасно. Вот типичный для direct control метод:

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

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

Это не неточность а статье, это Java-style именование, где префикс I перед интерфейсом не принят. Данная мода в последнее время расползается и на .Net (привет Роберту Мартину и его CleanCode). :)
Либо Марк имел в виду абстрактный класс, но уж точно не конкретную реализацию.
Если цепляетесь к словам, можно водить хороводы по кругу до скончания времен.

Я не цепляюсь к словам, я уточняю, что именно вы имеете в виду под конкретным словом. Потому что вы регулярно имеет в виду совершенно не то же самое, что я. «Статическая типизация» тому примером.

А именно: «точные и верифицируемые спецификации интерфейсов для компонентов системы»

Прекрасно. Как можно туда «дать данные»?

Тем более, выше несколько раз написал

Процитируйте, если вас не затруднит. Мне это найти не удалось.

Кстати, и здесь, почему-то вы воспринимаете на свой счет изначально

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

Кстати, тоже не нравится сама идея контрактного программирования.
… вот, пожалуйста, пример для моего утверждения выше.

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

Какой человек так считает?

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

Каким образом из этой идеи вытекает, что реализация — плохая и нестоящая внимания?

Есть разница, когда человек фор использует, а когда форич. Второй конструкцией человек явно указывает читателю кода, что ему порядок не имеет значения. Есть разница, вызвать у Enumerable Sum() или форич +=, первым способом человек указывает, что ему не важен алгоритм суммы (они бывают разные). Есть разница, создает интерфейс IList<>, или List<>. В первом случае говорит, что ему важен порядок элементов, но не важна реализация.

Это все — читабельность кода внутри метода. Это все прекрасно, но это все не имеет никакого (да, ровным счетом никакого) значения для потребителя этого кода. Для потребителя достаточно того, что код делает обещанное.

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

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

Поэтому локальная ясность реализации и сокрытие этой же реализации — это ортогональные техники.

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

Я надеюсь, вы понимаете, что это затрудняет тестирование.

Декларативные. Т.е. если весь код состоит только из деклараций, то странно описывать параллельно к этому коду декларации в виде контрактов. В коде прямо они и описываются.

Это не значит, что в этих языках нет понятия контракта, это (напротив) значит, что контракт в них встроен.

Главная ценность юнит-тестов — отлавливание багов до комита. И они отлавливаются, когда внимательно ветки протестировать.

Я уже объяснял, в чем проблема «тестирования веток» (в противовес «тестированию требований»). Если вкратце — это приводит к ситуации, когда все ветки протестированы, а требования не реализованы тем не менее.
Значит мы говорим совершенно на разных языках. У нас разные представления. Причем кардинально разные. Точнее я не могу объяснять. На уровне аналогий и интуиции много раз объяснил.

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

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

Д: Я хочу, чтобы когда мы «включаем» работу с пришедшими данными, у меня отображалось время первого действия. С любым из элементов этой группы.
Я: Странное требование. Вам может быть надо, чтобы отображалось время включения в работу элементов, а не именно время воздействия на первый случайный элемент? (конкретики вам не объясняю, надеюсь, суть будет понятна) Потому что это время сохраняется наряду и с другими и так в истории. Почему первого? Это время зависит от многих, независящих от элементов факторов.
Д: Нет, мне надо именно время первого действия. Дело в том, что со времени включения группы в работу может пройти какое-то время и иногда недопустимо большое. Мне надо это узнавать.
Я: Ага. Т.е. вам не время на самом деле первого действия надо, а вы боитесь, что от включения группы до первого действия проходит большое время. Т.е. вам нужно мониторить интервал времени (возможно открытый), от включения до первого действия.
Д: Нет, мне нужно именно время действия!
Я: Так вы же сами сказали, что может пройти недопустимый промежуток времени. Откуда вы знаете, когда включили группу? Может все таки промежуток? Тогда мы эти штуки сможем даже в отчеты выводить, мониторить автоматически
Д: Нет, мне нужно только время действия! Потому что я смогу посмотреть время включения в другом ГУИ интерфейсе, если надо. Или я помню. (включает в работу группы не она. Но надеется, что будет всегда знать, когда ответственный включит, на другом этаже в другом отделе)

Короче, номер не прошел. Мозг мне вынесли. И я, верите, не могу никак точнее объяснить. Начинаю говорить что-то пространное о том, что данные должны быть как можно более алгебраическими, поддающимися вычислениям. Что я по сути точно перевел ее требования на язык системы и т.д.
С ее точки зрения, она хочет только это, а язык, «алгебраическое» и все такое — просто непонятная фигня.

Вам я тоже попытаюсь, а может уже не вам, еще раз аналогией, где-то интуицией объяснить, что код с интерфейсами, это почти что код с фекалиями. Т.е. интерфейсы не всегда хорошо. Что код надо строить жестко, а гибкость получать по надобности. При этом, я совсем не против интерфейсов и фабрик и паттернов выше, как таковых. При этом много раз объяснял это. Разница только в том, что я в другой последовательности кодю. Сначала без них (если это не нарушает смысл, как с IList<> например), потом когда надо — с ними. В конце получается то же самое. Только нижняя граница гибкости, а не верхняя.

У вас стойкое свое понятие красоты кода.

Объясню еще раз с другой стороны.

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

Теперь думаем не о глобальних или статических переменных, а о членах класса. Удивительно, но это то же самое. Хотя и не поддается критике. (Это я просто вводную, пока темы это не касается).
Теперь, сервис-локатор. Первый кандидат на сайд эффект. Если вы тестируете метод, а в методе создаете некий интерфейс через сервис-локатор, а не передавая через параметры или не создавая явно класс с помощью new, то работа этого интерфейса уже создает сайд-эффект. Это ухудшает тестирование. Не улучшает. По сути, вы можете протестировать код метода, но при этом в рантайме сервис-локатор породит другой класс и ваш код будет вести себя иначе, чем в тестах. Вы сейчас, наверное, вынос мозга ощущаете от того, что я пишу бред, потому что интерфейс четко фиксирует сигнатуру (а реализация нас не интересует). Но это не так. Разные реализации будут порождать разное поведение. А значит, никогда нет гарантии, что код в рантайме не попадет в неожиданно-непротестированные места.
Да, для тестирования иногда надо разорвать связь и подставить мок. Но именно тогда, когда вы хотите точно рамки теста определить. А не во всех случаях.
Где-то та же логика касается и передачи интерфейсов через параметры. По сути, это не сайд-эффекты, но наглядность в тестировании нет. Моки нужны только там, где они имеют смысл для теста. В других местах лучше, чтобы система вела себя одинаково и определенно.

Почему надо тестировать закрытые методы Потому что код везде, и в закрытых методах тоже. И любой иф — это выражение некоторого смысла. Код, как черный ящик, могут и иногда обязаны рассматривать тестировщики. Но не программисты. Если вы думаете, что надо только выразить четкие спецификации на открытые члены (или на какой-то контракт), то вы пытаетесь не пользоваться языком программирования как языком. Если вы в шарпе начнете с обсуждений спецификаций до каждого эксепшина до кодирования на какой-то контракт (прошу вас, не цепляться снова к словам, потому как неконструктивно это. Мы люди интеллигентные, недеюсь, у нас работа, а не «базар»), то по сути, это некая реинкарнация водопада. Вы хотите очертить всё до кодирования. Тогда как любая строчка несет смысл, это не какая-то ерунда в черном ящике.

По ТДД, программист при кодировании всегда ставит требования. На любые изменения в коде. Если вы перестанете думать о коде, как о конструкциях и реализациях, а как о языке, выражении мыслей, то это становится очевидным: изменения в коде и требования — это одно и то же. Т.е. если программист представил себе условие, при котором система меняет поведение, то это равнозначно тому, что он сказал в коде «иф» (если иф). Так получилось, что сам способ кодирования заставляет нас писать некоторые сложные нелинейные функции. Если бы они были простые, то не нужно было бы в код смотреть, достаточно было бы генерировать автоматически переменные на вход, а потом автоматически проверять выход.

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

А уже отсюда следует, что лучше тестировать и открытые и закрытые члены. Потому как в таком случае тесты точно не уступают тестированию только открытых членов. Это плюс еще дополнительные тесты.
Потом, когда вы пишете класс, вы играете роль разработчика класса, представляя роль пользователя класса: закрывая от него всё, что ему не нужно. При этом, нет никакого противоречия в том, что вы пишете тесты на закрытые члены — вы тестируете свой код. Вы сейчас в роли разработчика класса, а не пользователя. Это значит, что код от вас не скрыт. Значит — любой метод в классе — содержит ваши требования к методу.

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

На самом деле, можно протестировать почти всегда (при наличии моков и убирании зависимостей) код класса через открытые члены. Потому что если вы его вынули в тарелку )), то работа класса не зависит больше ни от чего, только от вас. И получаем два случая, две проблемы:
1. Вы хотите рассматривать класс как черный ящик. Просто полагая, как он приблизительно должен себя вести. Тогда вы пропускаете много кода, не покрываете его тестами. Не по всем веткам тесты прошли.
2. Вы хотите как можно полнее протестировать класс и смотрите на ветки. Получаете проктологические проблемы (лечение глаз через задницу). Чтобы смоделировать доступ до какой-то ветки, иногда придется очень и очень долго выставлять нужное состояние класса. Иногда это нереально сложно.

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

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

Вот казалось бы, мы с вами оба принадлежим к «точной» профессии, под которой даже есть, по слухам, математическая база… так почему же вы объясняете на уровне аналогий и интуиции?

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

Это не объяснение, это голые эмоции.

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

У вас каша какая-то. Давайте поделим ее на две половины, для начала.

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

Половина вторая — объект, реализующий некоторый интерфейс. Так вот, работа этого объекта вызовет или не вызовет сайд-эффект вне зависимости от того, как был порожден этот объект — через сервис-локатор, или напрямую.

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

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

И это снова не зависит от того, как была порождена зависимость — через DI, через SL, или напрямую; от метода порождения зависит только то, как нам придется попрыгать, чтобы подменить зависимость на контролируемую нами.

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

Весь смысл мока в том, чтобы система вела себя одинаково и определенно — так, как определено в моке.

Вообще, я вам искренне рекомендую прочитать книгу Мезароса xUnit Test Patterns, мешанины в терминологии станет поменьше.

Просуммирую все вышесказанное снова: до тех пор, пока мы говорим о юнит-тестировании (а я не видел, чтобы вы где-то переходили к функциональному или интеграциионному тестированию), тестированию подлежит исключительно один «элемент» кода (отсюда и название Unit testing). Когда мы говорим о c#, таким «элементом» обычно считается класс (потому что оперировать отдельными методами достаточно сложно; при этом тест пишется на метод, но зависимости от других методов не изолируются). Все остальное нужно изолировать и полностью контролировать, чтобы результаты тестов были предсказуемыми.

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

[пропущено много воды]
Значит — любой метод в классе — содержит ваши требования к методу.

Нет. Если бы это было так, код бы не нуждался в тестах, поскольку если я уже выразил требования в коде, то все реализовано. И нельзя восстановить требования по коду, если правильность кода неизвестна.

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

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

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

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

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

class FastMath
{
    public double Sqrt(double val)
    {
        if (val < 0)
            return 0;
        return SqrtImpl(val);
    }

    public double StupidMath(double val)
    {
        if (val < 0)
            return 0;
        var sqrt = SqrtImpl(val);
        if (sqrt < 0)
            return 0;
        return SqrtImpl(sqrt);
    }
        
    private double SqrtImpl(double val)
    {
        if (val < 0)
            return 0;
        // адская математика
    }
}


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

Ок, мы написали все тесты, мы прогнали их, мы видим, что строчка с отрицательным аргументом в SqrtImpl не покрыта. Равно как и не покрыта строчка if (sqrt < 0) return 0;. И обе их невозможно покрыть, пользуясь публичным интерфейсом класса.

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

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

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

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

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

Я не ухожу. Просто это темы настолько глобальные, что могут коснуться даже психологии, философии, теории информации, физики и еще чего-то. Я пытаюсь быть наиболее понятным, выражать мысли наиболее простым языком. Ухожу от вопросов, ответы на которые мне кажутся риторическими и если за них зацепиться, у нас общение перейдет русло либо оскорблений, либо выяснений очень мелких деталей. Определять кто правильнее говорит названия — не очень конструктивно. Названий правильных вообще не бывает ;) На хабре не только мы, о таких вещах можно в личке писать. И банально, времени не хватает. Мне интересно с вами (и со всеми здесь) общаться. Но форумы очень много съедают времени. Думаю, как бы хоть на время покинуть эти чтения и общения. Очень интересные проекты у меня. Как на работе, так и дома.
За пропущенные специально или не специально вопросы прошу прощения. И за будущие грехи тоже. Так получается. Это не попытка показать неуважение.
Ох. Почитайте МакКоннела. Эти «умные техники» направлены на то, чтобы уменьшить сложность каждого локального куска кода (и сделать его более понятным). Они направлены на то, чтобы вам не было нужно читать код зависимостей.

Э… Как-то хвастаться мне, что читал Макконела, причем давно уже — не солидно. Хороший мужик. Но. Эти умные техники направлены не только на это. Примеры. Снова напомню первое мое правило. Код должен выражать смысл. А еще вести себя определенно и жестко.
Если мне нужен список, то я пишу:
IList<int> bindingList = new List<int>();


Заметьте, я четко в коде декларирую, зачем мне нужна переменная. Переменная мне нужна только для собирания списка и для доступа по индексу. Когда мне надо просто некоторая совокупность чего-то, без указания порядка (чаще всего так и надо), то я пишу:
IEnumerable<int> bindingList = new List<int>();</source">

Т.е. читать мне код там где-то не надо. Я декларирую это в типах переменных. Выражаю, так сказать минимализм -  суть того, зачем они мне нужны. Зачем в этом случае ходить в код конструктора и смотреть на реализацию? Но при этом везде сохранилась связь для того, чтобы код вел себя определенным неизменяемым способом. Сохранилась жесткость. Я уменьшаю по возможности неконтролируемые этим участком кода сайд-эффекты. Сайд-эффектом в данном случае я называю то, что при одинаковых параметрах метод возвратит разные результаты. Что не так? Потому что это другой метод другого объекта? А кого это волнует, это ж должно быть скрыто.
Ловко вы себя обманываете, закрепляя в тесте одни и те же моки и в границах теста не получая сайд-эффектов. Только интересно, вы за баги болеете? )
Но как только мне вдруг завтра понадобится гибкое поведение, легко заменяю хоть сервис-локатором, хоть через конструктор готовый давать. 
<blockquote>Я надеюсь, вы понимаете, что это затрудняет тестирование.</blockquote>
В шарпе нисколько не затрудняет, писал об этом фокусе:
<a href="http://habrahabr.ru/post/166287/#comment_5753359">m36</a>
<blockquote>Это все — читабельность кода внутри метода. Это все прекрасно, но это все не имеет никакого (да, ровным счетом никакого) значения для потребителя этого кода. Для потребителя достаточно того, что код делает обещанное.</blockquote>
Вот, и я об этом. С чего ли в шарпах и джавах люди так ненавидят язык программирования и пользуются любой возможностью, чтобы код хуже (да, эмоции ;). Например, гибким. Страх перед изменениями?
Для потребителя важно то, что код делает обещанное. Только самые дорогие баги, по тому же Макконелу - это неучтенные требования. Код часто о них говорит. Потом, потребитель юнит-ттестов - вы. Для вас, как разработчика класса - все методы открыты.
<blockquote>Я уже объяснял, в чем проблема «тестирования веток» (в противовес «тестированию требований»). Если вкратце — это приводит к ситуации, когда все ветки протестированы, а требования не реализованы тем не менее.</blockquote>
Да, объясняли. И я тоже объяснял. Так может это вы меня не поняли, а не я вас. Попытаюсь послать читать Кента Бека (TDD) . Код - это требования. И если вы пишете сначала тест, ветки еще нет, но порождая требования, далее порождаете ветку в коде, то по сути, такой подход и говорит, что вы тестируете требования. <b>Нет кода. Нет кода никогда.</b> Если у вас такой говнокод, что к нему как к неизвестности относиться можно и по нему нельзя понять, что от него хотели - то тесты, даже тестирующие требования - это уже попытки воскресить мертвого.
Поэтому тестирование всех требований при хорошем коде и при своевременном написании тестов - равносильно тестированию веток. Противоречие вы видите, скорее потому, что представляете тестирование в разрыве от кодирования и когда код уже написан. И еще, скорее, другим человеком. Да, конечно, тогда зачем же закреплять в тестах написанные баги. Тестируют как черный ящик, на всякие неожиданности - тестировщики. А юнит-тесты как раз - прозрачный ящик. Вы пишете тест (свои требования, к новому куску кода, который сейчас родится), потом пишете тест. Получилось много веток - шаг и тест были слишком общими и требование тоже. Понаписывали тесты, порефакторили, посмотрели для обратной связи - сколько веток тесты не проходят? Это все минусы в 100-процентное покрытие. Хотя, конечно, оно не важно. Но обратная связь такая важна, чтобы хотя бы потенциальные проблемы определить. Действительно, на самые мелочи тесты не нужны (хотя скорее полезны, чем вредны). Когда вы пишете тест на ветку, вы не закрепляете ветку. Вы этого сделать не можете. Вы тестируете некоторый участок в области определения функции. Это требование. Не просил пользователь - значит упустили требование. Не просил и это ему не нужно - значит функция делает лишние вещи - потенциальный баг.

Ладно, я думаю, большей ясности мы в общении уже не добьемся. А вырастили уже длинную "ветку"
Сори за неотформатированный текст.
Если мне нужен список, то я пишу: IList<int> bindingList = new List<int>();

Я так подозреваю, что давать вам ссылку на Липперта, и его рассуждения про var и intention-revealing variable names бесполезно?

я пишу: IEnumerable<int> bindingList = new List<int>()

А вот этот код совершенно за пределами добра и зла. И с точки зрения именования, и с точки зрения избыточности. Если бы я такое увидел на code review, автор бы долго объяснялся, а код был бы исправлен.

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

Выражаю, так сказать минимализм — суть того, зачем они мне нужны.

Почему бы не использовать для этого имена переменных? Они лучше доступны, чем тип, и универсальнее.

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

… ознакомьтесь, все же, с терминологией. Это не сайд-эффект, это non-deterministic behaviour.

Попытаюсь послать читать Кента Бека (TDD)

Спасибо, но уже прочитано.

Код — это требования.

Нет.

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

Тоже нет. Тестирование требований — это совершенно отдельный процесс, вообще никак не связанный с разработкой TDD.

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

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

Простите, вы мои фразы про том, что тесты надо писать до кода, целенаправленно игнорируете, или случайно?
Я так подозреваю, что давать вам ссылку на Липперта, и его рассуждения про var и intention-revealing variable names бесполезно?

А можно мне ссылку? :)
Простите, вы мои фразы про том, что тесты надо писать до кода, целенаправленно игнорируете, или случайно?

Нет. Вы сами себе противоречите. Говорили одно, а выводы про ветки говорят о вас другое.

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

Какая у меня тупая логика?! Сам от себя ифы в приватных методах ставлю. Следуя вашей как раз логике, эти два ифа легко там и останутся. Потому что нет требования и они не влияют на ход программы. И даже ровным счетом ничего не значат. Следуя моей логике, я бы ифы те вообще не писал. А если просто посмотрел на на писанный код, то становится в лом писать лишние тесты, т.к. очевидно, что туда код не зайдет.
Следуя моей логике, я переименовываю метод SqrtImpl в SqrtOfPositive (где-то так) и со спокойной совестью убираю ифы. Потому что волшебным образом название отражает и требование и избавляет от лишнего теста. Макконел не все полезные вещи писал. Для шарписта «защитное программирование» вредно. Уж очень грамотные дотнет эксепшины порождает, что добавить к ним нечего. И не входит в неопределенное состояние при работе с сырой памятью.

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

Рассуждения про вар Липерта мне тоже бесполезны. В мире до хрена авторитетов, на любой вкус. Верить всем одновременно — это обманывать себя.
Если вар урощает понимание, то лады, а если нет — не лады. В данном случае я декларировал, зачем мне нужна переменная. Извините, у нас давно не С. У нас очень выразительная система типов. А тип, не что иное, как такой хитрый объект по смыслу. Поэтому это давно уже не сишные детали реализации — а чуть ли не детали описания смысла или предметной области. «Кин-дза-дза» смотрели? Вар == ку во многих случаях. Хотя это и не нарушает типизацию. С удовольствием использую иногда вар на самом деле. Не в данном месте.

… ознакомьтесь, все же, с терминологией. Это не сайд-эффект, это non-deterministic behaviour.

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

Ладно, давайте так. Вы хотели понять, почему надо писать тесты на закрытые члены? Я дал очень достаточно информации для размышления. Больше у меня нет. Вашу точку зрения я понял. Даже и раньше знал. Читаем:
http://ru.wikipedia.org/wiki/Разработка_через_тестирование
Читаем раздел «Видимость кода». Про мнения. И убеждаемся, что не одни мы такие и не стоит устраивать холивар. Вспоминаем заодно майкрософт с его генератором для тестов оберток, чтобы можно было достучаться до закрытых членов. Тоже не просто так люди сделали.

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

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

В общем, мир такой и останется, а по сему расходимся ;)
Рассуждения про вар Липерта мне тоже бесполезны. В мире до хрена авторитетов, на любой вкус. Верить всем одновременно — это обманывать себя.

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

Искренне надеюсь, что мне никогда не придется иметь дела с написанным вами кодом.
т.е. как это мое исключительное мнение?

Вы про сайд-эффекты?
http://ru.wikipedia.org/wiki/Функциональное_программирование
Раздел «Чистые функции». Первое предложение. Как бы намекают. Не точно.
http://ru.wikipedia.org/wiki/Побочный_эффект_(программирование)
Тоже скользко, но написано «читать». К сожалению союзом «и» размыли формулировку.
Тогда и сюда:
http://en.wikipedia.org/wiki/Side_effect_(computer_science)
«or has an observable interaction with calling functions or the outside world»
Этим своим observable размыли формулировку. Но если последнее, что представить: у функций нет ни ручек ни ножек, только тело ), то наблюдать и констатировать чтение из внешнего мира мы можем за ней когда она начнет менять выходное значение при тех же параметрах.

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

Извините, что я прям так и на Липерта. Приходится опираться на свой опыт. Но он не исключительный. Действительно в нете можно найти куча абсолютно противоположных мнений. Одни хвалят, другие ругают. Одни — ООП — это жизнь, вторые умерло. Одни — ЮМЛ — это обязательно, вторые — код и только код. Приходится выбирать. И людей с таким мнением как у меня, тоже не мало.
var иногда, как по мне, сильно сбивает с толку. Одно дело, для линькю выражений, а другое, для, например вашего кода:
var sqrt = SqrtImpl(val);
Прямо по имени функции не скажешь что она вернула. И наверное, Липерт прав иногда, потому что заставляет такой способ лучше именовать методы. Но всё же. Если мне говорят «Калина», я как-то не уверен, что я на этой штуке доеду до Москвы ))

Для нью — вар хорошо подходит, потому как после равно тип понятен, а далее все равно по переменной не видно. Но все же, когда я декларировал тип интерфейса, то выражал некоторый смысл.
Это всё индивидуально, конечно. Другие люди, не пользуясь такой техникой как я, пишут кругом листы без интерфейсов, листы, если надо энумерейблы, и не парятся. Я же стараюсь отточить какую-то лаконичность. Правда, не всегда гладко получается.

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

DI никак не мешает статической типизации.
Но тогда любой внешний код при создании ProductService, должен знать то, что должно быть скрыто, а именно: что класс ProductService использует IProductRepository. С чего вдруг? Это кишки сервиса. И что он кушает, нам не важно, важно что он для нас делает. И банально, если этот сервис часто создается, кругом будет дублирование кода.

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

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

Это в независимости от логики репозитория. Просто у меня так рука набита.

Конечно, там может быть логика и какой-то хитрый и оправданный паттерн
Минусуете? )

Это просто подход такой к анализу кода. Прочитайте комментарий выше. То, что я написал не разобравшись — совершенно верно. К коду надо относиться с разных точек зрения. Фаулер любит такой термин «запах кода». Т.е. на код смотрите поверхностно, не углубляясь в то, что он делает и почему. Локально, на кусок кода смотрите и пытаетесь ощутить лажу. Вот и я ощутил. Описал почему.

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

А третий способ — исследовать смысл во всей совокупности всего кода.

Все три надо применять, независимо. Если пытаться только смотреть на то, что код делает (3 способ), то вы можете попадаться на «очарование» паттернов. На их гибкость. На то, что можно будет потом код «настраивать» без дополнительного кодирования. Но по факту, код может становиться нечитаемым и как раз наоборот — негибким — когда не угадали — паттерны «закрепили» код. Код не должен быть гибким, код должен быть наиболее простым для изменений. И не в смысле легкости «настраиваемости левым мизинцем».

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

Ответил на ваш комментарий выше.
Прочитайте что такое:
DI: ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8
IoC: ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F

И конкретные реализации для C#:
code.google.com/p/autofac/
www.ninject.org/

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

Повторю, что такие зависимости — не такая сложная проблема, как может показаться. Рефакторятся на раз. По требованию.
Но если хотите, то выше, показал, как через фабричный метод делается. На самом деле, это то, что по вашей ссылке «Dependency injection вручную».

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

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

Первая же проблема с ходу:
1. у вас есть возможность подсоединить класс, не поддерживающий интерфейс. Компилятор это не проверит. Ну, это ладно, может движок проверять при запуске сразу.
2. У вас есть возможность подсоединить не протестированный класс. Т.е. интерфейс он поддерживает, а тестами не покрыт.

Никаких проблем этот паттерн (с конфигом) не решает. Кроме, разве что, такой гибкости, что чуть ли не юзер в базе классы прописывать сможет. Это надо? На самом деле превращать код в «настройки» — плохая идея. Программы надо писать так, чтобы побольше смысла скормить компилятору, чтобы он на этапе компиляции проверял правильность программы. Желательно, чтобы код был однородным (в одном языке, например шарп)
Все реализации, про которые я написал выше поддерживают (и рекомендуют) конфигурацию в коде с использованием fluent синтаксиса. Соответственно мы избегаем проблем, описаных вами. Советую всё-таки ознакомиться с конкретными реализациями.
К комменту RaveNoX добавлю еще: советую все-таки ознакомиться с книжкой, на которую ссылаются в посте.
Не совсем понял по поводу «Bastard Injection»

«Как вариант избежать данного анти-паттерна, предлагается использовать Dependency Injection. Желательно опять же через конструктор, т.к. внедрение зависимостей через конструктор является наиболее корректным способом DI.»

Так если класс обязан иметь конструктор по умолчанию, который будет вызываться к примеру каким-то фреймворком или просто по какому-то паттерну, то как здесь использовать constructor injection?
Это уже что-то инфраструктурное, тут решение только воспользоваться ServiceLocator и в конструкторе инициализировать все зависимости.

PS: необязательно выносить все зависимости в конструктор — можно через свойства (Property Injection).
Я понимаю, что можно через property injection, я просто не понял как можно применить в данном случае constructor injection?
Для меня это звучит так же как «А бывают случаи, когда constructor injection по каким-либо причинам нельзя здесь использовать. Как вариант решения надо использовать DI, желательно через constructor injection»
Если у вас экземпляр создается внутри стороннего метода, требующего paramtereless constructo, к примеру ServiceHost(typeof(MyService)) — то DI вам тут НИКАК не поможет. Property Injection — это лишь альтернатива Constructor Injection с той лишь разницей, что через PI можно использовать цикличные зависимости.
Так если класс обязан иметь конструктор по умолчанию, который будет вызываться к примеру каким-то фреймворком или просто по какому-то паттерну, то как здесь использовать constructor injection?

Никак, если вкратце. Но это проблема использования DI в инфраструктуре, которая DI не поддерживает.
Понятно, спасибо.
Как раз в таком случае на помощь может придти Service Locator… :)
Да. Как, собственно, и приходится делать.
В оригинале (в книжке) предлагают два решения. Если есть универсальная и очевидная дефолтная реализация, которая находится в той же сборке, то можно отрефакторить к использованию property injection и установить в конструкторе значение этого свойства.

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

P. S. Вообще книжка отличная, полезна не только тем, кто хочет понять, как правильно делать DI, но и вообще, как управлять связанностью.
Между тем, границы принципов внедрения зависимости достаточно размыты. Невозможно провести действительно четкую границу между этим и другими принципами написания качественного объектно-ориентированного кода. Например, принцип Dependency Inversion из SOLID, который часто путают с Dependency Injection, как бы подразумевает внедрение зависимостей, но им не ограничивается.

Различия DI vs. DIP vs. IoC можно четко провести. Например, их разъясняет Сергей Тепляков в своем посте DI vs. DIP vs. IoC.
Хочу заметить, что вы сослались на статью, которая появилась через почти два года после написания этого поста.

Впрочем, то что Сергей Тепляков ввел некие собственные определения, не отменяет правоты того что написано в моем абзаце :)
то, что написано в абзаце — это цитата или Ваше личное мнение?
и изменилось ли что-то к сегодняшему моменту?
Это выжимка из Симана, Фаулера, Мартина и дискуссий наподобие
www.theserverside.com/discussions/thread/23358.html

Фаулер придумал DI(njection), Мартин придумал DI(nversion).
И где-то рядом «сверху» всегда был IoC. И все они где-то пересекаются, действительно четких границ нет, как бы нам этого не хотелось.
Ок, это Ваша интерпретация указанных авторов. Возможно, кто-то считает, что нет четких границ, но такие взгляды сами по себе довольно размыты и непрактичны. И такой подход может говорить о том, что на самом деле нет понимания сути вопроса. Я это пишу не для холивара, а скорее в образовательных целях с мыслями о тех, кто будет Ваш пост читать.

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

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

Я не утверждаю, что он изрек истину в последней инстанции. Возможно, у Вас за время, прошедшее с написания поста, целостное понимание сложилось? Поделитесь?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории