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

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

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

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


Содержание


ISP (Interface Segregation Principle).
Принцип разделения интерфейсов

При рассмотрении принципа разделения интрефейсов возникает ощущение déjà vu (дежавю). Где то уже делили интерфейсы. И в самом деле нечто подобное происходило с принципом единственной ответственности, когда класс должен был иметь только одну причину для изменения и, следовательно, ограничивать как свое поведение, так и свой интерфейс. Однако при аналогии восприятия имеется определенное различие в начинке. В случае SRP осуществляется разделение по предметным областям, а при использовании ISR. Хотя, если посмотреть на постановочные рисунки в чистой архитектуре, ощущение создается такое, что они отличаются только человечками вместо квадратиков [mr2018Мартин Р. Чистая архитектура. Искусство разработки программного обеспечения. — СПб.: Питер, 2018. — 352 с.].

Иллюстрация SRP Иллюстрация SRP

Принцип единственной ответственности: плохо --> хорошо

Иллюстрация SRP Иллюстрация SRP

Принцип разделения интерфейсов: плохо --> хорошо

Аналогия просматривается в стремлении избавиться от избыточности «жирных» интерфейсов, хотя изначальный посыл вроде бы разный. В обоих случаях предлагается разбить интерфейс класса на группы методов. А каждая группа должна обслуживать разнотипных клиентов. Одним клиентам нужна одна группа методов, другим – другая. Это из описания ISP в книгах по гибкой разрабоке [mr2004Мартин Р. Быстрая разработка программ: принципы, примеры, практика. – М.: Издательский дом “Вильямс”, 2004. – 752 с., mr2011Мартин Р., Мартин М. Принципы, паттерны и методики гибкой разработки на языке C#. – СПб.: Символ-Плюс, 2011. – 768 с.].

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

Туман войны

Многое еще покрыто мраком тумана войны...

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

Общение с Оракулом

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

Объектно-ориентированное решение с разделяемыми интерфейсами над жирными классами

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


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

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

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


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

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

  #include "client-draw-perimeter.h"

  // Вывод содержимого контейнера
  void ClientDrawPerimeter::DrawAndCalcPerimeters(std::ofstream &ofst) {
    ofst << "\n    ClientDrawPerimeter::DrawAndCalcPerimeters:\n";
    container.Out(ofst);
    // Вычисление периметров
    container.PerimeterOut(ofst);
  }

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

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

  #include "client-draw-area.h"

  // Вывод содержимого контейнера
  void ClientDrawArea::DrawAndCalcAreas(std::ofstream &ofst) {
    ofst << "\n    ClientDrawArea::DrawAndCalcAreas:\n";
    container.Out(ofst);
    // Вычисление площадей
    container.AreaOut(ofst);
  }

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


  // Организация вычисления периметра и вывода данных клиентом
  // работающим только с периметром
  ClientDrawPerimeter clientDrawPerimeter(c);
  std::cout << "ClientDrawPerimeter\n";
  clientDrawPerimeter.DrawAndCalcPerimeters(ofst);

  // Организация вычисления площади и вывода данных клиентом
  // работающим только с площадью
  ClientDrawArea clientDrawArea(c);
  std::cout << "ClientDrawArea\n";
  clientDrawArea.DrawAndCalcAreas(ofst);

А что в случае ППП?

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


  //==============================================================================
  // draw-area-client.h - содержит интерфейс, поддерживающий клиента,
  // осуществляющего вывод информации о фигурах и вычисление их площадей
  // В этом клиенте реализация инкапсулирована.
  //==============================================================================
  // Прототип используемый для рисовани и вычисления площадей
  void DrawAndCalcAreas(Container* c, FILE* ofst);

  //==============================================================================
  // draw-perimeter-client.h - содержит интерфейс, поддерживающий клиента,
  // осуществляющего вывод информации о фигурах и вычисление их периметров
  // В этом клиенте реализация инкапсулирована.
  //==============================================================================
  // Прототип используемый для рисования и вычисления периметров
  void DrawAndCalcPerimeters(Container* c, FILE* ofst);

  //==============================================================================
  // Реализации функций клиентов, использующих рисование и раздельное вычисление
  // периметров и площадей
  //==============================================================================

  //------------------------------------------------------------------------------
  // Рисование и вычисление периметров соответствующим клиентом
  void DrawAndCalcPerimeters(Container* c, FILE* ofst) {
    fprintf(ofst, "\n    DrawAndCalcPerimeters:\n");
    ContainerOut(c, ofst);
    ContainerPerimeterOut(c, ofst);
  }

  //------------------------------------------------------------------------------
  // Рисование и вычисление площадей соответствующим клиентом
  void DrawAndCalcAreas(Container* c, FILE* ofst) {
    fprintf(ofst, "\n    DrawAndCalcAreas:\n");
    ContainerOut(c, ofst);
    ContainerAreaOut(c, ofst);
  }

И небольшая добавка для тестирования в главную функцию.


  fprintf(stdout, "\nUsing Draw and Calc Perimeters client.\n");
  DrawAndCalcPerimeters(&c, stdout);
  fprintf(ofst, "\nUsing Draw and Calc Perimeters client.\n");
  DrawAndCalcPerimeters(&c, ofst);

  fprintf(stdout, "\nUsing Draw and Calc Areas client.\n");
  DrawAndCalcAreas(&c, stdout);
  fprintf(ofst, "\nUsing Draw and Calc Areas client.\n");
  DrawAndCalcAreas(&c, ofst);

Резюме

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


Содержание