在接口中使用默认方法是否违反了接口隔离原则?

does using default methods in interfaces violates Interface segregation principle?

我正在学习 SOLID 原则,ISP 指出:

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

}

使用以下 classes 实现 IUser Interface

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

编辑器Class:

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

观看者Class

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();
    }
}

由于编辑器和查看器 class 使用 readRecords() 方法并且 Admin class 没有提供该方法的实现,我将其实现为 IUser 界面中的默认方法以最大程度地减少代码重复(DRY 原则)。

我上面的代码是不是因为Adminclass没有使用read方法,所以在IUser中使用默认方法违反了接口隔离原则?

有人可以解释一下吗,因为我认为我没有强迫管理员 class 使用他们不使用的 methods/interfaces。

我认为这违反了 ISP 的原则。但是你不必严格遵循所有坚实的原则,因为这会使开发复杂化。

does using default methods in interfaces violate the principle?

不,如果使用得当则不会。事实上,它们可以帮助避免违反 ISP(见下文)。


您使用默认方法的示例是否违反了 ISP?

是的!我们很可能会。我们可以就它到底有多严重地违反 ISP 进行辩论,但它肯定违反了一系列其他原则,并且不是 Java 编程的好习惯。

问题是您正在使用默认方法作为实现 class 调用的方法。那不是他们的意图。

应使用默认方法来定义以下方法:

  1. 接口的用户 可能希望调用(即不是实现者)
  2. 提供聚合功能
  3. 对于接口的大多数(如果不是全部)实现者来说,有一个可能相同的实现

您的示例似乎违反了几个条件。

存在第一个条件的原因很简单:Java 接口上的所有可继承方法都是 public,因此它们总是 可以 被用户调用的界面。举一个具体的例子,下面的代码工作正常:

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

据推测,您不希望这成为可能,不仅是 Admin,还有 EditorViewer?我认为这是对 ISP 的一种违反,因为您依赖于 classes 的用户而不是调用 这些方法。对于 Admin class,您可以通过覆盖它并为其提供空操作实现来使 readRecords() 'safe',但这只会突出更直接的 ISP 违规行为。对于所有其他 methods/implementations,包括 使用 readRecords() 的 class,你完蛋了。与其从 ISP 的角度考虑这一点,不如将其称为 API 或实现泄漏:它允许您的 classes 以您不希望的方式使用(并且可能希望破坏将来)。

我说的第二个条件可能需要进一步解释。通过 聚合 功能,我的意思是这些方法可能应该调用(直接或间接)接口上的一个或多个抽象方法。如果他们不这样做,那么这些方法的行为就不可能依赖于实现 class 的状态,因此可能是静态的,或者完全移入不同的 class(即见 Single-responsibility principle)。有一些示例和用例可以放宽此条件,但应该非常仔细地考虑它们。在您给出的示例中,默认方法不是聚合的,但它看起来像是为了 Stack Overflow 而经过清理的代码,所以也许您的“真实”代码没问题。

关于我的第三个条件,2/3 的实施者是否算作“最多”是值得商榷的。但是,另一种思考方式是您应该提前知道编写实现classes 是否应该具有具有该功能的方法。您怎么能确定将来如果您需要创建一个新的 class 用户,他们将需要 readRecords() 的功能?无论哪种方式,这都是一个有争议的问题,因为只有在您没有违反前 2 条的情况下才真正需要考虑这种情况。

很好地使用默认方法

标准库中有很好使用 default 方法的示例。一种是 java.util.function.Function 及其 andThen(...)compose(...) 方法。这些对于 users 的函数来说是有用的功能,它们(间接地)利用函数的抽象 apply(...) 方法,重要的是,实现 class 永远希望覆盖它们,除非在某些高度专业化的场景中可能为了提高效率。

这些默认方法违反ISP,因为实现Function的classes不需要调用或覆盖它们。可能有许多用例,其中 Function 的具体实例永远不会调用其 andThen(...) 方法,但这很好 - 只要您不妨碍,您就不会通过提供有用但非必要的功能来破坏 ISP 强迫他们用它做一些事情。在 Function 的情况下,将这些方法提供为抽象而不是默认 违反 ISP,因为所有实现 classes 都必须添加自己的实现,即使他们知道它是不太可能被调用。

如何在不破坏 'the rules' 的情况下实现 DRY?

使用摘要 class!

摘要 classes 在讨论良好 Java 实践时经常被大便,因为它们经常被误解、误用和滥用。如果至少有一些像 SOLID 这样的编程最佳实践指南针对这种滥用而发布,我也不会感到惊讶。我见过的一个非常常见的问题是抽象 class 为大量方法提供“默认”实现,然后几乎在所有地方都被覆盖,通常是通过复制粘贴基本实现并更改 1 或 2 行。从本质上讲,这打破了我在上面的默认方法上的第三个条件(这也适用于 intended-to-be-subclassed 类型的任何方法),并且它经常发生。

但是,在这种情况下,抽象 classes 可能正是您所需要的。

像这样:

interface IUser {
    // Add all methods here intended to be CALLED by code that holds
    // 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();
    }
}

注意: 根据您的情况,可能有更好的抽象 classes 替代品,但仍然实现 DRY:

  • 如果您的常用辅助方法是无状态的(即不依赖于 class 中的字段),您可以使用辅助 class 静态辅助方法(有关示例,请参见 here

  • 您可能希望使用 组合 而不是抽象 class 继承。例如,不是像上面那样创建 AbstractRecordReadingUser,而是可以:

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

    这避免了 Java 不允许多重继承的问题,如果你试图让多个抽象 classes 包含不同的可选功能,以及一些最终的,这将成为一个问题classes 需要使用其中的几个。但是,根据 state(即字段),readRecord() 方法需要与之交互,可能无法将其完全分离到单独的 class 中.

  • 您可以将 readRecords() 方法放在 AbstractUser 中,避免使用额外的摘要 class。 Admin class 没有义务调用它,只要该方法是 protected,就没有其他人调用它的风险(假设您的包裹已正确分离) .这并不违反 ISP,因为即使 Admin 可以 readRecords() 交互,也不会 强制 交互。它可以假装方法不存在,大家没事!