LSP vs OCP / Liskov换人VS开盘关闭


48

我试图理解OOP的SOLID原则,并且得出的结论是LSP和OCP有一些相似之处(如果不多说的话)。

开放/封闭原则指出“软件实体(类,模块,功能等)应为扩展而开放,但为修改而封闭”。

LSP在简单的话指出,任何情况下Foo可以用的任何实例来代替Bar它源自Foo并计划将工作同样非常的方式。

我不是专业的OOP程序员,但在我看来,只有在Bar派生自LSP Foo不会更改其中任何内容而只能扩展它的情况下,LSP才是可能的。这意味着特别是程序LSP仅在OCP为true时才为true,而OCP仅在LSP为true时才为true。那意味着他们是平等的。

如果我错了纠正我。我真的很想了解这些想法。非常感谢您的回答。


4
这是对这两个概念的非常狭义的解释。可以保持打开/关闭状态,但仍然违反LSP。矩形/正方形或椭圆/圆形示例就是很好的例证。两者都遵守OCP,但都违反了LSP。
乔尔·埃瑟顿

1
世界(或至少是互联网)对此感到困惑。kirkk.com/modularity/2009/12/solid-principles-of-class-design。这个家伙说违反LSP也是违反OCP。然后,在第156页的“软件工程设计:理论与实践”一书中,作者举了一个遵循OCP但违反LSP的示例。我已经放弃了。
Manoj R

@JoelEtherton这些对仅在可变时才违反LSP。在不变的情况下,SquareRectangle不违反LSP。(但是在不可变的情况下,它的设计可能仍然很糟糕,因为您可以拥有不RectangleSquare
等于

简单类比(从库编写者-用户的角度来看)。LSP就像销售一种产品(库),声称可以实现100%(在界面或用户手册上)所说的内容,但实际上并没有(或与所说的内容不匹配)。OCP就像销售产品(库)一样,承诺在出现新功能(例如固件)时可以对其进行升级(扩展),但实际上如果没有工厂服务就无法进行升级。
rwong

Answers:


119

天哪,对OCP和LSP有一些奇怪的误解,还有一些是由于某些术语和示例不匹配而造成的。如果您以相同的方式实现它们,那么这两个原理只是“同一件事”。模式通常以一种或另一种方式遵循原则,只有少数例外。

差异将在下面进一步解释,但首先让我们深入了解原理本身:

开闭原则(OCP)

鲍伯叔叔说

您应该能够扩展类的行为,而无需对其进行修改。

请注意,在这种情况下,扩展一词不一定意味着您应该将需要新行为的实际类子类化。看到我最初提到的术语不匹配之处是什么?关键字extend仅表示Java中的子类,但是其原理比Java古老。

原始图片来自1988年的Bertrand Meyer:

软件实体(类,模块,功能等)应打开以进行扩展,但应关闭以进行修改。

在这里,将原理应用于软件实体更加清楚。一个不好的例子是在您完全修改代码而不是提供某些扩展点时覆盖软件实体。软件实体本身的行为应该是可扩展的,并且一个很好的例子是Strategy-pattern的实现(因为这是最简单的GoF-patterns串IMHO展示):

// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {

    // Context is however open for extension through
    // this private field
    private IBehavior behavior;

    // The context calls the behavior in this public 
    // method. If you want to change this you need
    // to implement it in the IBehavior object
    public void doStuff() {
        if (this.behavior != null)
            this.behavior.doStuff();
    }

    // You can dynamically set a new behavior at will
    public void setBehavior(IBehavior behavior) {
        this.behavior = behavior;
    }
}

// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
    public void doStuff();
}

另外,在上述的例子中Context锁定的进一步的修改。大多数程序员可能想对类进行子类化以扩展它,但是在这里我们不这样做,因为它假定可以通过实现接口的任何方式来更改其行为IBehavior

