在接口中使用默认方法是否违反了接口隔离原则? 很好地使用默认方法如何在不违反“规则”的情况下实现 DRY?

问题描述

我正在学习 SOLID 原则,ISP 声明:

不应该强迫客户依赖他们所做的接口 不使用。

在接口中使用方法是否违反了这个原则?

我看到了一个类似的问题,但我在这里发布了一个示例,以便更清楚地了解我的示例是否违反了 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() 方法,而 Admin 类不提供该方法的实现,因此我将其作为 IUser 界面中的方法实现以最大程度地减少代码重复(DRY 原则)。

我在IUser中使用方法是否违反了上面代码中的接口隔离原则,因为Admin类没有使用read方法

有人可以解释一下吗,因为我认为我不会强迫 Admin 类使用他们不使用的方法/接口。

解决方法

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

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


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

是的!我们很可能会。我们可以就它违反 ISP 的程度进行辩论,但它肯定违反了许多其他原则,并且不是 Java 编程的好习惯。

问题是您使用默认方法作为实现类调用的东西。这不是他们的意图。

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

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

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

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

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

想必,您不希望这成为可能,不仅对于 Admin,而且对于 EditorViewer 也是如此?我认为这是对 ISP 的一种违反,因为您依赖于您的类的用户不调用这些方法。对于 Admin 类,您可以通过覆盖它并为其提供无操作实现来使 readRecords() 变得“安全”,但这只是突出了对 ISP 的更直接的违反。对于所有其他方法/实现,包括确实使用 readRecords() 的类,您都搞砸了。与其从 ISP 的角度考虑这一点,我将其称为 API 或实现泄漏:它允许您的类以您不希望的方式使用(并且可能希望在未来中断)。

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

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

很好地使用默认方法

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

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

如何在不违反“规则”的情况下实现 DRY?

使用抽象类!

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

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

像这样:

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

注意:根据您的情况,可能有更好的替代方法来替代仍然实现 DRY 的抽象类:

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

  • 您可能希望使用组合代替抽象类继承。例如,不是像上面那样创建 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 不允许多重继承的问题,如果您试图让多个抽象类包含不同的可选功能,并且某些最终类需要使用其中的几个,这将成为一个问题。但是,根据 readRecord() 方法需要与之交互的状态(即字段),可能无法将其完全分离到一个单独的类中。

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

,

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...