这是否违反了《里斯科夫换人原则》?


132

假设我们有一个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();
     }
}

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


14
非常适合违反liskov替换的T示例。在此不要使用继承,这样就可以了。
吉米·霍法

8
您可能需要将其更改为:public Status Status { get; private set; }; 否则该Close()方法可以解决。
2012年

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

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

8
@BenLee想象您有一个TaskCloser流程closesAllTasks(tasks)。显然,此过程不会尝试捕获异常。毕竟,它不是的明确合同的一部分Task.Close()。现在ProjectTask,您TaskCloser开始介绍并突然开始引发(可能是未处理的)异常。这是一个大问题!
Andres F.

Answers:


173

是的,它违反了LSP。里氏替换原则要求

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

您的示例通过增强调用该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您使前提适用于Task和,从而ProjectTask解决了LSP违规问题:

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();
    }
}

17
我不喜欢重复检查。我希望将异常抛出到Task.Close并从Close中删除虚拟。
欣快的2012年

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

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

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

5
@Euphoric:啊,我误会了。没错,这为您提供了更加清洁的解决方案。
Brian

82

是。这违反了LSP。

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

根据我的评论:

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

    public virtual bool CanClose(out String reason) {
        reason = null;
        return true;
    }
    public void Close() {
        String reason;
        if (!CanClose(out reason))
            throw new Exception(reason);

        Status = Status.Closed;
    }
}

public ProjectTask : Task {
    public override bool CanClose(out String reason) {
        if (Status != Status.Started)
        {
            reason = "Cannot close a started Project Task";
            return false;
        }
        return base.CanClose(out reason);
    }
}

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

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

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

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

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

24

Liskov替换原则指出,基类可以用他的任何子类替换,而无需更改程序的任何所需属性。由于仅ProjectTask在关闭时才会引发异常,因此必须更改程序以适应该情况,应ProjectTask使用代替Task所以这是一种侵犯。

但是,如果您修改Task其签名中的说明,即在关闭时可能引发异常,那么您就不会违反该原则。


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

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

18

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

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

但是,您确实违反了SRP。事实上,任务的状态可以改变,而政策在某些国家的某些任务应该不会改变其他国家,是两个完全不同的责任。

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

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


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

Unca'Bob回应吗?“我们不值得!我们不值得!”。无论如何... 如果每个P都希望有异常,则不会违反LSP。但是如果我们规定一个T实例不能抛出一个OpenTaskException(提示,提示),并且每个P都希望有一个异常,那么关于接口代码而不是实现代码又怎么说呢我在说什么 我不知道。我只是在对Unca'Bob的答案发表评论而感到高兴。
2013年

3
您是正确的,证明违反LSP需要三个对象。但是,如果有任何程序P,这是在没有S的正确的,但失败的除S的LSP违反存在
凯文·克莱恩

16

可能会也可能不会违反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(即,如果使用该操作,您会得到什么new Task()。),它还告诉子类设计者如果不这样做,将继承哪些行为覆盖该方法。

现在,以下关系应该成立:

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

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

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

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

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

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

6

这并不违反《里斯科夫换人原则》。

里斯科夫替代原则说:

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

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


4

是的,这是违反规定。

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


3
每个任务都是可以关闭的,但并非在每种情况下都可以关闭。
保罗·T·戴维斯

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

如果Task本身并没有实现,CloseableTask那么他们会在某个不安全的地方进行强制甚至致电Close()
汤姆G

@TomG我很害怕
Jimmy Hoffa

1
已经有一个状态机。该对象处于错误状态,因此无法关闭。
卡兹(Kaz)2012年

3

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

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


1

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


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

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

哪个前提?我没看到。
Goyo

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

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

0

是的,这明显违反了LSP。

有人在这里争论说,在基类中明确声明子类可以引发异常将使这种情况可以接受,但我认为这不是真的。不管您在基类中记录了什么文档或将代码移至哪个抽象级别,子条件中的前提条件都仍然得到加强,因为您向其中添加了“无法关闭启动的项目任务”部分。这不是您可以通过变通办法解决的事情,您需要一个不违反LSP的其他模型(或者我们需要放宽“无法加强前提条件”的约束)。

如果要避免在这种情况下违反LSP,可以尝试使用装饰器模式。这可能有效,我不知道。

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.