即,上下文类关闭以进行修改,但打开以进行扩展。实际上,它遵循另一个基本原理,因为我们将行为与对象组成而不是继承结合在一起:

“ 从' 类继承 ' 到' 对象 '的偏爱 '。” (四人帮1995:20)

我会让读者阅读该原理,因为它不在本问题的范围之内。要继续该示例,请说我们具有IBehavior接口的以下实现:

public class HelloWorldBehavior implements IBehavior {
    public void doStuff() {
        System.println("Hello world!");
    }
}

public class GoodByeBehavior implements IBehavior {
    public void doStuff() {
        System.out.println("Good bye cruel world!");
    }
}

使用此模式,我们可以通过将setBehavior方法作为扩展点来在运行时修改上下文的行为。

// in your main method
Context c = new Context();

c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"

c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"

因此,每当要扩展“封闭”上下文类时,都可以通过将其“开放”协作依赖项子类化来实现。显然,这与对上下文本身进行子类化是不同的,但它是OCP。LSP对此也没有提及。

用Mixins扩展而不是继承

除子类化外,还有其他方法可以执行OCP。一种方法是通过使用mixins来使您的类保持开放状态以进行扩展。例如,这在基于原型而非基于类的语言中很有用。这个想法是根据需要修改具有更多方法或属性的动态对象,换句话说就是与其他对象融合或“混合”的对象。

这是一个mixin的javascript示例,可为锚点呈现一个简单的HTML模板:

// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
    render: function() {
        return '<a href="' + this.link +'">'
            + this.content 
            + '</a>;
    }
}

// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
    this.content = content;
    this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
    setLink: function(youtubeid) {
        this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
    }
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);

// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");

console.log(ytLink.render());
// will output: 
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>

这个想法是动态扩展对象,这样做的好处是即使对象处于完全不同的域中,它们也可以共享方法。在上述情况下,您可以通过扩展特定的实现,轻松创建其他类型的html锚LinkMixin

就OCP而言,“ mixins”是扩展名。在上面的示例中,YoutubeLink是我们的软件实体,已关闭以供修改,但通过使用mixins进行扩展以供扩展。对象层次结构被展平,这使得无法检查类型。但这并不是一件坏事,我将进一步解释说,检查类型通常不是一个好主意,并且会通过多态性破坏这个主意。

请注意,由于大多数extend实现可以混合多个对象,因此可以使用此方法进行多重继承:

_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);

您唯一需要记住的是不要冲突名称,即,mixin碰巧定义了某些属性或方法的名称,因为它们将被覆盖。以我的拙劣经验,这不是问题,如果确实发生,则表明设计存在缺陷。

里斯科夫的替代原则(LSP)

鲍勃叔叔的定义很简单:

派生类必须可以替代其基类。

这个原则是古老的,实际上Bob叔叔的定义没有区别原则,因为在上述Strategy示例中,使用了相同的超类型,使得LSP仍然与OCP紧密相关IBehavior。因此,让我们看一下Barbara Liskov最初的定义,看看是否可以找到关于该原理的其他一些东西,就像数学定理一样:

这里需要的是类似以下替换属性的内容:如果对于每个o1类型S的对象o2,都有一个对象类型T,使得对于所有P根据定义的程序TPo1替换时的行为不变,o2S是的子类型T

让我们对此稍作耸耸肩,注意,因为它根本没有提到类。在JavaScript中,即使它不是显式基于类的,您实际上也可以遵循LSP。如果您的程序包含至少两个JavaScript对象的列表,则这些列表:

  • 需要以相同的方式计算
  • 具有相同的行为,并且
  • 否则在某种程度上完全不同

...然后将这些对象视为具有相同的“类型”,这对于程序来说并不重要。这本质上是多态性从一般意义上讲;如果您使用的是接口,则不需要知道实际的子类型。OCP对此没有明确说明。实际上,它还指出了大多数新手程序员所做的设计错误:

每当您想检查对象的子类型的渴望时,您很可能会错误地执行它。

