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

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

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

© 2026
Александр Легалов


Содержание


SRP (Single-Responsibility Principle).
Принцип единственной ответственности

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

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

"Плохое" объектно-ориентированное решение

Так как придумывать новые примеры лениво, то обращуюсь уже к существующим геометрическим фигурам [evoЛегалов А.И. Динамический полиморфизм и эволюция. - 2026.], которые также присутствуют и в книгах Роберта Мартина. Но рассмотрю более насыщенный исходный пример, включающий наличие базового класса фигуры и пары потомков: прямоугольника и треугольника. Начну с "плохого" решения, в котором все используемые методы реализованы в одном классе контейнера, доступного клиентам для вывода (рисования) данных, а также для вычисления переметров и площадей размещенных в нем геометрических фигур. Предположим, что один клиент этой программы занимается только рисованием фигур (консольным выводом в файл), а другой вычислением, а также выводом периметров и площадей фигур, размещенных в контейнере. Будем считать, что ввод фигур из файла, а также очистка контейнера являются служе6ными методами, запускаемыми до запуска работы клиентов.

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


  //------------------------------------------------------------------------------
  // Класс, обобщающает все имеющиеся фигуры.
  // Является абстрактным, обеспечивая, тем самым, проверку интерфейса
  class Figure {
    public:
    // иденитификация, порождение и ввод фигуры из потока
    static  Figure* In(std::ifstream &ifst);      // упрощенный ввод фигур
    virtual void InData(std::ifstream &ifst) = 0; // ввод данных из потока
    virtual void Out(std::ofstream &ofst) = 0;    // вывод данных в стандартный поток
    virtual double Perimeter() = 0;               // вычисление периметра
    virtual double Area() = 0;                    // вычисление площади
  };

  //------------------------------------------------------------------------------
  // прямоугольник
  class Rectangle: public Figure {
    int x, y; // ширина, высота
    public:
    // переопределяем интерфейс класса
    virtual void InData(std::ifstream &ifst); // ввод данных из потока
    virtual void Out(std::ofstream &ofst);    // вывод данных в стандартный поток
    virtual double Perimeter();               // вычисление периметра
    virtual double Area();                    // вычисление площади
    Rectangle(): x{0}, y{0} {}                // создание без инициализации.
  };

  //------------------------------------------------------------------------------
  // треугольник
  class Triangle: public Figure {
    int a, b, c; // стороны
    public:
    // переопределяем интерфейс класса
    virtual void InData(std::ifstream &ifst); // ввод данных из потока
    virtual void Out(std::ofstream &ofst);    // вывод данных в стандартный поток
    virtual double Perimeter();               // вычисление периметра
    virtual double Area();                    // вычисление площади
    Triangle(): a{0}, b{0}, c{0} {}           // создание без инициализации.
  };

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


  //==============================================================================
  // rectangle.cpp - реализация методов прямоугольника
  //==============================================================================

  //------------------------------------------------------------------------------
  // Ввод параметров прямоугольника
  void Rectangle::InData(std::ifstream &ifst) {
    ifst >> x >> y;
  }
  //------------------------------------------------------------------------------
  // Вывод параметров прямоугольника
  void Rectangle::Out(std::ofstream &ofst) {
    ofst << "It is Rectangle: x = " << x << ", y = " << y << "\n";
  }
  //------------------------------------------------------------------------------
  // Вычисление периметра прямоугольника
  double Rectangle::Perimeter() {
    return (double)(2*(x + y));
  }
  //------------------------------------------------------------------------------
  // Вычисление площади прямоугольника
  double Rectangle::Area() {
    return (double)(x * y);
  }

  //==============================================================================
  // triangle.cpp - реализация методов треугольника
  //==============================================================================

  //------------------------------------------------------------------------------
  // Ввод параметров прямоугольника
  void Triangle::InData(std::ifstream &ifst) {
    ifst >> a >> b >> c;
  }

  //------------------------------------------------------------------------------
  // Вывод параметров треугольника
  void Triangle::Out(std::ofstream &ofst) {
    ofst << "It is Triangle: a = "
    << a << ", b = " << b
    << ", c = " << c << "\n";
  }

  //------------------------------------------------------------------------------
  // Вычисление периметра треугольника
  double Triangle::Perimeter() {
    return (double)(a + b + c);
  }

  //------------------------------------------------------------------------------
  // Вычисление площади треугольника
  double Triangle::Area() {
    double p = double(a + b + c) / 2.0;
    return sqrt(p * (p-a) * (p-b) * (p-c));
  }

