在接口中使用默认方法是否违反原则?
不,如果正确使用它们就不会。事实上,它们可以帮助避免违反 ISP(见下文)。
您使用默认方法的示例是否违反了 ISP?
是的!我们可能会。我们可以就它到底违反 ISP 的严重程度进行辩论,但它肯定违反了许多其他原则,并且不是 Java 编程的良好实践。
问题是您使用默认方法作为实现类调用的方法。那不是他们的意图。
应使用默认方法来定义以下方法:
-
users接口的成员可能希望调用(即不是实现者)
- provide 总计的功能性
- 对于接口的大多数(如果不是全部)实现者来说,有一个可能相同的实现
您的示例似乎违反了几个条件。
第一个条件的存在有一个简单的原因:Java 接口上的所有可继承方法都是公共的,因此它们总是can由接口的用户调用。举一个具体的例子,下面的代码可以正常工作:
Admin admin = new Admin();
admin.closeSession();
admin.readRecords();
想必,您不希望这种情况成为可能,不仅仅是为了Admin
, 但对于Editor
and Viewer
也?我认为这是对 ISP 的违反,因为你依赖于你的类的用户不打电话那些方法。为了Admin
类,你可以做readRecords()
通过覆盖它并给它一个无操作实现来“安全”,但这只是凸显了对 ISP 的更直接的侵犯。对于所有其他方法/实现,包括do利用readRecords()
, 你完蛋了。我不会从 ISP 的角度来考虑这一点,而是将其称为 API 或实现泄漏:它允许您的类以您不希望的方式使用(并且可能希望在将来打破)。
我所说的第二个条件可能需要进一步解释。经过总计的功能,我的意思是这些方法可能应该调用(直接或间接)接口上的一个或多个抽象方法。如果他们不这样做,那么这些方法的行为不可能依赖于实现类的状态,因此可能是静态的,或者完全移动到不同的类中(即参见单一职责原则 https://en.wikipedia.org/wiki/Single-responsibility_principle)。有一些示例和用例可以放宽此条件,但应非常仔细地考虑它们。在您给出的示例中,默认方法不是聚合的,但它看起来像是为了堆栈溢出而清理的代码,所以也许您的“真实”代码没问题。
对于我的第三个条件,2/3 的实施者是否算作“大多数”是有争议的。然而,另一种思考方式是你应该知道提前编写实现类,无论它们是否应该具有具有该功能的方法。您如何确定将来是否需要创建一个新的 User 类,它们将需要以下功能readRecords()
?无论哪种方式,这都是一个有争议的问题,因为只有在您没有违反前两个条件的情况下才真正需要考虑这个条件。
充分利用默认方法
标准库中有一些很好用的例子default
方法。一个是java.util.function.Function https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html以其andThen(...)
and compose(...)
方法。这些是有用的功能users函数,它们(间接)利用函数的抽象apply(...)
方法,重要的是,实现类不太可能希望覆盖它们,除非是为了某些高度专业化场景中的效率。
这些默认方法的作用是not违反 ISP,因为类实现Function
无需调用或覆盖它们。可能有许多用例中 Function 的具体实例永远不会有它们的andThen(...)
方法被调用,但这很好——你不会通过提供有用但非必要的功能来破坏 ISP,只要你不妨碍所有这些用例forcing他们用它做点什么。对于 Function,将这些方法作为抽象方法而不是默认方法提供would违反 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();
}
}
Note:根据您的情况,可能有更好的抽象类替代方案,但仍然实现 DRY:
-
如果您的常用辅助方法是无状态的(即不依赖于类中的字段),您可以使用静态辅助方法的辅助类来代替(请参阅here https://www.geeksforgeeks.org/java-helperclass/举个例子)。
-
您可能希望使用作品而不是抽象类继承。例如,不是创建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 不允许多重继承的问题,如果您尝试让多个抽象类包含不同的可选功能,并且某些最终类需要使用其中的多个抽象类,那么多重继承就会成为一个问题。然而,取决于什么state(即字段)readRecord()
方法需要交互,因此可能无法将其干净地分离到单独的类中。
-
你可以把你的readRecords()
中的方法AbstractUser
并避免使用额外的抽象类。这Admin
类没有义务调用它,只要该方法是protected
,不存在其他人调用它的风险(假设您已正确分离包裹)。这并不违反 ISP,即使Admin
can与。。。相互作用readRecords()
,这不是forced到。它可以假装该方法不存在,并且每个人都很好!