2

我正在学习 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 原则)。

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

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

4

2 回答 2

3

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

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


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

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

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

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

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

您的示例似乎打破了几个条件。

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

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

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

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

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

很好地使用默认方法

default良好使用方法的标准库中有示例。一个是java.util.function.Function它的andThen(...)compose(...)方法。这些对 Functions 的用户来说是有用的功能,它们(间接地)使用 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:

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

  • 您可能希望使用组合而不是抽象类继承。例如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(),也不是被迫的。它可以假装那个方法不存在,大家都没事!

于 2021-07-23T13:00:45.173 回答
0

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

于 2021-07-21T10:27:19.763 回答