假设我们有一个Task实体列表和一个ProjectTask子类型。任务可以随时关闭,但ProjectTasks除外,一旦状态为“已启动”就无法关闭。用户界面应确保关闭启动ProjectTask的选项永远不可用,但域中存在一些保护措施:

 public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
              throw new Exception("Cannot close a started Project Task");

          base.Close();
     }
}
 


现在,当在Task上调用Close()时,如果它是具有启动状态的ProjectTask,则有机会失败,而如果它是基本Task则不会。但这是业务需求。它应该失败。可以认为这违反了Liskov替代原则吗?

评论

非常适合违反liskov替换的T示例。不要在这里使用继承,这样就可以了。

您可能需要将其更改为:public Status Status {get;私人套装; };否则可以解决Close()方法。

也许仅仅是这个例子,但是我认为遵守LSP并没有实质性的好处。对我来说,这个问题的解决方案比符合LSP的解决方案更清晰,更易于理解和维护。

@BenLee维护起来并不容易。它看起来只是那样,因为您是孤立看到的。当系统很大时,确保Task的子类型不会在仅知道Task的多态代码中引入奇异的不兼容性。 LSP并不是一时兴起,而是被引入以帮助大型系统的可维护性。

@BenLee假设您有一个TaskCloser进程,它将关闭allTask​​s(tasks)。显然,此过程不会尝试捕获异常。毕竟,它不是Task.Close()的显式协定的一部分。现在,您介绍了ProjectTask,突然,您的TaskCloser开始引发(可能是未处理的)异常。这很重要!

#1 楼

是的,它违反了LSP。 Liskov替代原理要求


子条件不能加强先决条件。
子条件不能弱化后条件。
超类型的不变量必须保留在子类型。
历史约束(“历史规则”)。只能通过对象的方法(封装)将其视为可修改的对象。由于子类型可能会引入父类型中不存在的方法,因此这些方法的引入可能会导致子类型中状态不允许在父类型中发生变化。历史记录约束禁止这样做。

您的示例通过增强调用Close()方法的前提条件来打破第一个要求。

您可以通过将增强的前提条件应用于继承层次结构的顶层:



 public class Task {
    public Status Status { get; set; }
    public virtual bool CanClose() {
        return true;
    }
    public virtual void Close() {
        Status = Status.Closed;
    }
}
 


通过规定Close()的调用仅在CanClose()返回true的状态下有效,您可以将前提条件应用到TaskProjectTask,从而解决LSP冲突: =“ lang-cs prettyprint-override”> public class ProjectTask : Task { public override bool CanClose() { return Status != Status.Started; } public override void Close() { if (Status == Status.Started) throw new Exception("Cannot close a started Project Task"); base.Close(); } }

评论


我不喜欢重复检查。我希望将异常抛出到Task.Close中,并从Close中删除虚拟。

– up
2012年10月16日20:53

@Euphoric是的,让顶级Close进行检查,并添加受保护的DoClose将是有效的选择。但是,我想尽可能地与OP的示例保持一致。对此进行改进是一个单独的问题。

–谢尔盖·卡里尼琴科(Sergey Kalinichenko)
2012年10月16日在20:57

@Euphoric:但是现在没有办法回答这个问题,“这个任务可以关闭吗?”而不要关闭它。这不必要地强制将异常用于流量控制。但是,我承认,这种事情可能太过分了。太过分了,这种解决方案最终会导致企业混乱。无论如何,OP的问题使我对原则有了更多的了解,因此象牙塔的答案非常合适。 +1

–布赖恩
2012年10月16日20:58



@Brian CanClose仍然存在。仍然可以调用它来检查是否可以关闭Task。 Check Close中的检查也应调用此方法。

– up
2012年10月16日在20:59

@Euphoric:啊,我误会了。没错,这使解决方案更加干净。

–布赖恩
2012年10月16日在21:04

#2 楼

是。这违反了LSP。我的建议是在基本任务中添加CanClose方法/属性,以便任何任务都可以告知处于此状态的任务是否可以关闭。它还可以提供原因。并从Close中删除虚拟。

根据我的评论:

评论


感谢您这样做,使您更进一步了dasblinkenlight的示例,但是我很喜欢他的解释和理由。抱歉,我不能接受2个答案!

– Paul T Davies
2012年10月17日在7:55

我很想知道为什么签名是公共虚拟布尔CanClose(字符串原因)-用完了,您只是为了将来?还是我想念的更微妙的东西?

–老师吉尔特
2012年10月18日在20:51

@ReacherGilt我认为您应该检查/参考内容并再次阅读我的代码。你很困惑。简单地说:“如果任务无法完成,我想知道为什么。”

– up
2012年10月19日17:46

