«1. Введение

В этом уроке мы обсудим принцип разделения интерфейса, один из принципов SOLID. Представляя «I» в «SOLID», разделение интерфейсов просто означает, что мы должны разбивать большие интерфейсы на более мелкие.

Таким образом, реализующие классы не должны реализовывать нежелательные методы.

2. Принцип разделения интерфейсов

Этот принцип был впервые определен Робертом С. Мартином как: «Клиентов нельзя принуждать зависеть от интерфейсов, которые они не используют».

Цель этого принципа — уменьшить побочные эффекты использования больших интерфейсов, разбивая интерфейсы приложений на более мелкие. Это похоже на принцип единой ответственности, где каждый класс или интерфейс служит одной цели.

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

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

3. Пример интерфейса и реализации

Давайте рассмотрим ситуацию, когда у нас есть интерфейс Payment, используемый реализацией BankPayment:

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() {
        // ...
    }
}

Для простоты, давайте проигнорируем реальную бизнес-реализацию этих методов.

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

4. Загрязнение интерфейса

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

Для разработки этой новой функции мы добавим новые методы в интерфейс Payment:

public interface Payment {
 
    // original methods
    ...
    void intiateLoanSettlement();
    void initiateRePayment();
}

Затем у нас будет реализация LoanPayment:

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() {
        // ...
    }

    @Override
    public void intiateLoanSettlement() {
        // ...
    }

    @Override
    public void initiateRePayment() {
        // ...
    }
}

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

Итак, что происходит с нашим классом BankPayment:

public class BankPayment implements Payment {

    @Override
    public void initiatePayments() {
        // ...
    }

    @Override
    public Object status() {
        // ...
    }

    @Override
    public List<Object> getPayments() {
        // ...
    }

    @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");
    }
}

Обратите внимание, что в реализации BankPayment теперь реализованы новые методы. А поскольку они ему не нужны и для них нет логики, он просто выдает исключение UnsupportedOperationException. Здесь мы начинаем нарушать принцип.

В следующем разделе мы увидим, как мы можем решить эту проблему.

5. Применение принципа

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

Давайте разберем интерфейс для каждого типа оплаты. Текущая ситуация:

Обратите внимание на диаграмму классов и на интерфейсы в предыдущем разделе, что методы status() и getPayments() требуются в обеих реализациях. С другой стороны, метод initialPayments() требуется только для BankPayment, а методы InitialLoanSettlement() и инициироватьRePayment() — только для LoanPayment.

Разобравшись с этим, давайте разделим интерфейсы и применим принцип разделения интерфейсов. Таким образом, у нас появился общий интерфейс:

public interface Payment {
    Object status();
    List<Object> getPayments();
}

И еще два интерфейса для двух видов платежей:

public interface Bank extends Payment {
    void initiatePayments();
}
public interface Loan extends Payment {
    void intiateLoanSettlement();
    void initiateRePayment();
}

public class BankPayment implements Bank {

    @Override
    public void initiatePayments() {
        // ...
    }

    @Override
    public Object status() {
        // ...
    }

    @Override
    public List<Object> getPayments() {
        // ...
    }
}

И соответствующие реализации, начиная с BankPayment:

public class LoanPayment implements Loan {

    @Override
    public void intiateLoanSettlement() {
        // ...
    }

    @Override
    public void initiateRePayment() {
        // ...
    }

    @Override
    public Object status() {
        // ...
    }

    @Override
    public List<Object> getPayments() {
        // ...
    }
}

И, наконец, наша пересмотренная реализация LoanPayment:

Теперь давайте рассмотрим новую диаграмму классов:

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

6. Заключение

«В этом руководстве мы рассмотрели простой сценарий, в котором мы сначала отклонились от следования принципу разделения интерфейса, и увидели проблемы, вызванные этим отклонением. Затем мы показали, как правильно применять принцип, чтобы избежать этих проблем.

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

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

Как всегда, код доступен на GitHub.