我正在学习 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 或实现泄漏:它允许您的类以您不希望的方式使用(并且可能希望在未来中断)。
我所说的第二个条件可能需要进一步解释。通过聚合功能,我的意思是方法可能应该调用(直接或间接)接口上的一个或多个抽象方法。如果他们不这样做,那么这些方法的行为不可能依赖于实现类的状态,因此可能是静态的,或者完全移动到不同的类中(即参见单一职责原则) . 有一些例子和用例可以放宽这个条件,但应该非常仔细地考虑它们。在您给出的示例中,默认方法不是聚合的,但为了堆栈溢出,它看起来像是经过消毒的代码,所以也许您的“真实”代码没问题。
关于我的第三个条件,2/3 的实施者是否算作“最多”是有争议的。然而,另一种思考方式是,您应该在编写实现类之前知道它们是否应该具有具有该功能的方法。您如何确定将来是否需要创建一个新的 User 类,他们将需要readRecords()
? 无论哪种方式,这是一个有争议的问题,因为只有在您没有违反前 2 条的情况下才真正需要考虑这种情况。
标准库中有良好使用default
方法的示例。一个是java.util.function.Function
它的andThen(...)
和compose(...)
方法。这些对于Functions 的用户来说是有用的功能,它们(间接地)利用了 Function 的抽象apply(...)
方法,重要的是,实现类极不可能希望覆盖它们,除非可能是为了在某些高度专业化的场景中提高效率.
这些默认方法不违反 ISP,因为实现的类Function
不需要调用或覆盖它们。可能有很多用例,其中 Function 的具体实例从未andThen(...)
调用过它们的方法,但这很好——你不会通过提供有用但非必要的功能来破坏 ISP,只要你不妨碍所有这些用例通过强迫他们用它做某事。在 Function 的情况下,将这些方法作为抽象而不是默认提供将违反 ISP,因为所有实现类都必须添加自己的实现,即使他们知道它不太可能被调用。
使用抽象类!
抽象类在关于良好 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] 删除。
我来说两句