вторник, 26 апреля 2011 г.

Dependency Inversion Principle и инкапсуляция

При всей моей любви к Dependency Inversion Principle, меня всегда немного смущало то, как он соотносится с инкапсуляцией.

Давайте разберемся на примерах. И вот первый из них:

public class Car : ICar
{
private readonly IEngine _engine;

public Car(IEngine engine)
{
_engine = engine;
}

...
}

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

public class MyValidator : IValidator
{
private readonly IChecksumGenerator _checksumGenerator;

public MyValidator(IChecksumGenerator checksumGenerator)
{
_checksumGenerator = checksumGenerator;
}

...
}

В чем отличие от предыдущего примера? В том, что MyValidator завязан на использование конкретной реализации IChecksumGenerator (давайте возьмем это за данность), и программа отработает правильно только с конкретной реализацией. А раз я позволяю заинжектить, IChecksumGenerator то
1. может быть передана другая реализация IChecksumGenerator
2. нарушается инкапсуляция, и знание о требуемой реализации IChecksumGenerator выходит за рамки класса.

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

Update.
В комментариях возникли сомнения по поводу того, нарушает ли второй пример инкапсуляцию. Чтобы разобраться с этим вопросом, давайте рассмотрим второй пример без инъекции IChecksumGenerator (то, как это обычно и делается без SOLID'а):

public class MyValidator : IValidator
{
private readonly IChecksumGenerator _checksumGenerator;

public MyValidator()
{
_checksumGenerator = new MyChecksumGenerator();
}

...
}

Теперь привязка к конкретному типу IChecksumGenerator инкапсулирована внутри MyValidator. В предыдущей же реализации IChecksumGenerator прокидывался в публичный конструктор, и по-моему это самое настоящее нарушение инкапсуляции.

Update 2.
Я задал такой же вопрос на stackoverflow, и Mark Seemann (автор книги Dependency Injection in .NET) дал очень точный диагноз моему коду - нарушение Liskov Substitution Principle. Действительно! Если мой валидатор будет работать только с определенными наследниками IChecksumGenerator, нужно инжектить зависимость по более специфичному интерфейсу. И тогда любая реализация данного интерфейса, переденная в качестве зависимости, отработает корректно. Ума не приложу, почему у меня из головы вылетел LSP, ведь это так просто и очевидно!

Мораль: Все принципы Object-Oriented Design очень тесно связаны - вспоминая один, не забывайте о других.

16 комментариев:

Анонимный комментирует...

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

Unknown комментирует...

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

Unknown комментирует...

Я не вижу здесь нарушений принципов инкапсуляции.

Sul комментирует...

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

Анонимный комментирует...

"инкапсуляция"
В первый раз слышу.

Idsa комментирует...

Добавил в пост Update с пояснением того, каким, на мой взгляд, образом здесь нарушается инкапсуляция.

Роман комментирует...

одно из решений - использовать инъекцию в свойство, пометив его атрибутом (например [Import] как в MEF), тогда IoC-контейнер сам будет брать на себя обязанность "нарушить" принцип инкапсуляции. а конструктор можно оставить без параметров

Анонимный комментирует...

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

Idsa комментирует...

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

Кстати, а почему вы берете в кавычки нарушение инкапсуляции?

Анонимный, фанатизм говорите? Хм... Я, может быть, согласился бы с вами, но как мне при тестировании MyValidator застабить зависимость без DI?

Анонимный комментирует...

>> но как мне при тестировании MyValidator застабить зависимость без DI?
А зачем? Судя по вашей логике никто не должен знать о том как валидатор внутри устроен, и никому не долнжо быть дело используется ли там вообще какой-то Checksum generator.
Как вы будете стабить, если корректное поведение валидатора возможно только при использовании конретного генератора контрольных сумм? Соответсвенно при тесте валидатор должен содержать правильный инстанс генератора, а не стаб/мок.

Idsa комментирует...

Анонимный, ну вот смотрите. Есть реализация IChecksumGenerator, которую я отдельно протестировал. Теперь я хочу протестировать логику MyValidator, но исключить из тестирования ChecksumGenerator, застабив его (дабы сосредоточиться на коде MyValidator, и не тестировать повторно MyChecksumGenerator).

Описанный workflow тестирования стандартный, и, на мой взгляд, тот факт, что MyValidator завязан на конкретную реализацию IChecksumGenerator, в этом отношении ничего не меняет.

Ну и отдельно отвечу на эту цитату:

>> Как вы будете стабить, если корректное поведение валидатора возможно только при использовании конретного генератора контрольных сумм?

Раз есть конкретный генератор контрольных сумм, то застабить его не составит проблем.

Анонимный комментирует...

ну не знаю.
стрёмный пример. Если вы ипользуете DI то как можно завязыватся на конкретную реализацию. Если использовать конкретную реализацию, то нечего выставлять это "на показ".
Лично я бы конкретную реализацию прятал, и, да, пришлось бы тестировать MyChecksumGenerator 2 раза.

Idsa комментирует...

Анонимный, а по-моему отличный пример. Есть валидатор, он рассчитывает, что ему придет поток байт, контрольная сумма которого рассчитывается по определенному алгоритму. За уши ничего не притянуто.

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

Shaddix комментирует...

Анонимный, есть архитектурные принципы.

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

Ну например - в конструкторе этого самого MyChecksumGenerator'a требуются еще с пяток разных зависимостей. Откуда их взять в Валидаторе?

Анонимный комментирует...

сдаюсь :hands up:

Idsa комментирует...

Добавил Update 2 на тему Liskov Substitution Principle