问题描述
我正在学习 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 编程的好习惯。
问题是您使用默认方法作为实现类调用的东西。这不是他们的意图。
应该使用默认方法来定义以下方法:
- 界面的用户可能希望调用(即不是实现者)
- 提供聚合功能
- 有一个对于大多数(如果不是全部)接口实现者来说可能是相同的实现
您的示例似乎违反了几个条件。
存在第一个条件的原因很简单:Java 接口上的所有可继承方法都是公共的,因此它们总是可以被接口的用户调用。举一个具体的例子,下面的代码工作正常:
Admin admin = new Admin();
admin.closeSession();
admin.readRecords();
想必,您不希望这成为可能,不仅对于 Admin
,而且对于 Editor
和 Viewer
也是如此?我认为这是对 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 原则。但是您不必严格遵循所有可靠的原则,因为这会使开发复杂化。