Реализация контейнера также не ориентируется на разнообразных клиентов. Поэтому интерфейс класса вбирает все необходимые методы.


  //------------------------------------------------------------------------------
  // Простейший контейнер на основе одномерного массива
  class Container {
    enum {max_len = 100}; // максимальная длина
    int len; // текущая длина
    Figure *cont[max_len];
    public:
    void In(std::ifstream &ifst);     // ввод фигур в котнейнер из входного потока
    void Out(std::ofstream &ofst);    // вывод фигур в выходного потока
    void PerimeterOut(std::ofstream &ofst); // вывод периметров в поток
    void AreaOut(std::ofstream &ofst); // вывод периметров в поток
    void Clear();                      // очистка контейнера от фигур
    Container();                       // инициализация контейнера
    ~Container() {Clear();}            // утилизация контейнера перед уничтожением
  };

Размещение методов контейнера по модулям остается на усмотрение разработчика.


  //------------------------------------------------------------------------------
  // Инициализация контейнера
  Container::Container(): len{0} { }
  //------------------------------------------------------------------------------
  // Очистка контейнера от элементов (освобождение памяти)
  void Container::Clear() {
    for(int i = 0; i < len; i++) {
      delete cont[i];
    }
    len = 0;
  }
  //------------------------------------------------------------------------------
  // Ввод содержимого контейнера
  void Container::In(std::ifstream &ifst) {
    while(!ifst.eof()) {
      if((cont[len] = Figure::In(ifst)) != 0) {
        len++;
      }
    }
  }
  //------------------------------------------------------------------------------
  // Вывод содержимого контейнера
  void Container::Out(std::ofstream &ofst) {
    ofst << "Container contents " << len << " elements.\n";
    for(int i = 0; i < len; i++) {
      ofst << i << ": ";
      cont[i]->Out(ofst);
    }
  }
  //------------------------------------------------------------------------------
  // Вывод периметров в поток
  void Container::PerimeterOut(std::ofstream &ofst) {
    ofst << "Perimeters of figures:\n";
    for(int i = 0; i < len; i++) {
      ofst << i << ": " << cont[i]->Perimeter() << "\n";
    }
  }
  //------------------------------------------------------------------------------
  // Вывод площадей в поток
  void Container::AreaOut(std::ofstream &ofst) {
    ofst << "Areas of figures:\n";
    for(int i = 0; i < len; i++) {
      ofst << i << ": " << cont[i]->Area() << "\n";
    }
  }

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


  //------------------------------------------------------------------------------
  // Интерфейс клиентского кода, осуществляющего вывод (рисование) фигур
  class ClientDraw {
    public:
    ClientDraw(Container& c): container{c} {}
    // Вывод содержимого контейнера
    void DrawContainer(std::ofstream &ofst);
    private:
    // Ссылка на используемый внешний контейнер
    Container& container;
  };

  //==============================================================================
  // Реализация клиентского кода, осуществляющего вывод (рисование) фигур
  //==============================================================================

  // Вывод содержимого контейнера
  void ClientDraw::DrawContainer(std::ofstream &ofst) {
    container.Out(ofst);
  }

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


  //------------------------------------------------------------------------------
  // Интерфейс клиентского кода, осуществляющего геометрические вычисления
  class ClientGeometry {
    public:
    ClientGeometry(Container& c): container{c} {}
    // Вычисление и вывод периметров
    void CalcPerimeters(std::ofstream &ofst);
    // Вычисление и вывод площадей
    void CalcAreas(std::ofstream &ofst);
    private:
    // Ссылка на используемый внешний контейнер
    Container& container;
  };

  //==============================================================================
  // Реализация клиентского кода, осуществляющего геометрические вычисления
  //==============================================================================

  //------------------------------------------------------------------------------
  // Вычисление периметров
  void ClientGeometry::CalcPerimeters(std::ofstream &ofst) {
    container.PerimeterOut(ofst);
  }
  //------------------------------------------------------------------------------
  // Вычисление площадей
  void ClientGeometry::CalcAreas(std::ofstream &ofst) {
    container.AreaOut(ofst);
  }

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


  //==============================================================================
  // Методы супервизора, используемые для обслуживания клиентов
  //==============================================================================

  //------------------------------------------------------------------------------
  // Инициализация контейнера
  Container::Container(): len{0} { }
  //------------------------------------------------------------------------------
  // Очистка контейнера от элементов (освобождение памяти)
  void Container::Clear() {
    for(int i = 0; i < len; i++) {
      delete cont[i];
    }
    len = 0;
  }
  //------------------------------------------------------------------------------
  // Ввод содержимого контейнера
  void Container::In(std::ifstream &ifst) {
    while(!ifst.eof()) {
      if((cont[len] = Figure::In(ifst)) != 0) {
        len++;
      }
    }
  }

  //==============================================================================
  // Метод выводв фигур, используемые клиентом, осуществляющим "рисование"
  //==============================================================================

  //------------------------------------------------------------------------------
  // Вывод содержимого контейнера
  void Container::Out(std::ofstream &ofst) {
    ofst << "Container contents " << len << " elements.\n";
    for(int i = 0; i < len; i++) {
      ofst << i << ": ";
      cont[i]->Out(ofst);
    }
  }

  //==============================================================================
  // Методы, используемые клиентом, осуществляющим геометрические вычисления
  //==============================================================================

  //------------------------------------------------------------------------------
  // Вывод периметров в поток
  void Container::PerimeterOut(std::ofstream &ofst) {
    ofst << "Perimeters of figures:\n";
    for(int i = 0; i < len; i++) {
      ofst << i << ": " << cont[i]->Perimeter() << "\n";
    }
  }
  //------------------------------------------------------------------------------
  // Вывод площадей в поток
  void Container::AreaOut(std::ofstream &ofst) {
    ofst << "Areas of figures:\n";
    for(int i = 0; i < len; i++) {
      ofst << i << ": " << cont[i]->Area() << "\n";
    }
  }

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


  int main(int argc, char* argv[]) {
    if(argc !=3) {
      std::cout << "incorrect command line! Wated: command infile outfile\n";
      return 1;
    }
    std::ifstream ifst(argv[1]);
    std::ofstream ofst(argv[2]);

    std::cout << "Start\n";
    Container c;
    c.In(ifst);

    // Вывод контейнера рисующим клиентом
    ClientDraw clientDraw(c);
    std::cout << "ClientDraw draws container\n";
    clientDraw.DrawContainer(ofst);

    // Организация вычислений геометрическим клиентом
    ClientGeometry clientGeometry(c);
    std::cout << "ClientGeometry calcs perimeters\n";
    clientGeometry.CalcPerimeters(ofst);

    std::cout << "ClientGeometry calcs areas\n";
    clientGeometry.CalcAreas(ofst);

    c.Clear();
    ofst << "ClientDraw draws empty container.\n";
    // Вывод контейнера рисующим клиентом
    clientDraw.DrawContainer(ofst);

    std::cout << "Stop\n";
    return 0;
  }

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

