Andriy Shyrokoryadov

.Net developer, data scientist

Принципы SOLID - вопрос №7 на собеседование по программированию

Текст к видео "SOLID - принцип сегрегации интерфейса" на канале YouTube

Приветствую Вас на моём канале. Сегодня мы рассмотрим последний в списке, но не последний по значению принцип SOLID, принцип I, который в англоязычной литературе называется Interface Segregation Principle, а в литературе русскоязычной можно встретить определение «принцип разделения интерфейса». По моему скромному мнению, у данного принципа есть две интересные особенности. Я выделил эти особенности на основании моего практического опыта. Первая особенность – если принцип разделения интерфейса нарушен то, как правило, это также говорит о том, что принцип единственной ответственности также нарушен. Чуть позже на практическом примере я покажу как это работает. Второй особенностью данного принципа является то, что изначально код может соответствовать данному принципу, но со временем код может начать д еградировать и в конечном счете перестанет соответствовать принципу разделения интерфейса. Об мы также поговорим в сегодняшнем видео.

Начнем с определения, которое довольно простое: «Программные сущности не должны зависеть от методов, которые они не используют.». Хоть данное определение довольно простое пока не понятно, о чем идет речь. Пока мы можем выделить две части в этом определении – существуют некоторые программные сущности или, проще, классы с одной стороны и некоторые методы, которые могут не использовать с другой.

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

Вашему вниманию предлагается интерфейс IConnector, который содержит большое количество членов. Я не определял все члены, добавив комментарий, что всех элементов интерфейса может быть на пару десятков больше. Я думаю, моё намерение здесь понятно – показать большой интерфейс. Давайте ознакомимся более подробно с существующими элементами данного интерфейса. У нас есть несколько элементов, которые работают с классом User – это немного напоминает шаблон проектирования «Репозиторий». Далее есть пара методов для работы с классом Connection, судя по названиям методов и типу возвращаемых значений. Также есть пара методов, по названиям которых тяжело определить, что данные методы могут делать. По сути, анализируя данный интерфейс мы выделили 3 ответственности: работа с классом User, работа с классом Connection, и еще одна зона ответственности, которая пока непонятна. Это то, о чем я говорил в начале видео – пока неочевидно, что принцип разделения интерфейса нарушен (хотя на это может указывать большой интерфейс), однако уже можно точно сказать, что класс, который будет реализовывать данный интерфейс будет нарушать принцип единственной ответственности.

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

Казалось бы, несмотря на плохой большой интерфейс, мы справились с задачей и кое-как разделили зоны ответственности. Но есть определенная проблема. Давайте представим ситуацию – в функциональности, связанной с соединением, требуется изменения. В интерфейс IConnector будет добавлен новый метод Send, который будет отправлять некоторые данные используя текущее соединение. Интерфейс IConnector влияет на 3 класса, но фактически метод Send имеет смысл только для класса ConnectionHandler. Для остальных 2 реализаций, UserRepo и DataManager, будут добавлены реализации метода Send по умолчанию, скорей всего это будут пустые методы. А теперь вернемся к определению принципа разделения интерфейсов «Программные сущности не должны зависеть от методов, которые они не используют.». В нашем случае программные сущности UserRepo и DataManager зависят от методов, которые они не используют. В данный момент у нас проблема с методом Send, а изначально у нас была проблема с интерфейсом IConnector вообще.

Как нам решить проблему с интерфейсом IConnector? Смотрим на название нашего принципа - принцип разделения интерфейса. Нам необходимо разделить интерфейс IConnector. Разделим его на три части по 3 трём функциональным ответственностям. В итоге мы получили 3 независимые интерфейса и 3 независимы имплементации. Если по каким-то причинам нам необходимо оставить интерфейс IConnector, то можно создать такой интерфейс, который будет наследовать 3 новые интерфейсы. Класс, который будет реализовывать новый интерфейс IConnector, будет классом – фасадом, которые принимает 3 зависимости и делегируют запросы по интерфейсу IConnector к своим зависимостям.

Я думаю, сейчас уже понятно, что означает принцип разделения интерфейса, какие признаки нарушения данного признака, а также как можно исправить код с нарушением данного принципа. Непонятно только одно, как получается, что код, который не нарушает данный принцип со временем, деградирует и возникает нарушение принципа разделения интерфейса. Одна из причин этого явления описана в книге Роберта Мартина «Принципы, паттерны и методики гибкой разработки на языке C#». Сразу замечу, что это не реклама данной книги. Так вот. В данной книге автор определили так называемый «принцип разбитого окна». Если у нас есть какой-то дом, который забросили, то изначально такой дом может быть в хорошем состоянии. Однако может быть так что хулиганы разобьют в этом доме окно. Сначала будет только одно разбитое окно. Другие хулиганы увидят такой дом и распишут граффити его стены. Третья группа хулиганов разобьют еще пару окон. Потом кто-то выломает дверь в дом. В итоге дом, который изначально был в хорошем состоянии, станет непригодным для жилья.

Также и с кодом. Будет достаточно, что один из программистов добавить неподходящий метод в определенный интерфейс и никто этого не заметит. Вроде бы всё работает, но код испорчен. Далее через некоторое время другой программист увидит этот метод и подумает: «Раз это было добавлено здесь, то наверно я могу сюда также добавить еще что-нибудь». В итоге изначальный интерфейс деградирует и через некоторое времени его суть становиться размытой, и он не выполняет первоначально поставленных задач или выполняет, но плохо. Одна ошибка, как снежный ком, становиться большой проблемой через некоторое время. Так мы приходим к большим интерфейсам в которых много чего происходит.

Мне кажется темя принципа I уже раскрыта и мне пора заканчивать моё повествование. Спасибо за внимание. Если Вам понравилось данное видео, то вы знаете, что делать: лайк, подписка, колокольчик, комментарий. До новых встреч на канале.

Пример кода из видео на GitHub

Для открытия файла проекта необходимо Visual Studio 2019.