out并非在所有语言中都可用,返回一个元组(或封装原因和布尔值的简单对象将使其在OO语言中具有更好的可移植性,尽管这样做的代价是失去了直接使用bool的便利性。也就是说,对于DO语言支持,这个答案没有错。

– Newtopian
16-3-3在19:35

可以加强CanClose属性的前提条件吗?即添加条件?

–约翰五世
18年1月17日在16:55



#3 楼

Liskov替换原则指出,基类应该可以用其任何子类替换,而无需更改程序的任何所需属性。由于只有ProjectTask在关闭时会引发异常,因此,如果使用ProjectTask代替Task,则必须将程序更改为适应该情况。因此,这是一个违法行为。

评论


我使用c#,我认为这没有这种可能性,但是我知道Java可以。

– Paul T Davies
2012年10月17日7:56

@PaulTDavies您可以使用方法引发的异常来装饰方法,例如msdn.microsoft.com/en-us/library/5ast78ax.aspx。当您将鼠标悬停在基类库中的方法上时,您会注意到这一点,您将获得异常列表。它不是强制性的,但是仍然使调用者知道。

– Despertar
13年7月19日在6:19



#4 楼

违反LSP需要三方。类型T,子类型S和使用T但被赋予S实例的程序P。

您的问题提供了T(任务)和S(ProjectTask),但没有提供P。您的问题是不完整的,并且答案是合格的:如果存在一个P,该P不期望发生异常,那么对于该P,您有LSP违规。如果每个P都希望有异常,那么就没有LSP违规。

但是,您确实有SRP违规。可以更改任务状态以及不将某些状态中的某些任务更改为其他状态的策略是两个非常不同的职责。


职责1:代表任务。
职责2:实施更改任务状态的策略。

这两个职责由于不同的原因而发生变化,因此应该放在不同的类中。任务应处理作为任务的事实以及与任务相关联的数据。 TaskStatePolicy应该处理任务在给定应用程序中从状态过渡到状态的方式。

评论


责任在很大程度上取决于领域以及(在此示例中)任务状态及其更改者的复杂程度。在这种情况下,没有任何迹象表明,因此SRP没有问题。至于LSP违规,我相信我们都假设调用者不会期望异常,应用程序应该显示合理的消息而不是进入错误状态。

– up
2013年9月4日16:40



Unca'Bob回应? “我们不值得!我们不值得!”。无论如何...如果每个P都希望发生异常,那么就不会违反LSP。但是,如果我们规定一个T实例不能抛出OpenTaskException(提示,提示),并且每个P都希望有一个异常,那么对于接口代码,而不是实现代码,这意味着什么?我在说什么我不知道。我只是在对Unca'Bob的评论发表评论。

– radarbob
2013年9月5日在16:32



您是正确的,证明违反LSP需要三个对象。但是,如果有任何程序P在没有S的情况下是正确的,但在添加S的情况下失败,则存在LSP违规。

–kevin cline
16-3-3在21:31



#5 楼

这可能会也可能不会违反LSP。

如果遵循LSP,则ProjectTask类型的对象必须表现为预期的Task类型的对象表现。

代码的问题是您尚未记录Task类型的对象的预期行为。您已经编写了代码,但是没有合同。我将为Task.Close添加一个合同。根据我添加的合同,ProjectTask.Close的代码是否遵循LSP。

鉴于Task.Close的以下合同,ProjectTask.Close的代码不遵循LSP:

      // Behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }
 


鉴于Task.Close的以下约定,ProjectTask.Close的代码确实遵循LSP:

     // Behaviour: Moves the task to the closed status if possible.
     // If this is not possible, this method throws an Exception
     // and leaves the status unchanged.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }



应以两种方式记录可能被覆盖的方法:知道接收者对象是Task的客户端可以依靠它,但是不知道它是直接实例的哪个类。它还会告诉子类的设计者哪些替代是合理的,哪些替代是不合理的。如果您使用Task,则得到结果,它还告诉子类设计者如果不重写该方法将继承哪些行为。

现在应保持以下关系:


如果S是T的子类型,则S的书面行为应细化T的书面行为。 T的行为。
如果S是T(或等于)T的子类型,则S的默认行为应细化T的已记录行为。
类代码的实际行为应细化其T。已记录的默认行为。


评论


@ user61852提出了可以在方法签名中声明可以引发异常的观点,并且只需执行此操作(没有实际效果代码),就不再破坏LSP。

– Paul T Davies
2015年6月30日15:39

@PaulTDavies你说得对。但是在大多数语言中,签名并不是声明例程可能引发异常的好方法。例如在OP中(我认为是C#),Close的第二种实现确实抛出了。因此,签名声明可能会引发异常-并不是说不会。 Java在这方面做得更好。即使这样,如果您声明某个方法可以声明一个异常,则应记录可能(或将要)的情况。因此,我认为要确定是否违反LSP,我们需要签名以外的文档。

–西奥多·诺维(Theodore Norvell)
2015年7月1日在16:32



这里的许多答案似乎完全忽略了以下事实:如果您不知道合同,就无法知道合同是否经过验证。感谢您的回答。

– gnasher729
16年5月4日在16:37

好的答案,但其他答案也很好。他们推断基类不会引发异常,因为该类中没有任何东西显示异常迹象。因此,使用基类的程序不应为异常做好准备。

– inf3rno
17-10-10在2:53

没错,例外列表应该记录在某处。我认为最好的地方是在代码中。这里有一个相关的问题:stackoverflow.com/questions/16700130/…但是您也可以在没有注释等的情况下执行此操作,只需向if(false)抛出新的Exception(“ cannot start”)类。编译器将删除它,并且代码仍然包含所需的内容。顺便说一句。这些解决方法仍然会违反LSP,因为前提条件仍然得到加强...

– inf3rno
17-10-10在3:02



#6 楼

这并不违反Liskov替代原理。

Liskov替代原理说:


令q(x)是关于x的对象x的可证明性质。设S为T的子类型。如果存在类型S的对象y,使得q(y)不可证明,则类型S违反了Liskov替换原理。


原因(为什么您的子类型实现不违反Liskov替代原理)很简单:无法证明Task::Close()的实际作用。当然,ProjectTask::Close()会在Status == Status.Started时引发异常,但Status = Status.Closed中的Task::Close()可能会抛出异常。

#7 楼

是的,这是违反规定。

我建议您将层次结构向后移。如果不是每个Task都可关闭,则close()不属于Task。也许您想要一个所有非CloseableTask都可以实现的接口ProjectTasks

评论


每个任务都是可以关闭的,但并非在每种情况下都可以关闭。

– Paul T Davies
2012年10月16日20:46

这种方法对我来说似乎很冒险,因为人们可以编写代码,期望所有Task都实现ClosableTask,尽管它确实可以对问题进行准确建模。我讨厌这种方法和状态机,因为我讨厌状态机。

–吉米·霍法(Jimmy Hoffa)
2012年10月16日在20:47

如果Task本身并不实现CloseableTask,那么他们正在某个地方进行不安全的转换,甚至调用Close()。

–汤姆G
2012年10月16日在20:49

@TomG这就是我担心的

–吉米·霍法(Jimmy Hoffa)
2012年10月16日20:52

已经有一个状态机。该对象处于错误状态,因此无法关闭。

–卡兹
2012年10月17日在3:04

#8 楼

除了是LSP问题外,似乎它正在使用异常来控制程序流(我必须假设您在某个地方捕获了这个微不足道的异常,并执行了一些自定义流程,而不是使应用崩溃。)

这似乎是实现TaskState的State模式并让状态对象管理有效转换的好地方。

#9 楼

在这里,我缺少与LSP和按合同设计有关的重要事项-在前提条件中,呼叫者的责任是确保满足前提条件。在DbC理论中,被调用的代码不应验证前提条件。
合同应指定何时可以关闭任务(例如CanClose返回True),然后调用的代码应确保在满足关闭条件之前调用该条件()。

评论


合同应规定企业需要采取的任何行为。在这种情况下,在启动的ProjectTask上调用时,Close()将引发异常。这是一个后置条件(它表示方法被调用之后发生的事情),并且实现它是被调用代码的责任。

–停止伤害莫妮卡
18年7月26日在18:19



@Goyo是的,但是正如其他人所说的那样,子类型中引发了异常,该异常增强了前提条件,因此违反了(隐含的)契约,即调用Close()只是关闭了任务。

– Ezoela Vacca
18年7月26日在18:29



哪个前提?我没看到。

–停止伤害莫妮卡
18年7月26日在18:34

@Goyo检查接受的答案,例如:)在基类中,Close没有任何先决条件,它被调用并关闭任务。但是,在孩子中,存在状态不被启动的先决条件。正如其他人指出的那样,这是更严格的标准,因此行为不可替代。

– Ezoela Vacca
18年7月26日在18:37

没关系,我找到了问题的前提。但是,调用代码检查前提条件并在不满足条件时引发异常并没有错(在DbC方面)。这称为“防御性编程”。此外,如果有一个后置条件说明在这种情况下不满足前置条件时会发生什么情况,则实现必须验证前置条件以确保满足后置条件。

–停止伤害莫妮卡
18年7月26日在20:00

#10 楼

是的,这显然是对LSP的违反。不管您在基类中记录了什么文档或将代码移至哪个抽象级别,子条件中的前提条件都仍然得到加强,因为您向其中添加了“无法关闭启动的项目任务”部分。这不是您可以通过变通办法解决的问题,您需要一个不违反LSP的模型(或者我们需要放宽“无法加强前提条件”约束)。如果要避免在这种情况下违反LSP,则使用装饰器模式。它可能会起作用,我不知道。