Рубрика:
Разработка /
Принципы проектирования
|
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|
ВИЗИТКА
Ольга Федорова, технический лидер «Альфа банка»
Interface Segregation Principle Принцип разделения интерфейсов в проектировании приложений
Эта статья из серии «SOLID» посвящена четвертому принципу проектирования приложений – Interface Segregation или «разделения интерфейсов». Как обычно, начнем с оригинальной формулировки Роберта Мартина, которая звучит следующим образом: программные сущности не должны зависеть от методов, которые они не используют.
Чтобы это продемонстрировать, я хочу обратиться к реализации коллекции в языке Java. Как минимум, потому что это хороший пример того, что даже на уровне языков инженерные принципы не всегда реализованы должным образом и не надо относится к системным библиотекам как к идеалу инженерной индустрии. Знатоки JVM стека могут мне возразить, что для такой реализации были свои исторические причины, я не буду с ними спорить, но давайте посмотрим, что из этого вышло.
Для тех, кто не знаком с Java, я прикладываю картинку со структурой класса Collections, в принципе, для общего понимания его достаточно.
Итак, вы разрабатываете библиотеку, которая отправляет сообщения через TCP- сеть (разумеется, с использованием того, что выше, это же базовые возможности языка). Она достаточно умная, чтобы отправлять их ассинхронно, согласно приоритету, и накапливать смс в буфер, если все сразу не пролезли в сеть. Соответственно, при добавлении СМС в очередь у вас достаточно много логики, вы должны это дело залогировать, найти правильное место в очереди согласно приоритетам и т.д. Все работает, все счастливы.
В один непрекрасный день вы сталкиваетесь с необходимостью дать клиентскому коду возможность пробежаться по СМС, стоящим в очереди. Конечно же, вы не можете допустить чтобы кто-то модифицировал состояние коллекции, поэтому внутри библиотеки тихонько вызывается Collection.unmodifiedSet(), а вот уже результат вы отдаете клиентскому коду.
Что делает клиентский код? Видит метод add и воодушевленно вызывает его. Не всегда обоснованно, зачастую просто потому, что «почему бы и нет?». Однако, вместо ожидаемого результата, он встретит ошибку.
Здесь мы и вспоминаем оригинальную формулировку Interface Segregation. Суть заключается в том, чтобы клиентский код зависел только от тех частей системы, которые он реально использует. Если клиентскому коду не требуется использовать какие-то функции или возможности, то лучше вообще не предоставлять ему доступ к ним. Это позволяет избежать ненужных зависимостей и сделать систему более гибкой и поддерживаемой.
Рассмотрим другой пример и тоже из Java. Дабы у вас не сложилось предвзятое отношение к этому языку, сразу оговорюсь, что такие проблемы есть и в C#, и в других языках. Итак, интерфейс Collection:
Какие недостатки есть у исходного интерфейса? Как минимум, его можно было бы разделить на несколько более специализированных интерфейсов. Например:
- методы модификации коллекции: add, addAll, clear и т.д.;
- методы итерации и доступа: iterator, isEmpty и пр.;
- конвертеры toArray, toStream;
- наследование java object.
Это и было сделано, в частности, в языке Kotlin, который, хоть и является в общественном сознании в некотором роде синтаксическим сахаром для Java, кратно улучшил ряд концепций исходного языка.
Давайте рассмотрим другой пример. Предположим, у нас есть интерфейс Payment с определенным набором методов. Кто-то реализует этот интерфейс, и, на первый взгляд, вся эта схема выглядит неплохо.
public interface Payment { void initiatePayments(); Object status(); List<Object> getPayments(); } public class BankPayment implements Payment { @Override public void initiatePayments() { // реализация } @Override public Object status() { // реализация } @Override public List<Object> getPayments() { // реализация } }
Предположим, по мере развития программы, в какой-то момент появляется LoanPayment со своими методами, весьма похожими на те, что уже есть в Payment, за исключением небольшого отличия. И в целом, на первый взгляд, можно согласится на то, чтобы класс имплементировал интерфейс, хотя наличие Unsupported Operation уже вызывает сомнения.
public class LoanPayment implements Payment { @Override public void initiatePayments() { throw new UnsupportedOperationException("This is not a bank payment"); } @Override public Object status() { // реализация } @Override public List<Object> getPayments() { // реализация } }
Продукт продолжает эволюционировать и внезапно возникает необходимость расширить оригинальный интерфейс Payment, добавив к нему еще два дополнительных метода.
public interface Payment { void initiatePayments(); Object status(); List<Object> getPayments(); void intiateLoanSettlement(); void initiateRePayment(); }
Теперь у нас возникает неприятная ситуация: если раньше банковский платеж не поддерживал только один из оригинальных трех методов, то теперь он не поддерживает еще и новые методы интерфейса Payment.
public class BankPayment implements Payment { @Override public void initiatePayments() { // реализация } @Override public Object status() { // реализация } @Override public List<Object> getPayments() { // реализация } // Новые методы, добавленные к интерфейсу Payment @Override public void intiateLoanSettlement() { throw new UnsupportedOperationException("This is not a loan payment"); } @Override public void initiateRePayment() { throw new UnsupportedOperationException("This is not a loan payment"); } }
Это уже делает код очень неуклюжим.
Как мы видели ранее, во-первых, это может привести к неудачным последствиям, когда клиенты кода начинают использовать функциональность, которую они не должны использовать.
Во-вторых, тестировать такой класс становится сложнее даже на уровне юнит-тестов.
В-третьих, это очевидно не финальная версия класса, и по мере развития кодовой базы поддержка такого кода будет только усложняться.
В данном случае, возможно решить проблему путем наследования и вынесения общих методов в более верхнеуровневый интерфейс, который будет расширен оригинальным интерфейсом.
Кроме того, это подходит под принцип open-closed, о котором мы говорили ранее. Мы не модифицируем существующий код, а расширяем его. В результате код становится более чистым и соблюдает принципы SOLID.
public interface Payment { Object status(); List<Object> getPayments(); } public interface Bank extends Payment { void initiatePayments(); } public interface Loan extends Payment { void intiateLoanSettlement(); void initiateRePayment(); }
Такой подход позволяет избежать проблему неподдерживаемых методов, упрощает тестирование, защищает клиента, который использует код от нежелательных последствий и делает архитектуру более гибкой.
К слову, напомню, что наличие интерфейсов в программе само по себе это хорошая практика, потому что они уменьшают связность кода, что в свою очередь снижает риск превращения продукта в Big Ball of Mud. Да, можно сказать, что большое количество интерфейсов не есть хорошо, потому что это отнимает ресурсы памяти и все такое.
С одной стороны, это правда. Но, с другой стороны, в современном мире мы чаще страдаем не от нехватки железа, а от отсутствия общего понимания происходящего в программе спустя несколько лет активной разработки. Поэтому интерфейсы – это ключевой инструмент в поддержании чистоты и структурированности кода. А правильная организация интерфейсов (в том числе, следование принципу Interface Segregation) – это залог надежной архитектуры приложения.
- Федорова О. Single Responsibility Principle. «Системный администратор». 2024. №1-2.
- Федорова О. Open Closed Principle. «Системный администратор». 2024. №1-2.
- Федорова О. Принцип подстановки Барбары Лисков, и почему наследующий класс должен дополнять, а не замещать поведение базового класса. «Системный администратор». 2024. №3.
Ключевые слова: SOLID, ISP, Interface Segregation Principle, Роберт Мартин
Подпишитесь на журнал
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|