好了,所以它可能不是错所有的时间,但如果你有冲动做一些类型检查instanceof或枚举,你可能会做的节目多一点令人费解自己比它需要。但这并非总是如此。如果解决方案足够小,并且可以进行无情的重构,那么快速而又肮脏的hack使事情正常运行是我可以想到的让步,如果更改需要,它可能会得到改善。

可以根据实际问题解决“设计错误”的方法:

  • 超类没有调用先决条件,而是强制调用者这样做。
  • 超类缺少调用者需要的通用方法。

两者都是常见的代码设计“错误”。您可以执行几种不同的重构,例如上拉方法,或重构为类似Visitor模式的模式

我实际上非常喜欢Visitor模式,因为它可以处理大型的if语句意大利面条,并且比您在现有代码上的想法更易于实现。说我们有以下上下文:

public class Context {

    public void doStuff(string query) {

        // outcome no. 1
        if (query.Equals("Hello")) {
            System.out.println("Hello world!");
        } 

        // outcome no. 2
        else if (query.Equals("Bye")) {
            System.out.println("Good bye cruel world!");
        }

        // a change request may require another outcome...

    }

}

// usage:
Context c = new Context();

c.doStuff("Hello");
// prints "Hello world"

c.doStuff("Bye");
// prints "Bye"

可以将if语句的结果转换成自己的访问者,因为每个结果都取决于某些决策和要运行的某些代码。我们可以像这样提取这些:

public interface IVisitor {
    public bool canDo(string query);
    public void doStuff();
}

// outcome 1
public class HelloVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Hello");
    }
    public void doStuff() {
         System.out.println("Hello World");
    }
}

// outcome 2
public class ByeVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Bye");
    }
    public void doStuff() {
        System.out.println("Good bye cruel world");
    }
}

在这一点上,如果程序员不了解Visitor模式,那么他将实现Context类以检查其是否为某种特定类型。由于Visitor类具有boolean canDo方法,因此实现者可以使用该方法调用来确定它是否是执行此工作的正确对象。上下文类可以使用所有访问者(并添加新访问者),如下所示:

public class Context {
    private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();

    public Context() {
        visitors.add(new HelloVisitor());
        visitors.add(new ByeVisitor());
    }

    // instead of if-statements, go through all visitors
    // and use the canDo method to determine if the 
    // visitor object is the right one to "visit"
    public void doStuff(string query) {
        for(IVisitor visitor : visitors) {
            if (visitor.canDo(query)) {
                visitor.doStuff();
                break;
                // or return... it depends if you have logic 
                // after this foreach loop
            }
        }
    }

    // dynamically adds new visitors
    public void addVisitor(IVisitor visitor) {
        if (visitor != null)
            visitors.add(visitor);
    }
}

两种模式都遵循OCP和LSP,但是它们都针对它们确定了不同的地方。那么,如果代码违反其中一项原则,它的外观如何?

违反一个原则但遵循另一个原则

有多种方法可以打破其中一个原则,但仍然可以遵循另一个原则。出于充分的原因,以下示例似乎是人为的,但实际上我已经在生产代码中看到了这些示例(甚至更糟):

遵循OCP但不遵循LSP

假设我们有给定的代码:

public interface IPerson {}

public class Boss implements IPerson {
    public void doBossStuff() { ... }
}

public class Peon implements IPerson {
    public void doPeonStuff() { ... }
}

public class Context {
    public Collection<IPerson> getPersons() { ... }
}

这段代码遵循开闭原则。如果我们调用上下文的GetPersons方法,我们将得到一群拥有自己的实现的人。这意味着IPerson已关闭以进行修改,但可以进行扩展。但是,当我们不得不使用它时,事情就发生了转机:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // now we have to check the type... :-P
    if (person instanceof Boss) {
        ((Boss) person).doBossStuff();
    }
    else if (person instanceof Peon) {
        ((Peon) person).doPeonStuff();
    }
}

