Принцип наследования и подстановки Лискова

Я изо всех сил стараюсь придерживаться принципа замещения Лискова при создании своей классовой структуры. Я хочу иметь коллекцию элементов календаря, хранящуюся в классе Day. Должно быть несколько разных типов CalendarItem, например:

Назначение
NoteItem
RotaItem

все они имеют общую функциональность, которая присутствует в абстрактном базовом классе 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;
    }

}

Единственная проблема здесь в том, что тогда мне понадобится отдельная коллекция для каждого Concrete CalendarItem в моем классе Day?


person Luthervd    schedule 21.04.2014    source источник
comment
Вы не нарушаете LSP своим дизайном класса - у конкретных классов вполне могут быть собственные методы. Ваш дизайн действительно правильный. Однако вы, скорее всего, doing it wrong(tm) при использовании своих классов. Как вы используете эти специфичные для класса функции?   -  person DarkWanderer    schedule 21.04.2014
comment
Спасибо - я думаю, что я застрял с коллекцией базового класса, а не с дизайном класса. Было бы лучше иметь коллекцию для конкретного класса. В конечном итоге дневной класс будет кормить представление, и каждый конкретный класс будет полагаться на свои собственные методы для построения представления. В настоящее время кажется, что мне нужно будет выполнить проверку типов, чтобы построить представления из текущего списка ‹CalendarBaseItem›, который у меня есть в дневном классе. Мой первоначальный дизайн был основан на необходимости добавления новых классов Concrete в дальнейшем, поэтому текущая коллекция служит этой цели.   -  person Luthervd    schedule 21.04.2014
comment
Это довольно правильно, наличие if (item is RotaItem) {} вполне допустимо (если вы не перечисляете все элементы в календаре). Если вы планируете иметь много товаров и / или просто хотите что-то более продвинутое, проверьте шаблон посетителя   -  person DarkWanderer    schedule 21.04.2014


Ответы (1)


Я думаю, что вы сталкиваетесь не столько с нарушением принципа подстановки Лискова, сколько с ограничением полиморфизма в большинстве языков.

С чем-то вроде List<CalendarBaseItem> компилятор делает вывод, что вы имеете дело только с 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);

Это вводит потенциальные исключения во время выполнения, которые вы, вероятно, захотите поймать и обработать - вот почему некоторым это так не нравится. В настоящее время он также очень медленный по сравнению с ним, поэтому его не рекомендуется использовать в жестких циклах, которые чувствительны к производительности.

person Peter Ritchie    schedule 21.04.2014