如何避免强制转换派生类型-违反Liskov原则

时间:2019-07-11 10:14:31

标签: c# solid-principles liskov-substitution-principle

我想避免使用将基类类型转换为派生类类型的方法,如果我想访问通用功能,但是如果我想要专用功能,那么我不能成功地做到这一点

我已经编写了代码来演示我已经尝试过的内容。

public abstract class Animal : IAnimal
{
    public void Move()
    {        
    }
}

public interface IAnimal
{
     void Move();
}

public interface IDog:IAnimal
{
    void bark();
}

public class Dog : IDog
{
    public void Move()
    {

    }

    public void bark()
    {

    }
}

static void Main(string[] args)
{
    Animal animal = null;
    IDog dog = animal as IDog;

    dog.bark(); // can access specialized method

    IAnimal puppy = new Dog();
    puppy.Move(); // can only access generic functions    
}

如何重新设计类以访问“树皮”方法而无需强制转换?

2 个答案:

答案 0 :(得分:5)

简短的回答:您不能,也不应该。

相反,您可以做的是在MakeNoise()接口中实现IAnimal方法,因为您通常希望动物发出声音。

但是,如果您坚持将Bark()保留在IDog上,则不会期望IDuck能够访问它-它应该有一个Quack()方法。向下投射到IAnimal的对象都不可用,因为您如何猜测是Duck还是Dog


我将发布一些更多的“现实生活”示例,说明为什么在编程中可能需要继承,因为您提供的示例有点像“书籍示例”,因此它也是晦涩而模糊的。

using System.Collections.Generic;

namespace ConsoleApp1
{
    public static class DocumentHandling
    {
        public static List<IAccountable> Documents;
        public static dynamic InternalService { get; set; }
        public static dynamic IRS { get; set; }

        public static void HandleDocuments()
        {
            foreach (var document in Documents)
            {
                document.Account();
            }
        }
    }

    public interface IAccountable
    {
        void Account();
    }

    public abstract class Document
    {
        public int DatabaseId { get; set; }
        public string Title { get; set; }

    }

    public abstract class DocumentWithPositions : Document
    {
        public int[] PositionsIds { get; set; }
    }

    public class Invoice : DocumentWithPositions, IAccountable
    {
        public void Account()
        {
            var positions = DocumentHandling.InternalService.PreparePositions(this.PositionsIds);
            DocumentHandling.IRS.RegisterInvoice(positions);
        }
    }

    public class Receipt : DocumentWithPositions, IAccountable
    {
        public void Account()
        {
            Invoice invoice = DocumentHandling.InternalService.ConvertToReceipt(this);
            invoice.Account();
        }
    }
}

看看我如何将InvoiceReceipt这两个文档都填充到一个列表中(因为它们被向下转换为IAccountable)?现在,即使它们的具体实现对会计过程的处理方式不同,我也可以一次全部对其进行会计处理。

答案 1 :(得分:0)

让我们先谈谈Liskov Substitution Principle,然后再谈OOP和继承。

首先,让我们谈谈Abstract Data Types。在她的论文中,她使用了来自类型的对象的概念。

摘要数据类型(ADT)是对类型的描述,其中包含所有操作行为 ADT 的所有客户都应该知道使用它时的期望。

这是一个例子:

让我们将Stack定义为ADT

操作: pushpoptopElementsizeisEmpty

行为:

  • push 总是将元素添加到堆栈顶部!
  • size:返回堆栈中的元素数
  • pop:从堆栈顶部移除和元素。如果堆栈为空则报错
  • topElement:返回堆栈中的顶部元素。如果堆栈为空则报错
  • isEmpty:如果堆栈为空,则返回true,否则返回false

至此,我们就Stack的操作及其行为方式进行了描述。在这里我们不是在谈论个案,也不是在讨论具体的实现。这就是 抽象数据类型

现在让我们建立类型层次结构。在C#中,接口和类都是类型。由于接口仅定义操作,因此它们是不同的,因此从某种意义上说它们是契约。它们定义了ADT的操作。通常人们会假设只有相互继承的类才能定义类型层次结构。确实可以将互相继承的类称为 Superclass Baseclass Subclass ,但是从 Types的角度来看对于接口和类,我们确实有 Supertype Subtype ,因为它们都定义了类型。

注意:为简单起见,我将跳过方法实现中的错误检查

// interfaces are types. they define a contract so we can say that
// they define the operations of an ADT
public interface IStack<T> {
    T Top();
    int Size();
    void Push(T element);
    void Pop();
    bool IsEmpty();
    }

// the correct term here for C# whould be 'implements interface' but from 
// point of view of ADTs and *Types* ListBasedStack is a *Subtype*
public class ListBasedStack<T> : IStack<T> {