"Хорошее" решение, соответсвующее SRP

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

  //------------------------------------------------------------------------------
  // Интерфейс клиентского кода, осуществляющего вывод (рисование) фигур
  class ClientDraw {
    public:
    ClientDraw(Container& c): container{c} {}
    // Вывод содержимого контейнера
    void DrawContainer(std::ofstream &ofst);

    private:
    // Вывод содержимого фигуры
    void DrawFigure(Figure* f, std::ofstream &ofst);
    // Ссылка на используемый внешний контейнер
    Container& container;
  };

  //------------------------------------------------------------------------------
  // Интерфейс клиентского кода, осуществляющего геометрические вычисления
  class ClientGeometry {
    public:
    ClientGeometry(Container& c): container{c} {}
    // Вычисление и вывод периметров
    void CalcPerimeters(std::ofstream &ofst);
    // Вычисление и вывод площадей
    void CalcAreas(std::ofstream &ofst);

    private:
    // Вычиcление периметра фигуры
    double CalcFigurePerimeter(Figure* f, std::ofstream &ofst);
    // Вычиcление площади фигуры
    double CalcFigureArea(Figure* f, std::ofstream &ofst);
    // Ссылка на используемый внешний контейнер
    Container& container;
  };

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

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


//------------------------------------------------------------------------------
// Простейший контейнер на основе одномерного массива
class Container {
  friend class ClientDraw;
  friend class ClientGeometry;
  enum {max_len = 100}; // максимальная длина
  int len; // текущая длина
  Figure *cont[max_len];
  public:
  void In(std::ifstream &ifst);     // ввод фигур в котнейнер из входного потока
  void Clear();  // очистка контейнера от фигур
  Container();    // инициализация контейнера
  ~Container() {Clear();} // утилизация контейнера перед уничтожением
};

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


  //------------------------------------------------------------------------------
  // Класс, обобщающает все имеющиеся фигуры.
  // Является абстрактным, обеспечивая, тем самым, проверку интерфейса
  class Figure {
    public:
    // иденитификация, порождение и ввод фигуры из потока
    static  Figure* In(std::ifstream &ifst);   // упрощенный ввод фигур
    virtual void InData(std::ifstream &ifst) = 0; // ввод данных из потока
  };

  //------------------------------------------------------------------------------
  // прямоугольник
  class Rectangle: public Figure {
    friend class ClientDraw;
    friend class ClientGeometry;
    int x, y; // ширина, высота
    public:
    // переопределяем интерфейс класса
    virtual void InData(std::ifstream &ifst); // ввод данных из потока
    Rectangle(): x{0}, y{0} {} // создание без инициализации.
  };

  //------------------------------------------------------------------------------
  // треугольник
  class Triangle: public Figure {
    friend class ClientDraw;
    friend class ClientGeometry;
    int a, b, c; // стороны
    public:
    // переопределяем интерфейс класса
    virtual void InData(std::ifstream &ifst);  // ввод данных из потока
    Triangle(): a{0}, b{0}, c{0} {} // создание без инициализации.
  };

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


  //==============================================================================
  // Реализация клиентского кода, осуществляющего вывод (рисование) фигур
  //==============================================================================

  //------------------------------------------------------------------------------
  // Вывод содержимого контейнера
  void ClientDraw::DrawContainer(std::ofstream &ofst) {
    // container.Out(ofst);
    ofst << "Container contents " << container.len << " elements.\n";
    for(int i = 0; i < container.len; i++) {
      ofst << i << ": ";
      DrawFigure(container.cont[i], ofst);
    }
  }
  //------------------------------------------------------------------------------
  // Вывод содержимого фигуры
  void ClientDraw::DrawFigure(Figure* f, std::ofstream &ofst) {
    Rectangle* r;
    Triangle* t;
    if((r = dynamic_cast<Rectangle*>(f)) != nullptr) {
      ofst << "It is Rectangle: x = " << r->x << ", y = " << r->y << "\n";
    } else if((t = dynamic_cast<Triangle*>(f)) != nullptr) {
      ofst << "It is Triangle: a = "
      << t->a << ", b = " << t->b
      << ", c = " << t->c << "\n";
    } else {
      ofst << "Unknown Figure\n";
    }
  }

  //==============================================================================
  // Реализация клиентского кода, геометрические вычисления
  //==============================================================================

  //------------------------------------------------------------------------------
  // Вычисление периметров
  void ClientGeometry::CalcPerimeters(std::ofstream &ofst) {
    // container.PerimeterOut(ofst);
    ofst << "Perimeters of figures:\n";
    for(int i = 0; i < container.len; i++) {
      ofst << i << ": " << CalcFigurePerimeter(container.cont[i], ofst) << "\n";
    }
  }
  //------------------------------------------------------------------------------
  // Вычисление периметра фигуры
  double ClientGeometry::CalcFigurePerimeter(Figure* f, std::ofstream &ofst) {
    Rectangle* r;
    Triangle* t;
    if((r = dynamic_cast<Rectangle*>(f)) != nullptr) {
      return (double)(2*(r->x + r->y));
    } else if((t = dynamic_cast<Triangle*>(f)) != nullptr) {
      return (double)(t->a + t->b + t->c);
    }
    ofst << "Unknown Figure Perimeter\n";
    return 0.0;
  }
  //------------------------------------------------------------------------------
  // Вычисление площадей
  void ClientGeometry::CalcAreas(std::ofstream &ofst) {
    // container.AreaOut(ofst);
    ofst << "Areas of figures:\n";
    for(int i = 0; i < container.len; i++) {
      ofst << i << ": " << CalcFigureArea(container.cont[i], ofst) << "\n";
    }
  }
  //------------------------------------------------------------------------------
  // Вычиcление площади фигуры
  double ClientGeometry::CalcFigureArea(Figure* f, std::ofstream &ofst) {
    Rectangle* r;
    Triangle* t;
    if((r = dynamic_cast<Rectangle*>(f)) != nullptr) {
      return (double)(r->x * r->y);
    } else if((t = dynamic_cast<Triangle*>(f)) != nullptr) {
      double p = double(t->a + t->b + t->c) / 2.0;
      return sqrt(p * (p - t->a) * (p - t->b) * (p - t->c));
    }
    ofst << "Unknown Figure Perimeter\n";
    return 0.0;
  }

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

