覆盖虚拟布尔纯方法而不破坏LSP

时间:2018-04-26 15:04:04

标签: oop design-patterns solid-principles liskov-substitution-principle

例如,我们有以下结构:

class Base
{
    [pure]
    public virtual bool IsValid(/*you can add some parameters here*/)
    {
       //body
    }
}

class Child : Base
{
    public override bool IsValid(/*you can add some parameters here*/)
    {
       //body
    }
}

您能否请Base::IsValid()Child::IsValid()填写不同的机构,但不与LSP冲突?让我们想象它只是分析的方法,我们无法改变实例的状态。 我们能做到吗? 我对任何一个例子感兴趣。 在一般情况下,我试着了解虚拟(体型)布尔方法是否反模式

4 个答案:

答案 0 :(得分:5)

LSP的想法并不禁止子类的多态性。相反,它强调允许改变什么,不改变什么。 一般来说,这意味着:

  1. 任何覆盖函数都接受并返回相同类型的重写函数;包括可能抛出的异常(输入类型可能会扩展被覆盖的类型和输出类型可能会缩小它们 - 这仍将保持这种限制)。
  2. "历史规则" - " Base" Child对象的一部分不能被Child的函数更改为使用Base类函数永远无法访问的状态。因此,期望Base对象的函数永远不会得到意外的结果。
  3. 不得在儿童中更改基地的不变量。也就是说,关于基类行为的任何一般假设都必须由Child保留。
  4. 两个第一颗子弹的定义非常明确。 "不变量"更多的是感觉问题。例如,如果实时环境中的某些类要求其所有函数在某个恒定时间内运行,则其子类型中的所有重写函数也必须遵守该要求。

    在你的情况下,IsValid()意味着什么,那就是"某些东西"必须保持所有儿童类型。例如,假设您的Base类定义了一个产品,而IsValid()则告诉该产品是否有效销售。每件产品的确切含义可能会有所不同。例如,它必须设置其价格才能有效销售。但是儿童产品在出售之前还必须经过电力测试。

    在这个例子中,我们保留了所有要求:

    1. 功能的输入和输出类型不会更改。
    2. Child对象的Base-part的状态不会以Base类无法预期的方式更改。
    3. 保留班级的不变量:仍然不能出售没有价格的子对象;无效的含义仍然相同(不允许出售),只是以与儿童相匹配的方式计算。
    4. 您可以获得更多解释here

      ===

      编辑 - 根据笔记

      的其他一些解释

      多态性的整个想法是每个子类型都以不同的方式完成相同的功能。 LSP不违反多态性,但描述了多态性应该处理什么。 特别是,LSP要求在代码需要Child的情况下使用任何子类型Base,并且Base的任何假设都适用于他的任何Child。在上面的示例中,IsValis() 表示"有价格"。相反,它恰恰意味着:产品是否有效?在某些情况下,有一个价格就足够了。另外,它还需要电力检查,而在其他情况下,它可能还需要一些其他属性。如果Base类的设计者不要求通过设置价格使产品变为有效,而是将IsValid()作为单独的测试,则不会发生违反LSP的情况。什么样的例子会造成这种违规行为?一个例子,一个人询问一个对象IsValid(),然后调用一个不应该改变有效性的基类的函数,并且该函数将Child更改为无效了。这违反了LSP的历史规则。这里由其他人提供的已知示例是矩形的子矩形。但只要相同的函数调用序列不需要特定的行为(再次 - 没有定义设置价格使产品有效;它恰好在某些类型中就是这样) - LSP按需要保留

答案 1 :(得分:2)

LSP背后的基本思想不是阻碍OverrideBase方法的能力,而是为了避免改变Base类的内部状态(改变基类类的数据成员) )以Base类不具备的方式。

  

它只是说明:任何继承另一种类型的类型(类)必须是   替代该类型,以便Child类继承Base   class,然后是代码中Base类的对象所在的任何位置   我们可以在不更改的情况下提供Child类对象   系统行为。

然而,它并不妨碍我们修改Child类的成员。违反这个例子的着名例子是Square / Rectangle问题。您可以找到示例here的详细信息。

在您的情况下,由于您只是在分析IsValid()中的某些数据而不修改Base类的内部状态,因此不应该违反LSP。

答案 2 :(得分:2)

首先,你的回答:

class Base
{
    [pure]
    public virtual bool IsValid()
    {
       return false;
    }
}

class Child : Base
{
    public override bool IsValid()
    {
       return true;
    }
}

