国家模式是否违反《里斯科夫替代原则》?


17

该图像取自“ 应用域驱动的设计和模式:带有C#和.NET中的示例”

在此处输入图片说明

这是状态模式的类图,其中a SalesOrder在生命周期内可以具有不同的状态。在不同状态之间仅允许某些转换。

现在,OrderState该类是一个abstract类,并且其所有方法都继承为其子类。如果我们将子类视为Cancelled最终状态,不允许将其转换为其他任何状态,则必须对override此类中的所有方法进行抛出。

现在,这不是违反Liskov的替代原则吗,因为sublcass不应改变父级的行为吗?将抽象类更改为接口是否可以解决此问题?
如何解决?


将OrderState更改为在默认情况下会在其所有方法中引发“ NotSupported”异常的具体类吗?
詹姆斯

我还没有读过这本书,但是令我觉得奇怪的是OrderState包含Cancel(),然后是Canceled状态,这是不同的子类型。
欣快的2013年

@Euphoric为什么很奇怪?调用cancel()会将状态更改为已取消。
Songo 2013年

怎么样?当应该更改SalesOrder中的状态实例时,如何在OrderState上调用Cancel。我认为整个模型是错误的。我希望看到完整的实施。
欣快的

Answers:


3

这个特定的实现,是的。如果使状态成为具体的类而不是抽象的实现者,那么您将摆脱这种情况。

但是,您所指的实际上是状态机设计的状态模式从我所看到的状态的增长来看,总体上我是不同意的。我认为有足够的理由将其谴责为违反单一责任原则,因为这些状态管理模式最终成为了解系统许多其他部分当前状态的中央存储库。被集中化的状态管理部分通常会需要与系统许多不同部分相关的业务规则来合理地协调它们。

想象一下,如果系统中所有关心状态的部分都在不同的服务,不同的机器上的不同进程,一个中央状态管理器(详细说明每个位置的状态)有效地阻碍了整个分布式系统,并且我认为瓶颈是一个迹象违反SRP以及总体设计不佳的问题。

相比之下,我建议像MVC模式中的Model对象那样,使对象更智能,因为该模型知道如何处理自身,不需要外部协调器来管理其内部工作或原因。

即使将这样的状态模式放入对象内部,以便仅对其进行管理,也感觉您会使该对象太大。我要说的是,工作流应该通过构成各种自负的对象来完成,而不是通过一个单独的协调状态来管理其他对象的流或自身内部的智能流。

但是在那一点上,它比工程学更具有艺术性,因此,您对这些事情的处理绝对是主观的,它说的原理是一个很好的指南,是的,您列出的实现违反LSP的,但可以纠正为不正确。使用任何这种性质的模式时,请务必非常注意SRP,您可能会很安全。


基本上,最大的问题是面向对象适合于添加新​​类,但很难添加新方法。正如您所说,在状态的情况下,您不太可能需要经常用新状态扩展代码。
hugomg

3
@Jimmy Hoffa对不起,“您的确切含义是:“如果使状态成为具体的类而不是抽象的实现者,那么您将摆脱这种情况。” ?
Songo

@Jimmy:我尝试通过让每个组件只知道其自身状态来实现不带状态模式的状态。最终导致对流程的重大更改使实施和维护变得更加复杂。就是说,我认为使用状态机(理想情况下是将其他人的库强制将其视为黑匣子),而不是使用状态设计模式(因此,状态只是枚举中的元素,而不是完整的类,并且过渡只是愚蠢的edge)提供了状态模式的许多好处,同时对开发人员滥用状态模式的尝试表示敌视。
布赖恩

8

sublcass不应该改变父母的行为吗?

这是对LSP的常见误解。子类可以更改父代的行为,只要它对父代类型保持正确即可。

Wikipedia上有一个很好的长期解释,建议将破坏LSP的事情:

...该子类型必须满足许多行为条件。这些术语在类似于按合同设计方法的术语中进行了详细说明,从而对合同如何与继承交互产生了一些限制:

  • 前提条件不能在子类型中得到加强。
  • 子条件不能弱化后置条件。
  • 超类型的不变量必须保留在子类型中。
  • 历史记录约束(“历史记录规则”)。对象只能通过其方法(封装)被视为可修改的。由于子类型可能会引入父类型中不存在的方法,因此,这些方法的引入可能会导致子类型中状态不允许在父类型中发生变化。历史记录约束禁止这样做。这是Liskov和Wing引入的新颖元素。可以通过将MutablePoint定义为ImmutablePoint的子类型来举例说明违反此约束的情况。这违反了历史记录约束,因为在不可变点的历史记录中,状态在创建后始终是相同的,因此通常不能包含MutablePoint的历史记录。但是,可以安全地修改添加到子类型的字段,因为它们无法通过超类型方法观察到。

