jml*_*jml 44 java liskov-substitution-principle
鉴于以下课程:
class Example implements Interface1, Interface2 {
...
}
Run Code Online (Sandbox Code Playgroud)
当我使用Interface1以下方法实例化类时:
Interface1 example = new Example();
Run Code Online (Sandbox Code Playgroud)
...然后我只能调用Interface1方法,而不是Interface2方法,除非我施放:
((Interface2) example).someInterface2Method();
Run Code Online (Sandbox Code Playgroud)
当然,为了使这个运行时安全,我还应该用一个instanceof检查包装它:
if (example instanceof Interface2) {
((Interface2) example).someInterface2Method();
}
Run Code Online (Sandbox Code Playgroud)
我知道我可以有一个扩展两个接口的包装器接口,但最后我可能会有多个接口来满足可以由同一个类实现的所有可能的接口排列.有问题的接口并不会自然地相互扩展,因此继承似乎也是错误的.
instanceof/ cast方法是否会破坏LSP,因为我正在询问运行时实例以确定其实现?
无论我使用哪种实现,似乎都会在糟糕的设计或使用中产生一些副作用.
Mic*_*ael 35
我知道我可以有一个扩展两个接口的包装器接口,但是我最终可能会有多个接口来满足可以由同一个类实现的所有可能的接口排列
我怀疑如果你发现你的很多类实现了不同的接口组合,那么:你的具体类做得太多了; 或者(不太可能)你的界面太小而且太专业,以至于无法单独使用.
如果你有充分的理由要求某些代码既需要a Interface1又需要a Interface2然后绝对继续下去并制作一个扩展两者的组合版本.如果你很难想到一个合适的名称(不,不FooAndBar),那么这就是你的设计错误的指标.
绝对不要依赖铸造任何东西.它应该仅作为最后的手段使用,通常仅用于非常具体的问题(例如序列化).
我最喜欢和最常用的设计模式是装饰模式.因此,我的大多数类只会实现一个接口(除了更通用的接口,例如Comparable).我会说,如果你的类经常/总是实现多个接口,那么这就是代码味道.
如果您要实例化对象并在同一范围内使用它,那么您应该只是写作
Example example = new Example();
Run Code Online (Sandbox Code Playgroud)
正是这样很明显(我不知道这是否是你在暗示什么),在任何情况下,你应该永远是写这样的事情:
Interface1 example = new Example();
if (example instanceof Interface2) {
((Interface2) example).someInterface2Method();
}
Run Code Online (Sandbox Code Playgroud)
jbx*_*jbx 25
您的类可以很好地实现多个接口,并且它没有违反任何OOP原则.相反,它遵循界面隔离原则.
为什么你会拥有其中类型的东西的情况是混乱Interface1有望提供someInterface2Method().这就是你的设计错误的地方.
以一种略微不同的方式思考:想象一下,你有另一种方法,void method1(Interface1 interface1).它不能指望interface1也是一个例子Interface2.如果是这种情况,参数的类型应该是不同的.你所展示的例子就是这样,有一个类型的变量,Interface1但期望它也是类型Interface2.
如果您希望能够调用这两种方法,则应将变量的类型example设置为Example.这样你就可以instanceof完全避免和输入.
如果您的两个接口Interface1并Interface2没有那么松散耦合,并且您经常需要从两者中调用方法,那么分离接口可能不是一个好主意,或者您可能希望有另一个扩展两者的接口.
通常(虽然并非总是如此),instanceof检查和类型转换通常表明一些OO设计缺陷.有时候设计会适合程序的其余部分,但是你会遇到一个小例子,它可以更简单地输入,而不是重构一切.但是,如果可能的话,作为设计的一部分,您应该始终努力避免它.
Lin*_*ica 11
你有两个不同的选择(我打赌还有更多).
第一个是创建自己的interface,扩展另外两个:
interface Interface3 extends Interface1, Interface2 {}
Run Code Online (Sandbox Code Playgroud)
然后在整个代码中使用它:
public void doSomething(Interface3 interface3){
...
}
Run Code Online (Sandbox Code Playgroud)
另一种方式(在我看来,更好的方法)是使用每种方法的泛型:
public <T extends Interface1 & Interface2> void doSomething(T t){
...
}
Run Code Online (Sandbox Code Playgroud)
后一个选项实际上比前者更少受限制,因为泛型类型T被动态推断并因此导致更少的耦合(类不必实现特定的分组接口,如第一个示例).
核心问题
略微调整您的示例,以便解决核心问题:
public void DoTheThing(Interface1 example)
{
if (example instanceof Interface2)
{
((Interface2) example).someInterface2Method();
}
}
Run Code Online (Sandbox Code Playgroud)
所以你定义了这个方法DoTheThing(Interface1 example).这基本上是说"要做的事,我需要一个Interface1对象".
但是,在您的方法体中,您似乎确实需要一个Interface2对象.那你为什么不在你的方法参数中要求一个?很明显,你应该一直在要求Interface2
你在这里做的是假设Interface1你获得的任何对象也将是一个Interface2对象.这不是你可以依赖的东西.您可能有一些实现两个接口的类,但您可能还有一些只实现一个而不是另一个的类.
没有内在要求,由此Interface1和Interface2需要同时对同一对象上执行.您无法知道(也不依赖于假设)这种情况.
除非您定义固有要求并应用它.
interface InterfaceBoth extends Interface1, Interface2 {}
public void DoTheThing(InterfaceBoth example)
{
example.someInterface2Method();
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,您需要InterfaceBoth同时实现Interface1和Interface2.所以每当你问一个InterfaceBoth对象,你可一定要得到它实现了两个物体Interface1和Interface2,这样的话你可以使用从任一界面的方法,甚至无需投或检查的类型.
你(和编译器)知道这个方法总是可用的,并且没有机会不起作用.
注意:您可以使用Example而不是创建InterfaceBoth接口,但是您只能使用类型的对象,Example而不能使用任何其他类来实现这两个接口.我假设您对处理任何实现两个接口的类感兴趣,而不仅仅是Example.
进一步解构问题.
看看这段代码:
ICarrot myObject = new Superman();
Run Code Online (Sandbox Code Playgroud)
如果您假设此代码编译,您可以告诉我有关Superman该类的内容吗?它明确地实现了ICarrot界面.这就是你能告诉我的一切.您不知道是否Superman实现了IShovel接口.
所以,如果我尝试这样做:
myObject.SomeMethodThatIsFromSupermanButNotFromICarrot();
Run Code Online (Sandbox Code Playgroud)
或这个:
myObject.SomeMethodThatIsFromIShovelButNotFromICarrot();
Run Code Online (Sandbox Code Playgroud)
如果我告诉你这段代码编译,你会感到惊讶吗?你应该,因为这段代码不能编译.
你可以说"但我知道这是一个Superman拥有这种方法的对象!".但是你会忘记你只告诉编译器它是ICarrot变量而不是Superman变量.
你可能会说"但我知道这是一个Superman实现IShovel界面的对象!".但后来你会忘了,你只告诉编译器它是一个ICarrot变量,而不是一个Superman或IShovel变量.
知道了这一点,让我们回顾一下你的代码.
Interface1 example = new Example();
Run Code Online (Sandbox Code Playgroud)
你所说的只是你有一个Interface1变量.
if (example instanceof Interface2) {
((Interface2) example).someInterface2Method();
}
Run Code Online (Sandbox Code Playgroud)
假设此Interface1对象也恰好实现了第二个不相关的接口,这对您没有任何意义.即使这个代码在技术层面上工作,它也是糟糕设计的标志,开发人员期望两个接口之间存在一些固有的相关性,而实际上并没有创建这种相关性.
您可能会说"但我知道我正在放入一个Example对象,编译器也应该知道它!" 但是你会忽略这一点,如果这是一个方法参数,你将无法知道你的方法的调用者发送了什么.
public void DoTheThing(Interface1 example)
{
if (example instanceof Interface2)
{
((Interface2) example).someInterface2Method();
}
}
Run Code Online (Sandbox Code Playgroud)
当其他调用者调用此方法时,如果传递的对象未实现,编译器将仅停止它们Interface1.编译器不会阻止某人传递实现Interface1但未实现的类的对象Interface2.
您的示例不会破坏LSP,但它似乎打破了SRP.如果遇到需要将对象强制转换为第二个接口的情况,则包含此类代码的方法可能会被视为繁忙.
在类中实现2个(或更多)接口很好.在决定使用哪个接口作为其数据类型时,完全取决于将使用它的代码的上下文.
施法很好,特别是在改变背景时.
class Payment implements Expirable, Limited {
/* ... */
}
class PaymentProcessor {
// Using payment here because i'm working with payments.
public void process(Payment payment) {
boolean expired = expirationChecker.check(payment);
boolean pastLimit = limitChecker.check(payment);
if (!expired && !pastLimit) {
acceptPayment(payment);
}
}
}
class ExpirationChecker {
// This the `Expirable` world, so i'm using Expirable here
public boolean check(Expirable expirable) {
// code
}
}
class LimitChecker {
// This class is about checking limits, thats why im using `Limited` here
public boolean check(Limited limited) {
// code
}
}
Run Code Online (Sandbox Code Playgroud)
通常,许多客户端特定的接口都很好,并且在某种程度上是接口隔离原则(SOLID中的"I" )的一部分.在其他答案中已经提到了技术层面上的一些更具体的要点.
特别是你可以通过这样的类别来进行这种隔离
class Person implements FirstNameProvider, LastNameProvider, AgeProvider ... {
@Override String getFirstName() {...}
@Override String getLastName() {...}
@Override int getAge() {...}
...
}
Run Code Online (Sandbox Code Playgroud)
或者,相反,你有一个太强大的实现类,如
class Application implements DatabaseReader, DataProcessor, UserInteraction, Visualizer {
...
}
Run Code Online (Sandbox Code Playgroud)
我认为接口隔离原则的要点是接口应该是特定于客户端的.对于某个任务,它们应该基本上"总结"某个客户端所需的功能.
这样说:问题是在我概述的极端之间取得适当的平衡.当我试图找出界面及其关系(相互之间,以及实现它们的类)时,我总是试着退后一步,以一种有意天真的方式问自己:谁会接受什么?和什么是他打算用它做什么?
关于你提到的例子:当所有的客户总是需要的功能Interface1,并Interface2在同一时间,那么你应该考虑要么定义
interface Combined extends Interface1, Interface2 { }
Run Code Online (Sandbox Code Playgroud)
或者首先没有不同的接口.另一方面,当功能完全不同且不相关且从未一起使用时,您应该想知道为什么单个类同时实现它们.
在这一点上,人们可以参考另一个原则,即组合而不是继承.虽然不经典关系到实现多个接口,组成可也是在这种情况下有利.例如,您可以将类更改为不直接实现接口,但仅提供实现它们的实例:
class Example {
Interface1 getInterface1() { ... }
Interface2 getInterface2() { ... }
}
Run Code Online (Sandbox Code Playgroud)
它看起来在这个有点古怪Example(原文如此),而是取决于执行的复杂性Interface1和Interface2,才能够真正意义,让他们分开.
编辑回应评论:
这里的目的不是将具体类传递Example给需要两个接口的方法.这可能有意义的情况是,当一个类结合两个接口的功能时,但不是通过直接同时实现它们.很难做出一个看起来不太做作的例子,但这样的事情可能会带来这个想法:
interface DatabaseReader { String read(); }
interface DatabaseWriter { void write(String s); }
class Database {
DatabaseConnection connection = create();
DatabaseReader reader = createReader(connection);
DatabaseReader writer = createWriter(connection);
DatabaseReader getReader() { return reader; }
DatabaseReader getWriter() { return writer; }
}
Run Code Online (Sandbox Code Playgroud)
客户端仍然依赖于接口.方法如
void create(DatabaseWriter writer) { ... }
void read (DatabaseReader reader) { ... }
void update(DatabaseReader reader, DatabaseWriter writer) { ... }
Run Code Online (Sandbox Code Playgroud)
然后可以调用
create(database.getWriter());
read (database.getReader());
update(database.getReader(), database.getWriter());
Run Code Online (Sandbox Code Playgroud)
分别.
| 归档时间: |
|
| 查看次数: |
3489 次 |
| 最近记录: |