Процедурно-параметрическое решение

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

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

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


  //------------------------------------------------------------------------------
  // прямоугольник
  typedef struct Rectangle {
    int x, y; // ширина, высота
  } Rectangle;
  //------------------------------------------------------------------------------
  // треугольник
  typedef struct Triangle {
    int a, b, c; // стороны треугольника
  } Triangle;
  //------------------------------------------------------------------------------
  // структура, обобщающая фигуры
  typedef struct Figure {} <> Figure;
  //------------------------------------------------------------------------------
  // Фигура - прямоугольник
  Figure + < rect: Rectangle; >;
  //------------------------------------------------------------------------------
  // Фигура - треугольник
  Figure + < trian: Triangle; >;

Контейнер представляет простую абстракцию, реализующую хранение фигур в одномером массиве.


  //------------------------------------------------------------------------------
  // Ссылка на описание геометрической фигуры. Знание структуры самой фигуры
  // для представленной реализации контейнера не требуется
  struct Figure;
  enum {max_len = 100}; // максимальная длина
  //------------------------------------------------------------------------------
  // Простейший контейнер на основе одномерного массива
  typedef struct Container {
    int len; // текущая длина
    struct Figure *cont[max_len];
  } Container;

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


  //------------------------------------------------------------------------------
  // Вывод параметров прямоугольника в поток
  void RectangleOut(Rectangle *r, FILE* ofst) {
    fprintf(ofst, "It is Rectangle: x = %d, y = %d\n", r->x, r->y);
  }
  //------------------------------------------------------------------------------
  // Вывод параметров треугольника в поток
  void TriangleOut(Triangle *t, FILE *ofst) {
    fprintf(ofst, "It is Triangle: a = %d, b = %d, c = %d\n", t->a, t->b, t->c);
  }

  //------------------------------------------------------------------------------
  // Абстрактная обобщающая функция для вывода параметров фигуры
  void FigureOut<Figure *f>(FILE* file) = 0;
  //------------------------------------------------------------------------------
  // Вывод прямоугольника как фигуры
  void FigureOut<Figure.rect *f>(FILE* ofst) {
    RectangleOut(&(f->@), ofst);
  }
  //------------------------------------------------------------------------------
  // Вывод треугольника как фигуры
  void FigureOut<Figure.trian *f>(FILE* ofst) {
    TriangleOut(&(f->@), ofst);
  }

  //------------------------------------------------------------------------------
  // Вывод содержимого контейнера в указанный поток
  void ContainerOut(Container *c, FILE* ofst) {
    fprintf(ofst, "Container contains %d elements.\n", c->len);
    for(int i = 0; i < c->len; i++) {
      fprintf(ofst, "%d: " , i);
      FigureOut<c->cont[i]>(ofst);
    }
  }

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


  //==============================================================================
  // Вычисление периметров
  //==============================================================================

  //------------------------------------------------------------------------------
  // Вычисление периметра прямоугольника
  double RectanglePerimeter(Rectangle *r) {
    return (double)(2*(r->x + r->y));
  }
  //------------------------------------------------------------------------------
  // Вычисление периметра треугольника
  double TrianglePerimeter(Triangle *t) {
    return (double)(t->a + t->b + t->c);
  }

  //------------------------------------------------------------------------------
  // Вычисление периметра для обобщенной фигуры
  double FigurePerimeter<Figure *f>() {return 0.0;} //= 0;
  //------------------------------------------------------------------------------
  // Периметр прямоугольника как фигуры
  double FigurePerimeter<Figure.rect *f>() {
    return RectanglePerimeter(&(f->@));
  }
  //------------------------------------------------------------------------------
  // Периметр треугольника как фигуры
  double FigurePerimeter<Figure.trian *f>() {
    return TrianglePerimeter(&(f->@));
  }

  //------------------------------------------------------------------------------
  // Вывод периметров фигур контейнера в указанный поток
  void ContainerPerimeterOut(Container *c, FILE* ofst) {
    fprintf(ofst, "Perimeters of figures:\n");
    for(int i = 0; i < c->len; i++) {
      fprintf(ofst, "%d: %f\n" , i, FigurePerimeter<c->cont[i]>());
    }
  }

  //==============================================================================
  // Вычисление площадей
  //==============================================================================

  //------------------------------------------------------------------------------
  // Вычисление площади прямоугольника
  double RectangleArea(Rectangle *r) {
    return (double)(r->x * r->y);
  }
  //------------------------------------------------------------------------------
  // Вычисление площади треугольника
  double TriangleArea(Triangle *t) {
    double p = (double)(t->a + t->b + t->c) / 2.0;
    return sqrt(p * (p - t->a) * (p - t->b) * (p - t->c) );
  }

  //------------------------------------------------------------------------------
  // Вычисление площади для обобщенной фигуры
  double FigureArea<Figure *f>() {return 0.0;} //= 0;
  //------------------------------------------------------------------------------
  // Площадь прямоугольника как фигуры
  double FigureArea<Figure.rect *f>() {
    return RectangleArea(&(f->@));
  }
  //------------------------------------------------------------------------------
  // Площадь треугольника как фигуры
  double FigureArea<Figure.trian *f>() {
    return TriangleArea(&(f->@));
  }

  //------------------------------------------------------------------------------
  // Вывод площадей фигур контейнера в указанный поток
  void ContainerAreaOut(Container *c, FILE* ofst) {
    fprintf(ofst, "Areas of figures:\n");
    for(int i = 0; i < c->len; i++) {
      fprintf(ofst, "%d: %f\n" , i, FigureArea<c->cont[i]>());
    }
  }

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


  //------------------------------------------------------------------------------
  // Прототип используемый для "рисования", предоставляемый
  // соответствующему клиенту
  void ContainerOut(Container* c, FILE* ofst);

  // Можно переименовать, а при необходимости и скрыть предыдущую функцию,
  // если вместо inline инкапсулировать ее реализацию.
  static inline void Draw(Container* c, FILE* ofst) {
    ContainerOut(c, ofst);
  }

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


  //==============================================================================
  // geometry-client.h - содержит интерфейс, поддерживающий клиента,
  // осуществляющего геометрические вычисления
  // В этом клиенте реализация инкапсулирована.
  //==============================================================================

  //------------------------------------------------------------------------------
  // Прототип используемый для вычисления периметров
  void CalcPerimeter(Container* c, FILE* ofst);
  //------------------------------------------------------------------------------
  // Прототип используемый для вычисления площадей
  void CalcArea(Container* c, FILE* ofst);

  //==============================================================================
  // geometry-client.c - содержит спрятанную реализацию клиента,
  // осуществляющего геометрические вычисления.
  //==============================================================================

  #include <stdio.h>
  #include "container.h"

  // Прототипы функций, используемых для организации вычислений
  void ContainerPerimeterOut(Container* c, FILE* ofst);
  void ContainerAreaOut(Container* c, FILE* ofst);


  //------------------------------------------------------------------------------
  // Вычисление периметров геометрическим клиентом
  void CalcPerimeter(Container* c, FILE* ofst) {
    ContainerPerimeterOut(c, ofst) ;
  }

  //------------------------------------------------------------------------------
  // Вычисление периметров геометрическим клиентом
  void CalcArea(Container* c, FILE* ofst) {
    ContainerAreaOut(c, ofst) ;
  }

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


  //------------------------------------------------------------------------------
  // Ввод параметров прямоугольника из файла
  void RectangleIn(Rectangle *r, FILE* ifst) {
    fscanf(ifst, "%d", &(r->x));
    fscanf(ifst, "%d", &(r->y));
  }
  //------------------------------------------------------------------------------
  // Ввод параметров треугольника из потока
  void TriangleIn(Triangle *t, FILE* ifst) {
    fscanf(ifst, "%d", &(t->a));
    fscanf(ifst, "%d", &(t->b));
    fscanf(ifst, "%d", &(t->c));
  }

  //------------------------------------------------------------------------------
  // Обобщающая функция для ввода параметров фигуры
  void FigureIn<Figure *f>(FILE* file) {}//= 0;

  //------------------------------------------------------------------------------
  // Обобщающая функция, которая по указателю на специализацию
  // и значению из файла создает специализированную фигуру
  Figure* FigureCreateUseTag<Figure *pFig>(int k) {
    printf("Unnown figure type for k = %d\n", k);
    exit(13);
  }
  //------------------------------------------------------------------------------
  // Прототип функции ввода прямоугольника
  void RectangleIn(Rectangle*, FILE*);
  // Прототип обобщенной функции ввода фигуры
  void FigureIn<Figure *f>(FILE* file);
  // Прототип обобщенной функции создания фигуры клонированием
  Figure* FigureCreateUseTag<Figure *pFig>(int k);

  //------------------------------------------------------------------------------
  // Ввод прямоугольника как фигуры
  void FigureIn<Figure.rect *f>(FILE* ifst) {
    RectangleIn(&(f->@), ifst);
  }
  //------------------------------------------------------------------------------
  // Создание прямоугольника как фигуры клонированием
  Figure* FigureCreateUseTag<Figure.rect *pFig>(int k) {
    if(k == 1) {
      // printf("k = %d: It is Rectangle\n", k);
      return create_spec(Figure.rect);
    } else {
      return NULL;
    }
  }
  //------------------------------------------------------------------------------
  // Ввод треугольника как фигуры
  void FigureIn<Figure.trian *f>(FILE* ifst) {
    TriangleIn(&(f->@), ifst);
  }
  //------------------------------------------------------------------------------
  // Создание треугольника как фигуры клонированием
  Figure* FigureCreateUseTag<Figure.trian *pFig>(int k) {
    if(k == 2) {
      // printf("k = %d: It is Triangle\n", k);
      return create_spec(Figure.trian);
    } else {
      return NULL;
    }
  }
  //------------------------------------------------------------------------------
  // Ввод параметров одной из фигур из файла
  Figure* FigureCreateAndIn(FILE* ifst) {
    int figSpecSize = get_spec_size(Figure);
    // printf("figSpecSize = %d\n", figSpecSize);
    Figure *sp;
    int k = 0;
    fscanf(ifst, "%d", &(k));
    for(int i = 1; i < figSpecSize; i++) {
      Figure* pFig = get_spec_ptr(Figure, i);
      sp = FigureCreateUseTag<pFig>(k);
      if(sp != NULL) break;
    }
    if(sp == NULL) {
      printf("Incorrect pointer to input figure\n");
      exit(13);
    }
    FigureIn<sp>(ifst);
    return sp;
  }

  //------------------------------------------------------------------------------
  // Инициализация контейнера
  void ContainerInit(Container *c) {
    c->len = 0;
  }
  //------------------------------------------------------------------------------
  // Очистка контейнера от элементов (освобождение памяти)
  void ContainerClear(Container *c) {
    for(int i = 0; i < c->len; i++) {
      free(c->cont[i]);
    }
    ContainerInit(c);
  }
  //------------------------------------------------------------------------------
  // Ввод содержимого контейнера из указанного потока
  void ContainerIn(Container* c, FILE* ifst) {
    while(!feof(ifst))  {
      if((c->cont[c->len] = FigureCreateAndIn(ifst)) != 0) {
        c->len++;
      }
    }
  }