您必须进行类型检查和类型转换!还记得我上面提到的类型检查如何是一件坏事吗?不好了!但是不要担心,正如上面也提到的那样,要么做一些上拉重构,要么实现一个Visitor模式。在这种情况下,我们可以在添加常规方法后简单地进行上拉重构:

public class Boss implements IPerson {
    // we're adding this general method
    public void doStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void doBossStuff() { ... }
}


public interface IPerson {
    // pulled up method from Boss
    public void doStuff();
}

// do the same for Peon

现在的好处是,您不再需要遵循LSP知道确切的类型:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // yay, no type checking!
    person.doStuff();
}

遵循LSP但不遵循OCP

让我们看一下遵循LSP但不遵循OCP的一些代码,虽然有些人为设计,但请允许我对此进行编码,这是一个非常微妙的错误:

public class LiskovBase {
    public void doStuff() {
        System.out.println("My name is Liskov");
    }
}

public class LiskovSub extends LiskovBase {
    public void doStuff() {
        System.out.println("I'm a sub Liskov!");
    }
}

public class Context {
    private LiskovBase base;

    // the good stuff
    public void doLiskovyStuff() {
        base.doStuff();
    }

    public void setBase(LiskovBase base) { this.base = base }
}

该代码执行LSP,因为上下文可以在不知道实际类型的情况下使用LiskovBase。您可能认为此代码也遵循OCP,但仔细观察一下,该类是否真的关闭了?如果该doStuff方法不仅仅是打印一行,该怎么办?

如果遵循OCP,答案很简单:,这不是因为在此对象设计中,我们需要使用其他内容完全覆盖代码。这打开了蠕虫的剪切和粘贴功能,因为您必须从基类中复制代码才能使工作正常。该doStuff方法确定可以扩展,但尚未完全关闭以进行修改。

