这是否违反了里氏替换原则,如果是的话,我该怎么办?

4
使用场景:我正在使用数据模板将视图与视图模型匹配。 数据模板通过检查提供的具体类型的最派生类型来工作,它们不会看它提供了哪些接口,因此我必须在没有接口的情况下进行操作。
这里简化了示例并省略了NotifyPropertyChanged等内容,但在现实世界中,视图将绑定到Text属性。 为简单起见,想象一下一个带有TextBlock的视图将绑定到ReadOnlyText,而带有TextBox的视图将绑定到WritableText。
class ReadOnlyText
{
    private string text = string.Empty;

    public string Text
    {
        get { return text; }
        set
        {
            OnTextSet(value);
        }
    }

    protected virtual void OnTextSet(string value)
    {
        throw new InvalidOperationException("Text is readonly.");
    }

    protected void SetText(string value)
    {
        text = value;
        // in reality we'd NotifyPropertyChanged in here
    }
}

class WritableText : ReadOnlyText
{
    protected override void OnTextSet(string value)
    {
        // call out to business logic here, validation, etc.
        SetText(value);
    }
}

通过重写OnTextSet并改变其行为,我是否违反了LSP?如果是这样,有更好的方法吗?

通过重写OnTextSet并改变其行为,是否违反了{{LSP}}?如果是,有更好的方法吗?


李斯科夫替换原则,是面向对象编程中的五个设计原则之一。这个原则定义为:如果S是T的子类型,那么在不改变程序的正确性(即不改变预期的行为)的前提下,任何一个T类型的对象可以被替换成S类型的对象。这个原则由芭芭拉·李斯科夫于1987年首次提出,并且她和Jeannette Wing一起发展了这个原则。这个原则对于保证软件系统的灵活性、可扩展性和可维护性非常重要。 - Christopher Klein
@SomeMiscGuy:抱歉,已添加链接 :) - Scott Whitlock
顺便说一下,可以使用DataTemplateSelector根据实现接口的类来解析数据模板。这个对我很有用:http://complexdatatemplates.codeplex.com/ - Dan Bryant
4个回答

9
LSP原则指出,子类应该可以替换其父类(详见stackoverflow问题here)。你需要问自己的问题是,“可写文本是只读文本的一种类型吗?”答案显然是否定的,实际上它们是互斥的。因此,是的,这段代码违反了LSP原则。但是,可写文本是可读文本的一种类型(不是只读文本)吗?答案是肯定的。因此,我认为答案是要看你在每种情况下想做什么,并可能稍微改变抽象,如下所示:
class ReadableText
{
    private string text = string.Empty;
    public ReadableText(string value)
    {
        text = value;
    }

    public string Text
    {
        get { return text; }
    }
}          

class WriteableText : ReadableText
{
    public WriteableText(string value):base(value)
    {

    }

    public new string Text
    {
        set
        {
            OnTextSet(value);
        }
        get
        {
            return base.Text;
        }
    }
    public void SetText(string value)
    {
        Text = value;
        // in reality we'd NotifyPropertyChanged in here       
    }
    public void OnTextSet(string value)
    {
        // call out to business logic here, validation, etc.       
        SetText(value);
    }
}     

只是为了明确,我们使用Writeable类中Text属性上的new关键字来隐藏Readable类中的Text属性。
来自http://msdn.microsoft.com/en-us/library/ms173152(VS.80).aspx: 当使用new关键字时,新的类成员将被调用,而被替换的基类成员则不会被调用。这些基类成员被称为隐藏成员。如果将派生类的实例转换为基类的实例,则仍然可以调用隐藏的类成员。

这段代码不能完全编译通过,但可以进行修改以使其正常工作。我从未想过在派生类中为一个具有基类getter的属性定义setter。我学到了新知识。这将起作用。谢谢! - Scott Whitlock
有趣的是,我需要在派生类中将Text属性声明为“new”以避免编译器警告,但我认为这并不是真正的“new”,因为我没有覆盖getter方法。 - Scott Whitlock
我不相信你想要将SetText和OnTextSet设为公开的。 - Steve Ellinger
我编辑了这篇文章,加入了新的关键字。在 C# 中,它告诉我们我们正在隐藏 Readable 类中的 Text 属性,因为该属性只能被读取(getter),但在 Writeable 类中,该属性既可读(getter)又可写(setter)。 - Brandon
@Steve Ellinger:没错,在我的实现中,基类处理更新属性并在业务逻辑更改值时通知视图。 - Scott Whitlock

8

只有当 ReadOnlyText.OnTextSet() 的规范承诺会抛出异常时才会发生。

想象一下这样的代码:

public void F(ReadOnlyText t, string value)
{
    t.OnTextSet(value);
}

如果这没有出现错误,对你来说有意义吗? 如果没有,那么WritableText不能替代。

我认为WritableText应该继承自Text。 如果在ReadOnlyText和WritableText之间有一些共享代码,请将其放入Text中或放入另一个类中,它们都从中继承(继承自Text)


你对于我的具体问题是正确的,但Brandon指出了我对属性设置器和获取器的一个误解,这使我更优雅地解决了问题。感谢提供信息。 - Scott Whitlock

2

我认为这取决于合同。

如果ReadOnlyText的合同规定“任何尝试设置文本都会引发异常”,那么您肯定违反了LSP(Liskov Substitution Principle,里氏替换原则)。

如果没有这样的规定,您的代码仍然存在一个尴尬之处:对只读文本进行了setter操作。

在特定情况下,这是可以接受的“非正常化”操作。到目前为止,我还没有找到一种不依赖大量代码的更好方法。大多数情况下,干净的界面应该是:

IThingieReader
{
    string Text { get; }
    string Subtext { get; }
    // ...
}

IThingieWriter
{
    string Text { get; set; }
    string Subtext { get; set; }
    // ...
}

…并在适当时候实现接口。然而,如果您需要处理例如Text是可写的而Subtext不可写的情况,则会出现问题,并且对于许多对象/属性来说这是一种痛苦的操作。


正如我所说,这是理想的,但我不能使用接口,因为数据模板不会以接口为键,它们需要一个具体类型。 - Scott Whitlock

0

是的,它确实会这样做。如果受保护的重写 void OnTextSet(string value) 也引发了一个类型为“InvalidOperationException”或继承自它的异常,则不会这样做。

您应该有一个基类 Text,ReadOnlyText 和 WritableText 都从它继承。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接