就个人而言,我更容易记住这一点:如果我正在使用类型A的方法查看参数,有人传递子类型B会给我带来什么惊喜吗?如果他们愿意的话,就违反了LSP。

抛出异常是一个惊喜吗?并不是的。无论我是在OrderState上还是在Granted或Shipped上调用Ship方法,这种情况随时都可能发生。因此,我必须考虑到这一点,这并不是违反LSP。

也就是说,我确实认为有更好的方法来处理这种情况。如果我使用C#编写此代码,则将使用接口并在调用方法之前检查接口的实现。例如,如果当前的OrderState没有实现IShippable,则不要在其上调用Ship方法。

但是,在这种情况下,我也不会使用State模式。状态模式比这样的域对象的状态更适合应用程序的状态。

因此,简而言之,这是状态模式的一个拙劣设计示例,不是处理订单状态的特别好方法。但是可以说它没有违反LSP。与国家的模式,和其本身的,肯定没有。


在我看来,您正在将LSP与最小惊讶/惊讶的原则相混淆,我相信LSP在违反它们的地方具有更明确的界限,尽管您正在应用基于主观期望的更主观的POLA ; 每个人都期望与下一个有所不同。LSP基于提供者提供的合同和定义明确的担保,而不是由消费者猜测的主观评估的期望。
Jimmy Hoffa 2013年

@JimmyHoffa:您是对的,这就是为什么在我说出一种更简单的方式记住LSP之前,我拼写了确切的含义。但是,如果我心中有一个疑问,“惊喜”是什么意思,那么我将回过头来检查LSP的确切规则(您真的可以随意回忆一下它们吗?)。异常替换功能(反之亦然)是一项困难的工作,因为它没有违反上面的任何特定子句,这可能是应该以完全不同的方式处理它的线索。
pdr 2013年

1
异常是我在合同上定义的预期条件,想想一个NetworkSocket,如果未打开,它可能在发送时会有预期的异常,如果您的实现没有为封闭的套接字抛出该异常,则您违反了LSP ,如果合同规定相反,而您的子类型确实引发了异常,则您违反了LSP。这或多或少是我对LSP中的异常进行分类的方式。至于记住规则;我对此可能是错的,可以随意告诉我,但是我对LSP vs POLA的理解定义在上面我的评论的最后一句话中。
吉米·霍法

1
@JimmyHoffa:我从来没有考虑过POLA和LSP甚至可以远程连接(在UI术语中我想到了POLA),但是,既然您已经指出,它们就是这样。我不确定他们之间是否有冲突,或者我不能确定它们是否比另一个更主观。LSP是否不是软件工程术语中对POLA的早期描述?就像我在Ctrl-C不是复制(POLA)时感到沮丧一样,当ImmutablePoint可变时,我也会感到沮丧,因为它实际上是MutablePoint子类(LSP)。
pdr

1
CircleWithFixedCenterButMutableRadius如果的消费者合同ImmutablePoint规定if X.equals(Y),消费者可以自由地用X代替Y,反之亦然,则我认为这可能是违反LSP的,因此必须避免使用此类替代会引起麻烦。该类型可能能够合法定义equals,以便所有实例都可以比较为不同,但是这种行为可能会限制其用途。
超级猫

2

(这是从C#的角度编写的,因此没有检查到的异常。)

根据Wikipedia关于LSP的文章,LSP的条件之一是:

子类型的方法不应抛出新的异常,除非这些异常本身是超类型的方法所抛出的异常的子类型。

您应该如何理解?当超类型的方法是抽象的时,“超类型的方法抛出的异常”到底是什么?我认为这些是记录为超类型的方法可能引发的异常的异常。

这意味着如果OrderState.Ship()将其记录为“ InvalidOperationException如果当前状态不支持此操作则抛出”,那么我认为这种设计不会破坏LSP。另一方面,如果未以这种方式记录超类型方法,则违反LSP。

但这并不意味着这是一个好的设计,您不应该对正常的控制流使用异常,这似乎非常接近。另外,在实际的应用程序中,您可能想知道尝试执行某项操作之前是否可以执行该操作,例如,禁用UI中的“ Ship”按钮。


实际上,这正是让我思考的重点。如果子类抛出父类中未定义的异常,则它必须违反LSP。
Songo 2013年

我认为在不检查异常的语言中,抛出异常并不违反LSP。这只是语言本身的概念,任何时候任何地方都可以引发任何异常。因此,任何客户端代码都应为此做好准备。
Zapadlo
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.