我们可以对此应用模板方法模式。模板方法模式在框架中非常常见,以至于您可能在不知道它的情况下就一直在使用它(例如,java swing组件,c#表单和组件等)。这是一种关闭doStuff修改方法并通过使用java的final关键字将其标记为关闭的方法。该关键字可以防止任何人进一步对该类进行子类化(在C#中,您可以sealed用来做同样的事情)。

public class LiskovBase {
    // this is now a template method
    // the code that was duplicated
    public final void doStuff() {
        System.out.println(getStuffString());
    }

    // extension point, the code that "varies"
    // in LiskovBase and it's subclasses
    // called by the template method above
    // we expect it to be virtual and overridden
    public string getStuffString() {
        return "My name is Liskov";
    }
}

public class LiskovSub extends LiskovBase {
    // the extension overridden
    // the actual code that varied
    public string getStuffString() {
        return "I'm sub Liskov!";
    }
}

这个示例遵循OCP,看起来确实很愚蠢,但是可以想象它会随着更多代码的处理而扩展。我一直看到部署在生产中的代码,其中子类完全覆盖了所有内容,并且被覆盖的代码大部分在实现之间被剪切粘贴。它可以工作,但是与所有代码重复一样,它也是维护噩梦的一种设置。

结论

我希望所有这些都清除了一些有关OCP和LSP以及它们之间的异同的问题。很容易将它们视为相同,但是上面的示例应该表明它们并非相同。

请注意,从上面的示例代码收集:

  • OCP的目的是锁定工作代码,但仍要通过某种扩展名使其保持打开状态。

    这是通过封装与模板方法模式示例相同的代码来避免代码重复。它也允许快速失败,因为破坏性变化是痛苦的(即更改一个地方,将其破坏到其他地方)。为了维护起见,封装更改的概念是一件好事,因为更改总是会发生。

  • LSP旨在让用户处理实现超类型的不同对象,而无需检查它们的实际类型。这本质上就是多态性

    该原理提供了一种进行类型检查和类型转换的替代方法,它可以随着类型数量的增加而失控,并且可以通过上拉重构或应用模式(例如Visitor)来实现。


7
这是一个很好的解释,因为它不会通过暗示OCP始终意味着通过继承来实现来简化OCP。正是这种过度简化将OCP和SRP引入了某些人的思维,实际上它们实际上可以是两个完全独立的概念。
埃里克·金

5
这是我见过的最好的堆栈交换答案之一。我希望我可以投票10次。做得好,谢谢您的出色解释。
鲍勃·霍恩

在那里,我在Javascript上添加了Blub,这不是基于类的编程语言,但仍可以遵循LSP并编辑了文本,因此希望它能更流畅地阅读。!
Spoike

尽管您从LSP的Bob叔叔那里得到的报价是正确的(与他的网站相同),但是是否应该相反?它不应该声明“基类应该可以替代其派生类”吗?在LSP上,对派生类而不是基类进行“兼容性”测试。不过,我不是英语为母语的人,我认为我可能遗漏了一些有关该短语的细节。
Alpha

@Alpha:这是一个很好的问题。基类总是可以用它的派生类代替,否则继承将不起作用。如果您从需要实现的扩展类中忽略成员(方法或属性/字段),则编译器(至少在Java和C#中)会抱怨。LSP旨在阻止您添加仅在派生类上本地可用的方法,因为这要求那些派生类的用户了解它们。随着代码的增长,此类方法将难以维护。
Spoike

15

这引起了很多混乱。我宁愿从哲学角度考虑这些原则,因为它们有许多不同的例子,有时具体的例子并不能真正抓住它们的全部本质。

OCP尝试解决的问题

假设我们需要为给定程序添加功能。解决此问题的最简单方法,尤其是对于经过程序思考的人来说,是在需要的地方添加if子句或类似的东西。

问题在于

  1. 它更改了现有工作代码的流程。
  2. 它会在每种情况下强制执行新的条件分支。例如,假设您有一本书的清单,其中一些已经在出售中,并且您要遍历所有书籍并打印其价格,这样,如果它们在出售中,则打印的价格将包括字符串“ (出售)”。

您可以通过添加额外的字段名为“is_on_sale”的所有书籍做到这一点,那么你就可以打印任何图书价格时,检查该字段,或或者,你可以使用不同的类型,打印从数据库实例上销售的书籍价格字符串中的“(ON SALE)”(不是完美的设计,但它可以说明要点)。

第一个程序性解决方案的问题是每本书都有一个额外的领域,并且在许多情况下还存在额外的冗余复杂性。第二种解决方案仅在实际需要的地方强制使用逻辑。

现在考虑一个事实,即在很多情况下需要不同的数据和逻辑,并且您将明白为什么在设计类或对需求的变化做出反应时牢记OCP是一个好主意。

到现在为止,您应该已经有了主要思想:尝试使自己处于可以将新代码实现为多态扩展而不是过程修改的情况。

但是不要害怕分析上下文,看看弊端是否胜过了收益,因为即使不认真对待,即使是OCP之类的原理也可以使20行程序中的20类混乱

LSP试图解决的问题

我们都喜欢代码重用。随之而来的疾病是,许多程序无法完全理解它,以至于他们盲目地分解通用的代码行,只是在模块之间创建了难以理解的复杂性和冗余的紧密耦合,除了几行代码,就要完成的概念工作而言,没有什么共同之处。

最大的例子是接口重用。您可能自己亲眼目睹了。一个类实现一个接口,不是因为它是对它的逻辑实现(或在具体基类的情况下的扩展),而是因为它恰好在那时声明的方法就其所涉及的而言具有正确的签名。

但是随后您遇到了一个问题。如果类仅通过考虑它们声明的方法的签名来实现接口,那么您将发现自己能够将类的实例从一种概念功能传递到要求完全不同的功能的地方,而这些功能恰好取决于相似的签名。

那不是那么可怕,但是却引起很多混乱,我们拥有防止自己犯此类错误的技术。我们需要做的是将接口视为API + Protocol。API在声明中显而易见,而协议在接口的现有用法中显而易见。如果我们有2个共享相同API的概念性协议,则应将它们表示为2个不同的接口。否则,我们会陷入DRY教条主义,并且具有讽刺意味的是,这样做只会导致难以维护的代码。

现在您应该能够完全理解该定义。LSP说:不要继承基类并在那些依赖基类的其他地方无法相处的子类中实现功能。


1
我报名只是为了能够投票赞成这和Spoike的答案-做得好。
David Culp

7

据我了解:

OCP表示:“如果要添加新功能,请创建一个扩展现有类的新类,而不要对其进行更改。”

LSP说:“如果您创建一个扩展现有类的新类,请确保该类与其基础完全可互换。”

因此,我认为它们是相辅相成的,但并不平等。


4

虽然确实OCP和LSP都与修改有关,但OCP讨论的修改类型并不是LSP所讨论的。

关于OCP的修改是开发人员在现有类中编写代码的物理动作。

LSP处理派生类与其基类相比带来的行为修改,以及使用子类而不是超类可能导致的程序执行时运行时间更改。

因此,尽管从远处看它们看起来很相似OCP!= LSP。实际上,我认为它们可能是仅有的两个彼此无法理解的SOLID原则。


2

LSP用简单的话说,可以用Foo派生的Bar的任何实例替换Foo的任何实例,而不会损失程序功能。

错了 LSP声明Bar类不应引入行为,当Bar使用Foo派生代码时,这是不期望的。它与功能丧失无关。您可以删除功能,但是仅当使用Foo的代码不依赖于此功能时才可以删除。

但是最后,这通常很难实现,因为在大多数情况下,使用Foo进行的代码取决于其所有行为。因此删除它违反了LSP。但是像这样简化它只是LSP的一部分。


一个很常见的情况是被替换的对象消除了副作用:不输出任何内容的虚拟记录器,或测试中使用的模拟对象。
没用的2012年

0

关于可能违反的对象

要了解差异,您应该了解这两个原理的主题。可能违反或没有违反某些原则的不是代码或情况的某些抽象部分。它总是某些特定的组件-函数,类或模块-可能违反OCP或LSP。

谁可能违反LSP

仅当存在具有某些合同的接口以及该接口的实现时,才可以检查LSP是否断开。如果实现不符合接口,或者通常不符合合同,则LSP中断。

最简单的例子:

class Container {
    // Should add the object to the container.
    void addObject(object) {
        internalArray.append(object);
    }

    int size() {
        return internalArray.size();
    }
}

class CustomContainer extends Container {
    @Override void addObject(object) {
        System.console.print("Skipping object! Ha-ha!");
    }
}

void fillWithRandomNumbers(Container container) {
    while (container.size() < 42) {
        container.addObject(Randomizer.getNumber())
    }
}

合同明确规定addObject应将其论点附加到容器中。并CustomContainer显然违反了合同。因此该CustomContainer.addObject功能违反了LSP。因此,CustomContainer该类违反了LSP。最重要的后果是CustomContainer无法传递给fillWithRandomNumbers()Container不能用代替CustomContainer

请记住非常重要的一点。中断LSP的不是整个代码,具体地说CustomContainer.addObjectCustomContainer是中断LSP。当您声明LSP被违反时,您应该始终指定两件事:

  • 违反LSP的实体。
  • 实体违反的合同。

而已。只是合同及其执行。代码中的低调并没有说明LSP违规。

谁可能违反OCP

仅当数据集有限且处理该数据集中的值的组件时,才可以检查是否违反了OCP。如果数据集的限制可能随时间变化,并且这需要更改组件的源代码,则该组件违反了OCP。

听起来很复杂。让我们尝试一个简单的示例:

enum Platform {
    iOS,
    Android
}

class PlatformDescriber {
    String describe(Platform platform) {
        switch (platform) {
            case iOS: return "iPhone OS, v10.0.1";
            case Android: return "Android, v7.1";
        }
    }
}

数据集是受支持平台的集合。PlatformDescriber是处理该数据集中的值的组件。添加新平台需要更新的源代码PlatformDescriber。因此,PlatformDescriber该类违反了OCP。

另一个例子:

class Shop {
    void sellItemToCustomer(item, customer) {
        // some buisiness logic here
        ...
        logger.logItemSold()
    }
}

class Logger {
    void logItemSold() {
        logger.logToStdErr("an item was sold")
        logger.logToRemote("an item was sold")
        logger.logToDatabase("an item was sold")
    }
}

“数据集”是应在其中添加日志条目的通道集。Logger是负责将条目添加到所有通道的组件。添加对另一种日志记录方式的支持需要更新的源代码Logger。因此,Logger该类违反了OCP。

请注意,在两个示例中,数据集在语义上都不是固定的。它可能会随着时间而改变。一个新的平台可能会出现。一个新的日志记录渠道可能会出现。如果在这种情况下应更新组件,则它违反了OCP。

突破极限

现在是棘手的部分。将上面的示例与以下内容进行比较:

enum GregorianWeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

String translateToRussian(GregorianWeekDay weekDay) {
    switch (weekDay) {
        case Monday: return "Понедельник";
        case Tuesday: return "Вторник";
        case Wednesday: return "Среда";
        case Thursday: return "Четверг";
        case Friday: return "Пятница";
        case Saturday: return "Суббота";
        case Sunday: return "Воскресенье";
    }
}

您可能认为translateToRussian违反了OCP。但实际上并非如此。GregorianWeekDay确切名称的具体限制为7个工作日。重要的是,这些限制在语义上不能随时间变化。阳历星期总是有7天。始终会有星期一,星期二等。此数据集在语义上是固定的。translateToRussian的源代码不可能需要修改。因此,没有违反OCP。

现在应该清楚,穷举switch并不总是表明OCP损坏。

区别

现在感觉不同:

  • LSP的主题是“接口/合同的实现”。如果实现不符合合同,则它将中断LSP。该实现是否可以随时间而变化,是否可以扩展,并不重要。
  • OCP的主题是“一种响应需求变更的方法”。如果对新型数据的支持需要更改处理该数据的组件的源代码,则该组件将破坏OCP。组件是否违反合同并不重要。

这些条件是完全正交的。

例子

@Spoike的答案中违反一个原则,但遵循另一原则则完全错误。

在第一个示例中,for-loop部分明显违反了OCP,因为未经修改就无法扩展。但是没有迹象表明LSP违反。甚至不清楚Context合同是否允许getPersons返回除Bossor 以外的任何值Peon。即使假定允许IPerson返回任何子类的合同,也没有任何类可以覆盖此后置条件并违反它。而且,如果getPersons将返回某个第三类的实例,则for-loop将完成其工作而不会失败。但是,事实与LSP无关。

下一个。在第二个示例中,没有违反LSP,也没有违反OCP。同样,该Context部分与LSP无关-没有定义的合同,没有子类,没有突破性的覆盖。不是Context谁应该服从LSP,它是LiskovSub不应该违反其基础的合同。关于OCP,课程真的关闭了吗?- 是的。无需修改即可扩展它。显然,扩展点的名称表明状态做任何您想做的事,没有限制。该示例在现实生活中不是很有用,但显然并没有违反OCP。

让我们尝试制作一些正确违反OCP或LSP的正确示例。

遵循OCP,但不遵循LSP

interface Platform {
    String name();
    String version();
}

class iOS implements Platform {
    @Override String name() { return "iOS"; }
    @Override String version() { return "10.0.1"; }
}

interface PlatformSerializer {
    String toJson(Platform platform);
}

class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return platform.name() + ", v" + platform.version();
    }
}

