нарушает ли использование методов по умолчанию в интерфейсах принцип разделения интерфейсов?

Я изучаю принципы SOLID, и интернет-провайдер заявляет, что:

       Clients should not be forced to depend upon interfaces that they do not use.

нарушает ли принцип использование методов по умолчанию в интерфейсах?

Я видел аналогичный вопрос, но я публикую здесь с примером, чтобы получить более ясную картину, если мой пример нарушает ISP. Скажем, у меня есть этот пример:

      public interface IUser{

void UserMenu();
String getID();

default void closeSession() {
    System.out.println("Client Left");
}

default void readRecords(){
System.out.println("User requested to read records...");
System.out.println("Printing records....");
System.out.println("..............");

}

}

со следующими классами, реализующими интерфейс IUser

      public class Admin implements IUser{

public String getID(){
    return "ADMIN";
}

public void handleUser()  {

    boolean sessionIsOpen = true;
    while (sessionIsOpen) {
        switch (Integer.parseInt(in.readLine())) {
            case 1 -> addNewUser();
            case 2 -> sessionIsOpen=false;
            default -> System.out.println("Invalid Entry");
        }
    }
    closeSession();

}

private void addNewUser() {

System.out.println("Adding New User..."); }
}

Класс редактора:

      public class Editor implements IUser{
public String getID(){
    return "EDITOR";
}

public void handleUser() {

    boolean sessionIsOpen=true;

    while (sessionIsOpen){
        switch (Integer.parseInt(in.readLine())) {
            case 1 -> addBook();
            case 2 -> readRecords();
            case 3 ->  sessionIsOpen=false;
            default ->
                System.out.println("Invalid Entry");
        }

    }

    closeSession();


}


private void addBook()  {

System.out.println("Adding New Book..."); }

}

Класс зрителя

      public class Viewer implements IUser{

public String getID(){
    return "Viewer";
}

public void handleUser() {

    boolean sessionIsOpen=true;

    while (sessionIsOpen){
        switch (Integer.parseInt(in.readLine())) {
            case 1 -> readRecords();
            case 2 ->  sessionIsOpen=false;
            default ->
                System.out.println("Invalid Entry");
        }

    }

    closeSession();


}
}

поскольку класс редактора и средства просмотра, использующий метод readRecords() и класс администратора, не обеспечивает реализацию этого метода, я реализовал его как метод по умолчанию в интерфейсе IUser, чтобы минимизировать повторение кода (принцип DRY). Нарушаю ли я принцип разделения интерфейса в приведенном выше коде, используя методы по умолчанию в IUser, потому что класс Admin не использует метод чтения?

2 ответа

нарушает ли принцип использование методов по умолчанию в интерфейсах?

Нет, если они используются правильно. Фактически, они могут помочь избежать нарушения ISP (см. Ниже).


Ваш пример использования методов по умолчанию нарушает ISP?

Да! Ну, наверное. Мы могли бы спорить о том, насколько серьезно это нарушает ISP, но это определенно нарушает множество других принципов и не является хорошей практикой в ​​программировании на Java.

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

Методы по умолчанию следует использовать для определения методов, которые:

  1. пользователи интерфейса, скорее всего, захотят позвонить (т. е. не разработчики)
  2. обеспечивать совокупную функциональность
  3. иметь реализацию, которая, вероятно, будет одинаковой для большинства (если не для всех) разработчиков интерфейса

Ваш пример, похоже, нарушает несколько условий.

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

      Admin admin = new Admin();
admin.closeSession();
admin.readRecords();

По-видимому, вы не хотите, чтобы это стало возможным не только для, но и для и слишком? Я бы сказал, что это своего рода нарушение ISP, потому что вы зависите от пользователей ваших классов, не вызывающих эти методы. Что касается класса, вы можете сделать его «безопасным», переопределив его и предоставив ему реализацию без операций, но это лишь подчеркивает гораздо более прямое нарушение ISP. Для всех других методов / реализаций, включая классы, которые действительно используются, вы облажались. Вместо того, чтобы думать об этом с точки зрения интернет-провайдера, я бы назвал это утечкой API или реализации: это позволяет использовать ваши классы способами, которые вы не планировали (и, возможно, захотите сломаться в будущем).

Второе условие, которое я сформулировал, может потребовать дальнейшего объяснения. Под совокупной функциональностью я имею в виду, что методы, вероятно, должны вызывать (прямо или косвенно) один или несколько абстрактных методов интерфейса. Если они этого не сделают, то поведение этих методов не может зависеть от состояния реализующего класса, и поэтому, вероятно, может быть статическим или полностью перемещенным в другой класс (т. Е. См. Принцип единой ответственности ) . Существуют примеры и варианты использования, когда можно ослабить это состояние, но о них следует подумать очень внимательно. В приведенном вами примере методы по умолчанию не являются агрегированными, но они выглядят как очищенный код для переполнения стека, так что, возможно, ваш «настоящий» код в порядке.

