【问题标题】:Inheritance and Liskov substitution principle继承和 Liskov 替换原则
【发布时间】:2014-06-05 15:43:25
【问题描述】:

在创建班级结构时,我正在努力遵守 Liskov 替换原则。我想在 Day 类中存储一组日历项目。需要有几种不同类型的 CalendarItem,例如:

约会项目
注意事项
轮播项目

它们都有一些共同的功能,这些功能存在于抽象基类 CalendarItem 中:

public abstract class CalendarBaseItem
{
  public string Description { get; private set; }
  public List<string> Notes { get; private set; }
  public TimeSpan StartTime { get; private set; }
  public TimeSpan EndTime { get; private set; }
  public int ID { get; private set; }
  public DateTime date { get; private set; }

  code omitted...
}

但例如 RotaItem 有一些额外的功能:

public class RotaItem : CalendarBaseItem
{
    public string RotaName { get; private set; }
    private bool spansTwoDays;

    public bool spanTwoDays()
    {
        return this.spansTwoDays;
    }

}

其他类也添加了自己的逻辑等。

我为我的日课收集了一组 CalendarBaseItem:

List<CalendarBaseItem> calendarItems;

但是在回顾这一点时,我可以看到我违反了 LSP 原则,因为我必须检查并强制转换每个具体类型才能获得我想要的每个子类的功能。

如果有人能建议如何避免这个问题,我将不胜感激。我应该使用组合方法并向每个最终类添加一个 CalendarItem 类,例如

 public class RotaItem
{
    private CalendarBaseItem baseItem;
    public string RotaName { get; private set; }
    private bool spansTwoDays;

    public RotaItem(baseArgs,rotaArgs)
    {
       baseItem = new CalendarBaseItem(baseArgs);

    }

    public bool spanTwoDays()
    {
        return this.spansTwoDays;
    }

}

这里唯一的问题是我需要为我的 Day 类中的每个 Concrete CalendarItem 单独收集一个集合?

【问题讨论】:

  • 你的类设计并没有破坏 LSP——具体的类很可能有自己的方法。你的设计实际上是正确的。但是,在使用您的课程时,您很可能是doing it wrong(tm)。您如何使用这些特定于类的函数?
  • 谢谢 - 我想我被基类的集合而不是类设计所困。每个具体类都有一个集合会更好吗?日课最终将提供视图,每个具体类都依赖自己的方法来构建视图。目前,我似乎必须进行类型检查才能从我在日课中的当前 List 构建视图。我最初的设计是基于需要添加新的 Concrete 类,所以当前的集合服务于这个目的。
  • 非常正确,拥有if (item is RotaItem) {} 是完全有效的(只要您不枚举日历中的所有项目)。如果您打算拥有很多物品和/或只是想要更高级的东西,请查看visitor pattern

标签: c# design-patterns liskov-substitution-principle


【解决方案1】:

我认为您遇到的与其说是违反 Liskov 替换原则,不如说是您在大多数语言中遇到了多态性限制。

对于List&lt;CalendarBaseItem&gt; 之类的东西,编译器会推断您只是在处理CalendarBaseItem,如果CalendarBaseItem 是抽象的,这显然是不正确的——但这就是强类型语言所做的:它只是被告知 CalendarBaseItem 这就是它的使用限制。

有些模式可以让您处理这种限制。最流行的是双重分派模式:多重分派的一种特化,它将方法调用分派到运行时类型。这可以通过提供一个覆盖来实现,该覆盖在分派时会分派预期的方法。 (即“双重调度”)。由于缺乏细节,很难准确地与您的情况联系起来。但是,如果您想基于某种其他类型进行一些处理,例如:

public abstract class CalendarBaseItem
{
    abstract void Process(SomeData somedata);
//...
}

public class RotaItem : CalendarBaseItem
{
    public override void Process(SomeData somedata)
    {
        // now we know we're dealing with a `RotaItem` instance,
        // and the specialized ProcessItem can be called
        someData.ProcessItem(this);
    }
//...
}

public class SomeData
{
    public void ProcessItem(RotaItem item)
    {
        //...
    }
    public void ProcessItem(NoteItem item)
    {
        //...
    }
}

这将取代类似的东西:

var someData = new SomeData();
foreach(var item in calendarItems)
    someData.ProcessItem(item);

现在,这就是在 C# 中的“经典”方式——它涵盖了所有 C# 版本。在 C# 4 中,引入了 dynamic 关键字以允许运行时类型评估。因此,您可以做您想做的事,而无需自己编写双重调度,只需将您的项目转换为 dynamic。这会强制方法评估在运行时发生,因此将选择专门的覆盖:

var someData = new SomeData();
foreach(var item in calendarItems)
    someData.ProcessItem((dynamic)item);

这引入了您可能想要捕获和处理的潜在运行时异常——这就是为什么有些人不太喜欢这个的原因。相比之下,它目前也非常慢,因此不建议在对性能敏感的紧密循环中使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-03
    • 2019-10-29
    • 2016-08-20
    相关资源
    最近更新 更多