在这里,HumanReadablePlatformSerializer添加新平台时不需要任何修改。因此,它遵循OCP。

但是合同要求toJson必须返回格式正确的JSON。该类不这样做。因此,它不能传递给PlatformSerializer用于格式化网络请求正文的组件。从而HumanReadablePlatformSerializer违反LSP。

遵循LSP但不遵循OCP

对先前示例的一些修改:

class Android implements Platform {
    @Override String name() { return "Android"; }
    @Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return "{ "
                + "\"name\": \"" + platform.name() + "\","
                + "\"version\": \"" + platform.version() + "\","
                + "\"most-popular\": " + isMostPopular(platform) + ","
                + "}"
    }

    boolean isMostPopular(Platform platform) {
        return (platform instanceof Android)
    }
}

序列化程序返回格式正确的JSON字符串。因此,此处没有违反LSP的行为。

但是有一个要求,即如果平台使用最广泛,那么在JSON中应该有相应的指示。在此示例中,OCP被HumanReadablePlatformSerializer.isMostPopular功能侵犯,因为某天iOS成为最受欢迎的平台。从形式上来说,这意味着目前将最常用的平台集定义为“ Android”,并且isMostPopular不足以处理该数据集。数据集在语义上不是固定的,可以随时间自由变化。HumanReadablePlatformSerializer的源代码需要进行更改以进行更新。

