|
© 2026
Александр Легалов
Содержание
DIP (Dependency Inversion Principle). Принцип инверсии зависимости
Принцип инверсии зависимости (Dependency-Inversion Principle – DIP) определяется Робертом Мартином [mr2011Мартин Р., Мартин М. Принципы, паттерны и методики гибкой разработки на языке C#. – СПб.: Символ-Плюс, 2011. – 768 с.] следующим образом:
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те и другие должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Если отбросить последующие заумные рассуждения об инверсии и различиях между структурным и объектно-ориентированным проектированием (грань при мультипарадигменном программировании давно протерта до дыр), то можно привести более адекватную "железную" ассоциацию: процессор можно запаять в плату, а можно поставить, используя сокет (разьем). Гибкость в замене процессора или материнки, при выходе из строя или обновлении любого из этих узлов, во втором случае менее трудоемка. Подобные решения могут быть гибкими как при использовании интерфейсов, так и функций, косвенно связанных с аргументами. В большинстве случаев, что отражено и в книгах дядюшки Боба, они заточены на гибкое подключение различных альтернатив. Такие альтернативы могут возникать как при подключении различных по функционалу программных объектов, так и в случае апдейтов, связанных с исправлением ошибок предыдущих версий. Любые разновидности динамического полиморфизма могут оказаться здесь полезными, хотя могут вносить дополнительные промежуточных слои.
Решение о форме промежуточных абстракций является неоднозначным вопросом и во многом зависит от специфики программы. Но то, что это не всегда чистый интерфейс - однозначно. Интерфейсы не строятся сами по себе. Они зависят от данных и алгоритмов, осуществляющих их обработку. Поэтому они в чистом виде не могут появиться до появления конкретных объектов или классов. Зачастую вместо интерфейса, особенно при использовании C++, возникает необходимость использования абстрактных классов, содержащих, наряду с чистыми методами, дополнительные данные, конструкторы и деструкторы. Это позволяет избавиться от лишних промежуточных слоев при формировании производных классов, определяющих подключаемые альтернативы. Но в любом случае для повышения гибкости и такие промежуточные слои определяют зависимость от абстракций, а не конкретных конструкций.
Для демонстрации использования промежуточных абстракций и обоснования их необходимости рассмотрим простой пример. Разработаем программу, осуществляющую ввод строки цифр в некоторый буфер с последующим выводом отдельных символов в столбик. Для первоначальной реализации и демонстрации можно создать простую программу, например на C, которая в лоб решает поставленную задачу. Ну, не решает, а только демонстрирует ее понимание и множество ошибок которые при этом возникают.
//==============================================================================
// Простейшая программа, осуществляющая чтение строки цифр во внутренний
// буфер, из которого эти символы выводятся в столбик
//==============================================================================
#include <stdio.h>
#include <string.h>
#define MAX_STR_SIZE 256
int main() {
char buf[MAX_STR_SIZE];
fgets(buf, MAX_STR_SIZE, stdin);
int len = strlen(buf);
// Тестовый вывод введенной строки
printf("buf[%d] = %s\n", len, buf);
for(int i = 0; i < len-1; ++i) {
putchar(buf[i]);
putchar('\n');
}
}
Этот эрзац-набросок неадекватно ведет себя во многих ситуациях. Поэтому для более реального применения желательно перейти к более чистой реализации, включая разделение обязанностей и прочее, так как любые модификации ведут к изменению написанного кода. Если же изменения вносятся постоянно, то наличие абстрактных промежуточных слоев, соответствующих принципу инверсии зависимости, позволяют сформировать более удобоваримый код.
Объектно-ориентированная программа с промежуточным абстрактным слоем
В случае объектно-ориентированной программы промежуточный слой для объекта, осуществляющего ввод строки, можно сформировать в виде абстрактного класса, который имеет возможность привязаться к выделяемому ему символьному буферу, реализует интерфейс для метода загрузки с консоли строки, а также имеет метод, обеспечивающий доступ из вне к буферу другим объекта. В целом формируется простой класс не требующий каких-то специальных комментариев. Простота примера также позволяет провести демонстрацию без дополнительных наворотов в виде библиотек C++.
#define MAX_STR_SIZE 256 // размер буфера строки
//------------------------------------------------------------------------------
// Интерфейст читателя цифр. Включает вспомогательные данные и функции
// помимо переопределяемой далее функции чтения строки.
class IReader {
protected:
char* buf;
public:
IReader(char* b): buf{b} { b[0] = '\0'; }
char* getBuf() {
return buf;
}
virtual void readDigStr() = 0;
};
На основе промежуточного абстрактного слоя можно сформировать класс, обеспечивающий необходимый ввод данных, аналогично тому, как это было уже реализовано в программе на C.
//------------------------------------------------------------------------------
// Читатель строки, допускающий на выходе любые символы, а не только цифры
class BadReader: public IReader {
public:
BadReader(char* b): IReader{b} {}
void readDigStr() {
fgets(buf, MAX_STR_SIZE, stdin);
buf[strlen(buf) - 1] = '\0'; // убрали `\n`
printf("Bad reader string = %s\n", buf);
}
};
Аналогичным образом формируются абстрактный слой и конкретный класс, обеспечивающие вывод цифр в стандартный поток. То, что там еще выводится и мусор, пока проигнорируем.
//------------------------------------------------------------------------------
// Интерфейс принтеров, обрабатывающих цифры
class IPrinter {
protected:
IReader* reader;
public:
IPrinter(IReader* r): reader{r} {}
virtual void outDigits() = 0;
};
//------------------------------------------------------------------------------
// Принтер, печатающий цифры как есть
class DigPrinter: public IPrinter {
public:
DigPrinter(IReader* r): IPrinter{r} {}
void outDigits() {
char* buf = reader->getBuf();
int len = strlen(buf);
for(int i = 0; i < len; ++i) {
putchar(buf[i]);
putchar('\n');
}
}
};
Клиент, используемый в главной функции, может взаимодействовать с переопределяемыми методами через абстрактные интерфейсы.
//------------------------------------------------------------------------------
// Тестовая функция демонстрирующая взаимодействие читателей строк и
// принтеров через абстрактные интерфейсы
int main() {
// Буфер, используемый для строки
char buf[MAX_STR_SIZE];
// Абстрактные интерфейсы для подключения читателей строки и принтеров
IReader* ir;
IPrinter* ip;
// Демонстрация плохого читателя, пропускающего не цифры
BadReader br(buf);
ir = &br;
printf("Input string for the bad reader: ");
ir->readDigStr();
DigPrinter dp(ir);
ip = &dp;
ip->outDigits();
return 0;
}
Использование методов конкретных объектов через абстрактные интерфейсы, реализованные с использованием указателей позволяют избавиться от непосредственного взаимодействия. Вместе с тем следует отметить, что реализованный конкретный читатель пропускает различные символы, а не только цифры, что ведет к некорректному выводу данных. Поэтому разработаем новую версию, которая оставляет в буфере только цифры, обеспечивая для принтера только правильные данные. Эта новая версия может заменить старую, подключаясь через тот же абстрактный интерфейс.
//------------------------------------------------------------------------------
// Читатель, оставляющий после прочтения только цифры
class DigReader: public IReader {
public:
DigReader(char* b): IReader{b} {}
void readDigStr() {
fgets(buf, MAX_STR_SIZE, stdin);
// Оставление только цифровых символов
int len = strlen(buf);
int j = 0;
for(int i = 0; i < len; ++i) {
if((buf[i] >= '0') && (buf[i] <= '9')) {
buf[j++] = buf[i]; // Пропуск цифры
}
}
buf[j] = '\0'; // конец цифровой строки
printf("%s\n", buf);
}
};
Аналогичным образом можно осуществлять доработки и принтера. Например, помимо вывода чисел добавим их название.
//------------------------------------------------------------------------------
// Принтер, печатающий цифры и их названия
class WordPrinter: public IPrinter {
static const char* wd[];
public:
WordPrinter(IReader* r): IPrinter{r} {}
void outDigits() {
char* buf = reader->getBuf();
int len = strlen(buf);
for(int i = 0; i < len; ++i) {
printf("%c: %s\n", buf[i], wd[buf[i]-'0']);
}
}
};
// Массив названий, используемый для печати после сопоставления с цифрами
const char* WordPrinter::wd[] = {
"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"
};
В окончательной главной функции демонстрируются альтернативные варианты читателей и принтеров, подключаемых через абстрактные интерфейсты
//------------------------------------------------------------------------------
// Тестовая функция демонстрирующая взаимодействие читателей строк и
// принтеров через абстрактные интерфейсы
int main() {
// Буфер, используемый для строки
char buf[MAX_STR_SIZE];
// Абстрактные интерфейсы для подключения читателей строки и принтеров
IReader* ir;
IPrinter* ip;
// Использования читателя, оставляющего в строке только цифры
DigReader dr(buf);
ir = &dr;
printf("Input string for the good reader: ");
ir->readDigStr();
DigPrinter dp(ir);
ip = &dp;
ip->outDigits();
WordPrinter wp(ir);
ip = ℘
ip->outDigits();
// Демонстрация плохого читателя, пропускающего не цифры
BadReader br(buf);
ir = &br;
printf("Input string for the bad reader: ");
ir->readDigStr();
ip = &dp;
ip->outDigits();
// Может упасть, если во водимой строке будут не только цифры
ip = ℘
ip->outDigits();
return 0;
}
Процедурно-параметрическая поддержка DIP
Отсутствие динамического полиморфизма в чистом процедурном подходе не позволяет непосредственно формировать и использовать промежуточные абстракции. Однако его наличие в процедурно-параметрической парадигме позволяет обойти эти недостатки, обеспечивая тем самым гибкое расширение альтернатив за счет независимых функций вместо использования интерфейсов. Все начинается с иного подхода к формировании альтернатив. Формируемая в начале обобщающая структура читателя, как и соответствующий абстрактный класс ОО программы, содержит указатель на подключаемый буфер и функции, осуществляющие инициализацию структуры, а также доступа к буферу. Обобщенная функция является абстрактной и будет переопределяться для специализаций.
#define MAX_STR_SIZE 256 // размер буфера строки
//------------------------------------------------------------------------------
// Интерфейс читателя цифр. Включает вспомогательные данные и функции
// помимо переопределяемой далее функции чтения строки.
typedef struct Reader {
char* buf;
}<> Reader;
// Инициализация общей для всех читателей части.
// Осуществляетс связь с используемым буфером.
void InitReader(Reader* r, char* b) {
r->buf = b;
r->buf[0] = '\0';
}
// Получение указателя на буфер, подключенный к читателю
char* getBuf(Reader* r) {
return r->buf;
}
// Обобщающая функция, переопределяемая конкретными читателями
void readDigStr<Reader* r>() = 0;
Как и в случае ОО программы первоначально можно сформировать "плохого" читателя, добавив в качестве основы признак, имитирующий перечислимый тип, так как дополнительные данные отсутствуют. Обработчик специализации выполняет конкретные действия по чтению данных в буфер аналогично тому, как это происходит в ОО программе.
//------------------------------------------------------------------------------
// Читатель строки, допускающий на выходе любые символы, а не только цифры
Reader + <Bad: void;>;
// Переопределение функции чтения в буфер
void readDigStr<Reader.Bad* r>() {
fgets(r->buf, MAX_STR_SIZE, stdin);
r->buf[strlen(r->buf) - 1] = '\0'; // убрали `\n`
printf("Bad reader string = %s\n", r->buf);
};
По аналогии с читателем формируется обобщение и его обобщающая функция, которые расширяются при создании конкретного принтера, выводящего только цифры. То есть можно видеть идентичное, по сравнению с ООП, повторение процесса разработки.
//------------------------------------------------------------------------------
// Интерфейс принтеров, обрабатывающих цифры
typedef struct Printer {
Reader* reader;
}<> Printer;
// Инициализация данных, общих для всех принтеров.
// Осуществляетс связь с используемым читателем.
void InitPrinter(Printer* p, Reader* r) {
p->reader = r;
}
// Обобщающая функция отображения цифр
void outDigits<Printer* p>() = 0;
//------------------------------------------------------------------------------
// Принтер, печатающий цифры как есть
Printer + <Dig: void;>;
// Вывод цифр как есть
void outDigits<Printer.Dig* p>() {
char* buf = getBuf(p->reader);
int len = strlen(buf);
for(int i = 0; i < len; ++i) {
putchar(buf[i]);
putchar('\n');
}
}
Дальнейшиет модификации и улучшение ранее разработанных специализаци проводится по аналогичной схеме и вряд ли требует дополнительных комментариев. Улучшаем читателя, который оставляет только цифры, и расширяем принтер выводом названий цифр.
//------------------------------------------------------------------------------
// Читатель, оставляющий после прочтения только цифры
Reader + <Dig: void;>;
// Переопределение функции чтения в буфер
void readDigStr<Reader.Dig* r>() {
fgets(r->buf, MAX_STR_SIZE, stdin);
// Оставление только цифровых символов
int len = strlen(r->buf);
int j = 0;
for(int i = 0; i < len; ++i) {
if((r->buf[i] >= '0') && (r->buf[i] <= '9')) {
r->buf[j++] = r->buf[i]; // Пропуск цифры
}
}
r->buf[j] = '\0'; // конец цифровой строки
printf("%s\n", r->buf);
}
//------------------------------------------------------------------------------
// Принтер, печатающий вместо цифр их названия
Printer + <Word: void;>;
// Массив названий, используемый для печати после сопоставления с цифрами
const char* wd[] = {
"zero", "one", "two", "three", "four",
"five", "six", "seven", "eight", "nine"
};
// Вывод названий цифр
void outDigits<Printer.Word* p>() {
char* buf = getBuf(p->reader);
int len = strlen(buf);
for(int i = 0; i < len; ++i) {
printf("%c: %s\n", buf[i], wd[buf[i]-'0']);
}
}
Главная функция, как и в ОО программе, организут доступ к обработчикам специализаций через абстракции, задаваемые в данном случае обобщениями.
//------------------------------------------------------------------------------
// Тестовая функция демонстрирующая взаимодействие читателей строк и
// принтеров через абстрактные интерфейсы
int main() {
// Буфер, используемый для строки
char buf[MAX_STR_SIZE];
// Абстрактные интерфейсы для подключения читателей строки и принтеров
Reader* ir;
Printer* ip;
// Использования читателя, оставляющего в строке только цифры
Reader.Dig dr; InitReader(&dr, buf);
ir = &dr;
printf("Input string for the good reader: ");
readDigStr<ir>();
Printer.Dig dp; InitPrinter(&dp, ir);
ip = &dp;
outDigits<ip>();
Printer.Word wp; InitPrinter(&wp, ir);
ip = ℘
outDigits<ip>();
// Демонстрация плохого читателя, пропускающего не цифры
Reader.Bad br; InitReader(&br, buf);
ir = &br;
printf("Input string for the bad reader: ");
readDigStr<ir>();
ip = &dp;
outDigits<ip>();
// Может упасть, если во водимой строке будут не только цифры
ip = ℘
outDigits<ip>();
return 0;
}
Резюме
Процедурно-параметрическая парадигма, используя обобщения и обобщающие функции, позволяет легко формировать промежуточные абстракции, отделяющие различные модули от конкретных реализаций. Наличие функций, не привязанных к интерфесам обеспечивает процесс косвенного взаимодействия дополнительной гибкостью. В целом, глядя на возникающие возможности и варианты использования, я бы не стал сводить данный принцип только к инверсии интрефейсов. Скорее всего он больше завязан (и для ОО подхода тоже) на подключение альтернатив различными методами, включая и использование интерфейсов.
Содержание
|