具有后备情况的特殊情况是否违反《里斯科夫替代原则》?


20

假设我有一个FooInterface具有以下签名的接口:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

还有一个ConcreteFoo实现该接口的具体类:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

ConcreteFoo::doSomething()如果它传递了一种特殊类型的SomethingInterface对象(例如称为SpecialSomething),我想做一些独特的事情。

如果我加强方法的先决条件或引发新的异常,则绝对是LSP违规,但是如果我SpecialSomething在为通用SomethingInterface对象提供后备时又对特殊情况的对象进行了处理,这是否仍会违反LSP ?就像是:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}

Answers:


19

可能取决于还有什么是在合同规定的是违反LSP的doSomething方法。但这几乎可以肯定是代码气味,即使它没有违反LSP。

例如,如果合同的部分doSomething是,它会调用something.commitUpdates()至少一次返回前,而对于特殊情况下,它调用commitSpecialUpdates()代替,那就是违反了LSP的。即使SpecialSomethingcommitSpecialUpdates()方法被有意识地设计成与都具有相同的作用commitUpdates(),但这只是抢先攻击LSP违规行为,而如果人们始终遵循LSP,那正是一种黑客不必做的黑客行为。诸如此类的事情是否适用于您的情况,您必须通过检查合同中的该方法(无论是显式的还是隐式的)来弄清楚。

这是代码异味的原因是因为检查您的一个参数的具体类型首先错过了为其定义接口/抽象类型的要点,并且因为原则上您不能再保证该方法仍然有效(想象(如果有人SpecialSomething用假设commitUpdates()会调用来编写的子类)。首先,尝试使这些特殊更新在现有的环境中起作用SomethingInterface; 那是最好的结果。如果您确实确定不能执行此操作,则需要更新界面。如果您不控制界面,则可能需要考虑编写自己的界面来实现您想要的功能。如果您甚至无法提供一个适用于所有接口的接口,则可能应该完全废弃该接口,并采用采用不同具体类型的多种方法,或者可能需要进行更大的重构。我们将不得不更多地了解您已注释掉的魔术,以告诉您哪种魔术合适。


谢谢!这会有所帮助。假设地,该doSomething()方法的目的是将类型转换为SpecialSomething:如果接收到该方法SpecialSomething,则只会返回未修改的对象,而如果接收到一个通用SomethingInterface对象,则将运行算法将其转换为SpecialSomething对象。由于前提条件和后置条件保持不变,因此我认为合同没有受到违反。
伊万2015年

1
@Evan哦,哇...这是一个有趣的案例。这实际上可能完全没有问题。我唯一能想到的是,如果要返回现有对象而不是构造一个新对象,也许有人会依赖此方法返回一个全新的对象...但是这种事情是否会破坏人们可能取决于语言。有人可以呼叫y = doSomething(x),然后呼叫,x.setFoo(3)然后找到y.getFoo()返回3的呼叫吗?
Ixrec 2015年

这取决于语言,尽管通过返回SpecialSomething对象的副本即可轻松解决此问题,但这是有问题的。尽管出于纯粹的缘故,我还可以看到在传递对象时放弃了特殊情况的优化,SpecialSomething而只是通过较大的转换算法运行它,因为它仍然可以作为SomethingInterface对象运行,因此仍然可以工作。
伊万

1

这并不违反LSP。但是,它仍然违反“请勿进行类型检查”规则。最好以一种自然而然的方式设计代码。可能SomethingInterface需要另一个可以完成此任务的成员,或者您需要在某个地方注入一个抽象工厂。

但是,这不是一成不变的规则,因此您需要确定权衡是否值得。现在,您有一种代码味道,并且可能成为将来增强功能的障碍。摆脱它可能意味着复杂得多的体系结构。没有更多信息,我就说不出哪个更好。


1

不,要利用给定参数不仅提供接口A而且还提供接口A2的事实,而不违反LSP。

只需确保特殊路径没有任何更强的先决条件(除了决定采用它的测试条件),也没有任何较弱的后继条件。

C ++模板通常这样做是为了提供更好的性能,例如通过要求InputIterators,但是如果使用RandomAccessIterators 则提供了额外的保证。

如果您必须在运行时进行决定(例如,使用动态转换),请当心要决定使用哪条路径来消耗所有或更多的潜在收益。

利用特殊情况常常会遇到DRY(不要重复一遍),因为您可能不得不重复编写代码;而遇到KISS(请保持简单),因为它更复杂。


0

在“不要进行类型检查”和“隔离接口”之间要权衡。如果许多类提供了执行某些任务的可行但可能效率不高的方法,而其中一些也可以提供更好的方法,那么就需要一种可以接受可以执行任务的较广泛类别的项目的代码。 (可能是效率低下的),然后又要尽可能高效地执行任务,则有必要使所有对象实现一个接口,该接口包括一个成员,以说是否支持更有效的方法,如果可以,则使用另一个方法。让接收对象的代码检查它是否支持扩展接口,如果支持,则将其强制转换。

就个人而言,我希望使用前一种方法,尽管我希望像.NET这样的面向对象的框架允许接口指定默认方法(使较大的接口使用起来更省力)。如果通用接口包括可选方法,则单个包装器类可以处理具有许多不同功能组合的对象,同时仅向消费者承诺原始包装对象中存在的那些功能。如果将许多功能拆分为不同的接口,那么对于包装对象可能需要支持的接口的每种不同组合,都将需要不同的包装对象。


0

Liskov替代原理是关于根据其超型契约执行作用的子类型。因此,正如Ixrec所写,没有足够的信息来回答是否违反LSP。

但是这里违反了开放封闭原则。如果您有新要求-做SpecialSomething魔术-并且必须修改现有代码,那么您肯定违反了OCP

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.