    private List<T> mElements;

    public int Size() { return mElements.Count; }

    public T Top() { mElements(mElements.Count - 1); }

    public void Push(T element) { mElements.Add(element); }

    public void Pop() { mElements.Remove(mElements.Count - 1); }

    public bool IsEmpty() { return mElements.Count > 0; }
}

public class SetBasedStack<T> : IStack<T> {

    private Set<T> mElements;

    public int Size() { return mElements.Count; }

    public T Top() { mElements.Last(); }

    public void Push(T element) { mElements.Add(element); }

    public void Pop() { mElements.RemoveLast(); }

    public bool IsEmpty() { return mElements.Count > 0; }
}

请注意,我们有两个 Aem 相同的子类型。现在让我们考虑一个测试案例。

public class Tests { 

    public void TestListBasedStackPush() {
        EnsureUniqueElementsArePushesToAStack(new ListBasedStack<int>());
    }

    public void TestSetBasedStackPush() {
        EnsureUniqueElementsArePushesToAStack(new SetBasedStack<int>());
    }

    public void EnsureUniqueElementsArePushesToAStack(IStack<int> stack) {

          stack.Push(1);
          stack.Push(1);

          Assert.IsTrue(stack.Size() == 2);
   }
}

结果是:

  • TestListBasedStackPush:通过
  • TestSetBasedStackPush:失败!

SetBasedStack违反了push的规则: 总是将元素添加到堆栈的顶部! ,因为集合只能包含唯一的元素元素,第二个stack.Push(1)不会将新元素添加到堆栈中。

这违反了LSP。

现在介绍示例,并输入诸如IAnimalDog之类的层次结构。当您处于 正确的摘要级别 时,一种类型的行为应与预期的一致。如果确实需要Dog,请使用Dog。如果您确实需要IAnimal,请使用IAnimal

如果有Bark,您如何访问IAnimal 您不要! 。您处于错误的抽象级别。如果确实需要Dog,请使用Dog。如果需要,请投射。

public class Veterenerian {
    public void ClipDogNails(IAnimal animal) { } // NO!
    public void ClipDogNails(Dog dog) { } // YES!
}

private Veterenerian mOnDutyVeterenerian;
private List<IAnimal> mAnimals;

public ClipAllDogsNails() {
     // Yes
     foreach(var dog in mAnimals.OffType<Dog>()) {
         mOnDutyVeterenerian.ClipDogNails(dog);
     }
     // NO
     foreach(var animal in mAnimals) {
         mOnDutyVeterenerian.ClipDogNails(animal);
     }
}

您需要投射吗?有时是的。如果最好不这样做呢?是的,大多数时候都是这样。

您如何解决上述问题?您可以将Dog Clip做成自己的指甲。您是否正在将方法ClipNails添加到IAnimal,并使仅带有指甲的动物实现此方法,而使其他动物子类保留此方法为空?没有!因为在IAnimal 的抽象级别上没有意义,所以它也违反了LSP。另外,如果您执行此操作,可以致电animal.ClipNails(),这样就可以了,但是如果您有时间表说狗应该在其他动物的星期五修剪指甲,那么星期一又会卡住,因为您可以让所有动物都修剪它们的指甲,不仅是狗。

有时,一个 Type 的对象将被另一个 Type 的对象使用。有些操作在类型中没有意义。这个例子说明了狗怎么不能剪指甲。应该由Veterenerial完成。

但是,我们确实需要在IAnimal抽象级别上进行工作。 Veterenerian Clinic中的所有事物都是动物。但是有时需要对动物的特定类型(在这种情况下为operations)执行某些Dog,因此我们确实需要过滤<按其 类型 的strong> 动物

但这与上面的Stack示例完全不同。

下面是一个示例,说明何时不应该使用强制类型转换,并且客户端代码不应包含具体实现的情况:

public abstract class Serializer {
    public abstract byte[] Serialize(object o);
}

public class JSONSerializer : Serializer {
    public override byte[] Serialize(object o) { ... }
}

public class BinarySerializer : Serializer {
    public override byte[] Serialize(object o) { ... }
}

public void DoSomeSerialization(Serializer serializer, Event e) {
    EventStore.Store(serializer.Serialize(e));
}

DoSomeSerialization方法 不应该关心传递给它的串行器。您可以通过符合Serializer规范的 任何 Serializer,它应该可以工作。这就是具有多个实现的抽象的意义。 DoSomeSerializationSerializer级别上工作。我们可以将Serializer定义为ADT。从Serializer派生的所有类都应遵守ADT的规范,并且系统运行正常。这里没有强制转换,因为问题不同,因此无需强制转换。