Question

I am struggling to adhere to Liskov substitution principle when creating my class structure. I want to have a Collection of calendar items stored within a Day class. There need to be several different type of CalendarItems e.g:

AppointmentItem
NoteItem
RotaItem

they all share some common functionality which is presnt in the abstract base class 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...
}

but then for example RotaItem has some extra functionality:

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

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

}

the other classes also add there own logic etc.

I have a collection of CalendarBaseItem for my day class:

List<CalendarBaseItem> calendarItems;

but on reviewing this I can see that I am breaking LSP principles as I have to check and cast each concrete type to get at the functionality that I desire for each subclass.

I would be grateful if someone could advise how to avoid this problem. Should I use a composition approach and add a CalendarItem class to each of the final classes e.g

 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;
    }

}

The only problem here is that I will then need a seperate collection for each Concrete CalendarItem in my Day class?

Was it helpful?

Solution

I think what you're encountering is not so much a Liskov Substitution Principle violation as you are encountering a polymorphism limitation in most languages.

With something like List<CalendarBaseItem> the compiler is inferring that you're only dealing with CalendarBaseItem which obviously can't be true if CalendarBaseItem is abstract--but that's what a strongly-typed language does: It's only been told about CalendarBaseItem so that's what it limits usage to.

There are patterns that allow you to deal with this sort of limitation. The most popular is the double-dispatch pattern: a specialization of multiple dispatch that dispatches method calls to the run-time type. This can be accomplished by providing an override, that when dispatched, dispatches the intended method. (i.e. "double dispatch"). It's hard to associate exactly to your circumstances because of the lack of detail. But, if you wanted to do some processing based on some sort of other type for example:

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)
    {
        //...
    }
}

which would replace something like:

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

Now, that's the "classical" way of doing in in C#--which spans all versions of C#. With C# 4 the dynamic keyword was introduced to allow run-time type evaluation. So, you could do what you want without having to write the double-dispatch yourself simply by casting your item to dynamic. Which forces the method evaluation to occur at run-time and thus will chose the specialized override:

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

This introduces potential run-time exceptions that you'd likely want to catch and deal with--which is why some people don't like this so much. It's also currently very slow in comparison, so it's not recommended in tight loops that are performance sensitive.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top