基本上,LSP说(它是"子类型&#34的定义):

  

如果对于类型S的每个对象o1,存在类型为T的对象o2,使得对于以T表示的所有程序P,当o1代替o2时P的行为不变,则S是子类型T.(Liskov,1987)

"但我不能用o1类型Base替换o2类型的Child,因为它们的行为明显不同!& #34;为了解决这个问题,我们不得不绕道而行。

什么是子类型?

首先,请注意Liskov不仅讨论类,还讨论类型。类是类型的实现。类型有好的和坏的实现。我们会尝试区分它们,特别是在涉及子类型时。

Liskov替换原则背后的问题是:什么是子类型?通常,我们假设子类型是其超类型的特化及其功能的扩展:

> The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra (Liskov, 1987)

另一方面,大多数编译器都假设子类型是一个至少具有相同方法(同名,相同签名,包括协方差和异常)的类,无论是继承还是重新定义(或第一次定义)和标签(inheritsextends,...)。

但这些批评不完整并导致错误。以下是两个臭名昭着的例子:

  • SortedList是(?)List的子类型:它表示已排序的列表(特化)。
  • Square是(?)Rectangle的子类型:它代表一个四边相等的矩形(专业化)。

为什么SortedList不是List?由于List类型的语义。类型不仅是签名的集合,方法也具有语义。 通过语义,我的意思是对象的所有授权使用(记住维特根斯坦:"一个词的含义是它在语言中的用法")。例如,您希望在放置它的位置找到一个元素。但是,如果列表始终排序,则新插入的元素将在其#34;右侧"地点。因此,您无法在放置它的位置找到此元素。

为什么Square不是Rectangle?想象一下你有一个方法set_width:有一个正方形,你也必须改变高度。但set_width的语义是改变宽度但保持高度不变。

(一个正方形不是一个矩形?这个问题有时引发热烈的讨论,因此我将详细阐述这个主题。我们都知道正方形是一个矩形。但在纯数学的天空中这是真的,其中对象是不可变的。如果你定义ImmutableRectangle(具有固定的宽度,高度,位置,角度和计算的周长,面积,......),那么ImmutableSquare将是ImmutableRectangle的子类型根据LSP。乍一看,这样的不可变类似乎没有用,但有一种方法可以解决这个问题:用创建新对象的方法替换setter,就像在任何函数式语言中一样。例如,ImmutableSquare.copyWithNewHeight(h)将返回一个新的... ImmutableRectangle,其高度为h,宽度为广场的size。)

我们可以使用LSP来避免这些错误。

为什么我们需要LSP?

但为什么在实践中,我们是否需要关心LSP?因为编译器不捕获类的语义。您可能有一个不是子类型实现的子类。

对于Liskov(和Wing,1999),类型规范包括:

  
      
  • 类型名称
  •   
  • 类型空间的描述
  •   
  • 类型的不变量和历史属性的定义;
  •   
  • 对于每种类型的方法:      
        
    • 它的名字;
    •   
    • 其签名(包括发出信号的例外情况);
    •   
    • 其在前置条件和后置条件方面的行为
    •   
  •   

如果编译器能够为每个类强制执行这些规范,它将能够(在编译时或运行时,取决于规范的性质)告诉我们:"嘿,这不是一个子类型!&#34 ;.

(实际上,有一种编程语言试图捕获语义:Eiffel。在Eiffel中,不变量,前提条件和后置条件是类定义的重要部分。因此,你不要&# 39; t必须关心LSP:运行时会为你做。这很好,但是Eiffel也有局限性。这种语言(任何语言?)都不足以表达定义完整的语义isValid(),因为此语义不包含在前/后条件或不变量中。)

现在,回到示例。这里,我们对isValid语义的唯一指示是方法的名称:如果对象有效则返回true,否则返回false。您显然需要上下文(可能还有详细的规范或领域知识)来了解什么是无效的。

实际上,我可以想象任何类型为Base的对象有效的情况,但Child类型的所有对象都无效(请参阅答案顶部的代码)。例如。将Base替换为Passport,将Child替换为FakePassword(假设假密码是密码......)。

因此,即使Base类说:"我有效",Base类型说:"我的几乎所有实例都有效,但那些无效的人应该说出来!"这就是为什么你有Child类实现Base类型(以及派生Base类)的原因:"我无效&#34 ;

一个更有趣的例子

但是我认为你选择的例子并不是检查前/后条件和不变量的最佳例子:因为函数是纯粹的,所以它可能不会破坏任何不变量;因为返回值是布尔值(2个值),所以没有有趣的后置条件。如果您有一些参数,唯一可以拥有的是一个有趣的前置条件。

让我们举一个更有趣的例子:一个集合。在伪代码中,您有:

abstract class Collection {
    abstract iterator(); // returns a modifiable iterator
    abstract size();

    // a generic way to set a value
    set(i, x) {
        [ precondition: 
            size: 0 <= i < size() ]

        it = iterator()
        for i=0 to i:
            it.next()
        it.set(x)

        [ postcondition:
            no_size_modification: size() = old size()
            no_element_modification_except_i: for all j != i, get(j) == old get(j)
            was_set: get(i) == x ]
    }

    // a generic way to get a value
    get(i) {
        [ precondition:
            size: 0 <= i < size() ]

        it = iterator()
        for i=0 to i:
            it.next()
        return it.get()

        [ postcondition:
            no_size_modification: size() = old size()
            no_element_modification: for all j, get(j) == old get(j) ]
    }

    // other methods: remove, add, filter, ...

    [ invariant: size_positive: size() >= 0 ]
}

此集合有一些抽象方法,但setget方法已经是一种方法。此外,我们可以说他们 可以用于链表,但不适用于由数组支持的列表。让我们尝试为随机访问集合创建更好的实现:

class RandomAccessCollection {
    // all pre/post conditions and invariants are inherited from Collection.

    // fields:
    // self.count = number of elements.
    // self.data = the array.

    iterator() { ... }
    size() { return self.count; }

    set(i, x) { self.data[i] = x }

    get(i) { return self.data[i] }

    // other methods
}

很明显,getsetRandomAccessCollection的语义符合Collection类的定义。特别是,满足所有前/后条件和不变量。换句话说,满足LSP的条件,因此,LSP得到尊重:我们可以在每个程序中用模拟替换Collection类型的任何对象类型为RandomAccesCollection的对象,而不会破坏程序的行为。

结论

如您所见,尊重LSP比破坏它更容易。但有时我们会破坏它(例如,尝试创建继承SortedRandomAccessCollection)的RandomAccessCollection。 LSP的清晰表述有助于我们缩小出错的地方以及应该采取什么措施来纠正设计。

更一般地说,如果基类具有足够的肉来实现方法,则虚拟(身体)布尔方法不是反模式。但是如果基类是如此抽象,以至于每个子类都必须重新定义方法,那么将方法保留为抽象。

参考

Liskov有两篇主要的原始论文:Data Abstraction and Hierarchy(1987)和Behavioral Subtyping Using Invariants and Constraints(1994,1999,与J. M. Wing合着)。请注意,这些是理论论文。

答案 3 :(得分:0)

Barbara Liskov,Jeannette Wing 1994:
“设q(x)是关于类型T的对象x可证明的属性。那么对于类型S的对象y,q(y)应该是可证明的 其中S是T“的子类型 简单来说:当代码的行为不改变时,Basetypes可以被Childtypes替换。这意味着一些固有的限制。
以下是一些例子:

  1. 例外

    class Duck { void fly() {} }
    class RedheadDuck : Duck { void fly() {} }
    class RubberDuck : Duck { void fly() { throw new CannotFlyException(); }}
    class LSPDemo
    {
       public void Main()
       {
          Duck p = new Duck ();
          p.fly(); // OK
          p = new RedheadDuck();
          p.fly(); // OK
          p = new RubberDuck();
          p.fly(); // Fail, not same behavior as base class
       }
    }
    
  2. 方法论证的反演方法

    class Duck { void fly(int height) {} } 
    class RedheadDuck : Duck { void fly(long height) {} } 
    class RubberDuck : Duck { void fly(short height) {} }
    class LSPDemo 
    { 
       public void Main() 
       { 
          Duck p = new Duck(); p.fly(int.MaxValue);
          p = new RedheadDuck(); p.fly(int.MaxValue); // OK argumentType long(Subtype) >= int(Basetype)
          p = new RubberDuck(); p.fly(int.MaxValue); // Fail argumentType short(Subtype) < int(Basetype) 
       } 
    }
    
  3. 返回类型的协方差

    class Duck { int GetHeight() { return int.MaxValue; } } 
    class RedheadDuck: Duck { short GetHeight() { return short.MaxValue; } } 
    class RubberDuck: Duck { long GetHeight() { return long.MaxValue; } }
    class LSPDemo { 
       public void Main() 
       { 
          Duck p = new Duck(); int height = p.GetHeight();
          p = new RedheadDuck(); int height = p.GetHeight(); // OK returnType short(Subtype) <= int(Basetype)
          p = new RubberDuck(); int height = p.GetHeight(); // Fail returnType long(Subtype) > int(Basetype) 
       } 
    }
    
  4. 历史约束

     class Duck 
     { 
       protected string Food { get; private set; } 
       protected int Age { get; set; } 
       public Duck(string food, int age) 
       { 
          Food = food; 
          Age = age; 
       } 
     } 
    
     class RedheadDuck : Duck 
     { 
        void IncrementAge(int age) 
        { 
           this.Age += age; 
        } 
     } 
    
     class RubberDuck : Duck 
     { 
        void ChangeFood(string newFood) 
        { 
           this.Food = newFood; 
        } 
     } 
    
     class LSPDemo 
     { 
        public void Main() 
        { 
           Duck p = new Duck("apple", 10); 
    
           p = new RedheadDuck(); 
           p.IncrementAge(1); // OK 
    
           p = new RubberDuck(); 
           p.ChangeFood("pie"); // Fail, Food is defined as private set in base class
        } 
     }
    
  5. 以及更多......我希望你能得到这个想法。