Функция main, обеспечивая тестовый запуск клиентов через их интерфейсы.


  //------------------------------------------------------------------------------
  int main(int argc, char* argv[]) {
    if(argc !=3) {
      printf("incorrect command line!\nWaited: command infile outfile\n");
      return 1;
    }
    FILE* ifst = fopen(argv[1], "r");
    FILE* ofst = fopen(argv[2], "w");

    printf("Start\n");

    Container c;
    ContainerInit(&c);
    ContainerIn(&c, ifst);
    fclose(ifst);

    //============================================================================
    // Использование ограниченных интерфейсов клиентов
    //============================================================================

    fprintf(stdout, "\nDraw client.\n");
    Draw(&c, stdout);
    fprintf(ofst, "\nDraw client.\n");
    Draw(&c, ofst);

    fprintf(stdout, "\nCalulation of Perimeter by Geometry client.\n");
    CalcPerimeter(&c, stdout);
    fprintf(ofst, "\nCalulation of Perimeter by Geometry client.\n");
    CalcPerimeter(&c, ofst);

    fprintf(stdout, "\nCalulation of Area by Geometry client.\n");
    CalcArea(&c, stdout);
    fprintf(ofst, "\nCalulation of Area by Geometry client.\n");
    CalcArea(&c, ofst);

    ContainerClear(&c);

    fprintf(stdout, "Empty container.\n");
    ContainerOut(&c, stdout);

    fprintf(ofst, "Empty container.\n");
    ContainerOut(&c, ofst);
    fclose(ofst);

    printf("Stop\n");
    return 0;
  }

Резюме

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

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


Содержание