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

摩萨里

我正在学习 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 类使用他们不使用的方法/接口。

用户31601

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

不,如果它们使用正确,则不会。事实上,它们可以帮助避免违反 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 或实现泄漏:它允许您的类以您不希望的方式使用(并且可能希望在未来中断)。

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

关于我的第三个条件,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(),也不是被迫的。它可以假装方法不存在,大家都没事!

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章