SoftCraft
разноликое программирование

Яндекс.Метрика

SOLID и процедурно-параметрическое программирование

© 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 = &wp;
    ip->outDigits();

    // Демонстрация плохого читателя, пропускающего не цифры
    BadReader br(buf);
    ir = &br;
    printf("Input string for the bad reader: ");
    ir->readDigStr();

    ip = &dp;
    ip->outDigits();

    // Может упасть, если во водимой строке будут не только цифры
    ip = &wp;
    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 = &wp;
    outDigits<ip>();

    // Демонстрация плохого читателя, пропускающего не цифры
    Reader.Bad br; InitReader(&br, buf);
    ir = &br;
    printf("Input string for the bad reader: ");
    readDigStr<ir>();

    ip = &dp;
    outDigits<ip>();

    // Может упасть, если во водимой строке будут не только цифры
    ip = &wp;
    outDigits<ip>();

    return 0;
  }

Резюме

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


Содержание