在此示例中,您可能还会注意到违反“单一责任”的情况。我故意这样做是为了能够在同一个主题实体上展示这两个原则。要修复SRP,您可以将isMostPopular函数提取到某些外部文件,Helper并向中添加参数PlatformSerializer.toJson。但这是另一个故事。


0

LSP和OCP不一样。

LSP谈到了程序的正确性,因为它主张。如果将子类型的实例替换为祖先类型的代码时会破坏程序的正确性,则说明您违反了LSP。您可能必须模拟一个测试来显示这一点,但是不必更改基础代码库。您正在验证程序本身,看它是否符合LSP。

OCP讨论了程序代码更改(从一个源版本到另一个源版本的差额)的正确性。行为不应修改。它只能扩展。经典示例是字段加法。所有现有字段将继续像以前一样运行。新字段仅添加功能。但是,删除字段通常违反OCP。在这里,您要验证程序版本增量,以查看其是否符合OCP。

这就是LSP和OCP之间的关键区别。前者仅验证当前的代码库,后者仅验证从一个版本到下一个版本的代码库增量。因此,它们不能是同一事物,它们被定义为验证不同的事物。

我会给您一个更正式的证明:说“ LSP暗示OCP”将意味着一个增量(因为在平凡的情况下OCP要求一个,而LSP则不需要一个)。因此,这显然是错误的。相反,我们可以简单地通过说OCP是有关增量的语句来反驳“ OCP暗示LSP”,因此,它不涉及就地程序的语句。这是因为您可以从任何程序开始创建任何增量。他们是完全独立的。


-1

我将从客户的角度来看它。如果客户端正在使用接口的功能,并且该功能在内部已由A类实现。假设有一个B类扩展了A类,那么明天如果我从该接口中删除A类并放入B类,则B类应该还向客户端提供相同的功能。标准示例是游泳的Duck类,如果ToyDuck扩展了Duck,那么它也应该游泳并且不会抱怨它不会游泳,否则ToyDuck不应具有Duck扩展类。


如果人们在拒绝任何答案的同时也发表评论,那将是非常有建设性的。毕竟,我们所有人都在这里共享知识,仅仅在没有适当理由的情况下做出判断就不会有任何目的。
AKS

这似乎并没有提供任何实质性的过度点进行,而且在以往6个回答解释
蚊蚋

1
听起来您只是在解释其中一个原则,我认为是L。没关系,但是这个问题要求比较/对比两种不同的原理。这可能就是为什么有人反对的原因。
StarWeaver 2014年
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.