Спорный вопрос, считаются ли 2/3 исполнителей «большинством» в отношении моего третьего условия. Тем не менее, еще один способ думать о том, что вы должны знать заранее написание , реализующие классов , должны ли они этот метод с этой функциональностью. Как с уверенностью можно сказать, потребуются ли в будущем, если вам потребуется создать новый класс User, функциональность? В любом случае, это спорный вопрос, это условие действительно нужно учитывать только в том случае, если вы не нарушили первые 2.

Хорошее использование методов по умолчанию

В стандартной библиотеке есть примеры хороших применений. методы. Один был быjava.util.function.Function с его и методы. Это полезные функциональные возможности для пользователей функций, они (косвенно) используют абстрактные функции функции. метод, и, что важно, очень маловероятно, что реализующий класс когда-либо захочет их переопределить, за исключением, возможно, эффективности в некоторых узкоспециализированных сценариях.

Эти методы по умолчанию не нарушают ISP, поскольку классы, которые их реализуют, не нуждаются в их вызове или реализации. Может быть много случаев использования, когда конкретные экземпляры Function никогда не имеют своих вызывается метод, но это нормально - вы не нарушаете работу ISP, предоставляя полезные, но несущественные функции, до тех пор, пока вы не обременяете все эти варианты использования, заставляя их что-то делать с ним. В случае с Function предоставление этих методов как абстрактных, а не по умолчанию будет нарушением ISP, поскольку все реализующие классы должны будут добавлять свои собственные реализации, даже если они знают, что это вряд ли когда-либо будет вызвано.

Как добиться СУХОЙ, не нарушая «правил»?

Используйте абстрактный класс!

Абстрактные классы часто путались в дискуссиях о хорошей практике Java, потому что их часто неправильно понимали, неправильно использовали и злоупотребляли. Меня не удивило бы, если бы в ответ на это злоупотребление были опубликованы хотя бы некоторые руководства по передовому опыту программирования, такие как SOLID. Очень частая проблема, с которой я сталкивался, заключается в том, что абстрактный класс предоставляет реализацию "по умолчанию" для множества методов, которая затем переопределяется почти везде, часто путем копирования и вставки базовой реализации и изменения 1 или 2 строк. По сути, это нарушает мое третье условие для методов по умолчанию, описанных выше (которое также относится к любому методу для предполагаемого подкласса), и это происходит ОЧЕНЬ МНОГО.

Однако в этом сценарии абстрактные классы, вероятно, именно то, что вам нужно.

Что-то вроде этого:

      interface IUser {
    // Add all methods here intended to be CALLED by code that hods instances of IUser
    // e.g.:
    void handleUser();
    String getID();

    // If some methods only make sense for particular types of user,
    // they shouldn't be added.
    // e.g.:
    // NOT void addBook();
    // NOT void addNewUser();
}

abstract class AbstractUser implements IUser {
    // Add methods and fields here that will be USEFUL to most or
    // all implementations of IUser.
    //
    // Nothing should be public, unless it's an implementation of
    // one of the abstract methods defined on IUser.
    //
    // e.g.:
    protected void closeSession() { /* etc... */ }
}

abstract class AbstractRecordReadingUser extends AbstractUser {
    // Add methods here that are only USEFUL to a subset of
    // implementations of IUser.
    //
    // e.g.:
    protected void readRecords(){ /* etc... */ }
}

final class Admin extends AbstractUser {

    @Override
    public void handleUser() {
        // etc...
        closeSession();
    }

    public void addNewUser() { /* etc... */ }
}

final class Editor extends AbstractRecordReadingUser {

    @Override
    public void handleUser() {
        // etc...
        readRecords();
        // etc...
        closeSession();
    }

    public void addBook() { /* etc... */ }
}

final class Viewer extends AbstractRecordReadingUser {

    @Override
    public void handleUser() {
        // etc...
        readRecords();
        // etc...
        closeSession();
    }
}

Примечание: в зависимости от вашей ситуации могут быть лучшие альтернативы абстрактным классам, которые по-прежнему достигают DRY:

  • Если ваши общие вспомогательные методы не имеют состояния (т.е. не зависят от полей в классе), вы можете вместо этого использовать вспомогательный класс статических вспомогательных методов (см. Здесь пример).

  • Возможно, вы захотите использовать композицию вместо наследования абстрактных классов. Например, вместо создания как указано выше, у вас может быть:

            final class RecordReader {
        // Fields relevant to the readRecords() method
    
        public void readRecords() { /* etc... */ }
    }
    
    final class Editor {
        final RecordReader r = new RecordReader();
    
        @Override
        void handleUser() {
            // etc...
            r.readRecords();
            // etc...
        }
    }
    
    // Similar for Viewer
    

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

  • Вы можете просто вставить свой метод и избегайте наличия дополнительного абстрактного класса. Класс не обязан его вызывать, и пока метод , нет риска, что кто-то еще вызовет его (при условии, что ваши пакеты правильно разделены). Это не нарушает ISP, хотя может взаимодействовать с , Он не вынужден к. Он может делать вид, что метода не существует, и все в порядке!

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

Другие вопросы по тегам