|
© 2026
Александр Легалов
Содержание
ISP (Interface Segregation Principle). Принцип разделения интерфейсов
При рассмотрении принципа разделения интрефейсов возникает ощущение déjà vu (дежавю). Где то уже делили интерфейсы. И в самом деле нечто подобное происходило с принципом единственной ответственности, когда класс должен был иметь только одну причину для изменения и, следовательно, ограничивать как свое поведение, так и свой интерфейс. Однако при аналогии восприятия имеется определенное различие в начинке. В случае SRP осуществляется разделение по предметным областям, а при использовании ISR. Хотя, если посмотреть на постановочные рисунки в чистой архитектуре, ощущение создается такое, что они отличаются только человечками вместо квадратиков [mr2018Мартин Р. Чистая архитектура. Искусство разработки программного обеспечения. — СПб.: Питер, 2018. — 352 с.].
Принцип единственной ответственности: плохо --> хорошо
Принцип разделения интерфейсов: плохо --> хорошо
Аналогия просматривается в стремлении избавиться от избыточности «жирных» интерфейсов, хотя изначальный посыл вроде бы разный. В обоих случаях предлагается разбить интерфейс класса на группы методов. А каждая группа должна обслуживать разнотипных клиентов. Одним клиентам нужна одна группа методов, другим – другая. Это из описания 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, выливается в формирование соответствующих заголовочных файлов, учитывающих ограничение функциональности отдельных клиентов. В целом не происходит ничего интересного.
Содержание
|