1

Тема: Принцип инверсии зависимости (Dependency Inversion Principle)

Принцип Инверсии Зависимости

В этой статья я хотел бы поговорить об элементе D из набора принципов и паттернов S.O.L.I.D. Принципы и паттерны, относящиеся к S.O.L.I.D. можно считать краеугольными камнями «хорошего» проектирования приложений. В данном контексте D представляет принцип инверсии зависимости (dependency inversion). В предыдущей статья я рассказывал про элемент S представляющий принцип одной ответственности (single responsibility).


Что такое «Плохое проектирование»?

Давайте сначала немного поговорим о том, что такое плохое проектирование. Можно ли что-то называть плохим проектированием, если кто-то заявляет:

Я бы сделал это по-другому..

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

•    Система не обладает гибкостью (rigit): очень трудно изменить какую-то часть системы, так чтобы это не затронуло слишком много других ее частей.

•    Система ненадежна (fragile): при изменении какой-либо отдельной части, другие части системы перестают корректно работать.

•    Система не или ее часть имеет много связей (immobile): очень трудно повторно использовать части системы в других приложения, поскольку они имеют много связей и зависимостей с другими частями приложения.

Thumbs up Thumbs down

2

Re: Принцип инверсии зависимости (Dependency Inversion Principle)

Сильная связанность (immobile design)

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

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

public class EncryptionService
{
    public void Encrypt(string sourceFileName, string targetFileName)
    {
        // Читаем содержимое файла
        byte[] content;
        using(var fs = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read))
        {
            content = new byte[fs.Length];
            fs.Read(content, 0, content.Length);
        }
 
        // шифруем
        byte[] encryptedContent = DoEncryption(content);
 
        // записываем шифрованные данные
        using(var fs = new FileStream(targetFileName, FileMode.CreateNew, FileAccess.ReadWrite))
        {
            fs.Write(encryptedContent, 0, encryptedContent.Length);
        }
    }
 
    private byte[] DoEncryption(byte[] content)
    {
        byte[] encryptedContent = null;
        // здесь находится алгоритм шифрования..
        return encryptedContent;
    }
}

Листинг 1: Служба шифрования, зависящая от деталей


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

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

public enum ContentSource { File, Database }
public enum ContentTarget { File, WebService }
 
public class EncryptionService_2
{
    public void Encrypt(ContentSource source, ContentTarget target)
    {
        // Читаем данные
        byte[] content;
        switch (source)
        {
            case ContentSource.File:     content = GetFromFile(); break;
            case ContentSource.Database: content = GetFromDatabase(); break;
        }
 
        // шифруем
        byte[] encryptedContent = DoEncryption(content);
 
        // записываем шифрованные данные
        switch (target)
        {
            case ContentTarget.File:       WriteToFile(encryptedContent); break;
            case ContentTarget.WebService: WriteToWebService(encryptedContent); break;
        }
    }
 
    // остальной код опущен краткости ради
}

Листинг  2: Немного улучшенная служба шифрования


Однако, это только добавляет в систему новые взаимозависимости. С течением времени все больше и больше различных типов источников и приемников данных будут добавляться в эту программу, и в итоге метод Encrypt будет загажен операторами switch/case, и будет зависеть от множества низкоуровневых модулей. Так что в конце концов система потеряет гибкость и станет хрупкой.

На помощь приходит принцип инверсии зависимости.

Thumbs up Thumbs down

3

Re: Принцип инверсии зависимости (Dependency Inversion Principle)

Принцип инверсии зависимости

Теория: характерные черты принципа инверсии зависимости:

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

б) Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Рассмотрим диаграмму классов на рисунке ниже.

http://silverlight.su/data/users/Sergey/patterns/SOLID/image_5F00_thumb.png


Мы имеем класс EncryptionService, который владеет абстрактным классом Reader (определяемым интерфейсом IReader) и абстрактным классом Writer (определяемым интерфейсом IWriter). Заметьте, что в данном случае абстрагирование достигается не посредством наследования, а через использование интерфейсов. Т.е. мы отделяем интерфейс от реализации.

Метод Encrypt использует Reader для получения данных и посылает зашифрованные данные через Writer

public class EncryptionService
{
    public void Encrypt(IReader reader, IWriter writer)
    {
        // Читает данные
        byte[] content = reader.ReadAll();
 
        // Шифрует
        byte[] encryptedContent = DoEncryption(content);
 
        // Записывает шифрованные данные
        writer.Write(encryptedContent);
    }
 
    // остальное код...
}

Листинг 3: Служба шифрования, зависящая только от абстракций


Теперь метод Encrypt службы шифрования не зависит от особенностей класса Reader или Writer. Зависимости были инвертированы; класс EncryptionService зависит от абстракций, и классы Reader/Writer тоже зависят от тех же самых абстракций.

Описание двух используемых интерфейсов:

public interface IReader
{
    byte[] ReadAll();
}
 
public interface IWriter
{
    void Write(byte[] content);
}

Листник 4: Интерфейсы Reader и  Writer


Теперь служба шифрования может использоваться повторно. Мы может придумывать новые виды реализаций «Reader» и «Writer» для передачи методу Encrypt нашей службы. Более того, совершенно не важно, какое кол-во видов сущностей «Reader» и «Writer» мы создадим, поскольку служба шифрования не будет зависеть ни от одного из них. Не будет никаких взаимных зависимостей, которые могли бы сделать приложение хрупким или негибким, и служба шифрования может теперь использоваться во множестве различных контекстов. Наша служба приобрела маневренность.

Thumbs up Thumbs down

4

Re: Принцип инверсии зависимости (Dependency Inversion Principle)

Почему это называется Инверсией Зависимости?

Структура зависимостей в хорошо спроектированном объектно-ориентированном приложении «инвертирована» по отношению к «традиционной» структуре приложений реализованных в процедурном стиле. В процедурном приложении модули высокого уровня зависят от модулей низкого уровня, а абстракции зависят от деталей (см. листинг 1 и 2).

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

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

Thumbs up Thumbs down

5

Re: Принцип инверсии зависимости (Dependency Inversion Principle)

Резюме

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

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

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

Thumbs up Thumbs down

6

Re: Принцип инверсии зависимости (Dependency Inversion Principle)

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

А в чем преимущества именно такого подхода, почему именно интерфейсы а не наследования. Есил я правильно понимаю, например в asp.net всюду используеться модель провайдеров и она, если я не ошибаюсь основанна именно на наследовании.

Thumbs up Thumbs down

7

Re: Принцип инверсии зависимости (Dependency Inversion Principle)

Ничто не мешает применять наследование при абстрагировании. В языке С++, например, вообще нет такого понятия, как интерфейс, поэтому для этих целей используют абстрактные классы. Однако наследование подразумевает нечто большее, чем только создание общего интерфейса. О назначении того и другого подхода можно прочесть здесь: http://msdn.microsoft.com/ru-ru/library/3b5b8ezk.aspx

Thumbs up Thumbs down

8

Re: Принцип инверсии зависимости (Dependency Inversion Principle)

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

Thumbs up Thumbs down

9

Re: Принцип инверсии зависимости (Dependency Inversion Principle)

Gisdev пишет:

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

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

Thumbs up Thumbs down