Inheritance is not necessarily the best way to model problems where instances of types can change over time.
You may want to consider using composition instead. Something like:
class Task
{
private TaskDetail m_Detail;
public TaskDetail Detail { get { return m_Detail; } }
}
abstract class TaskDetail { ... }
class PhoneCallDetail : TaskDetail { ... }
class FaxDetail : TaskDetail { ... }
class EmailDetail : TaskDetail { ... }
Tasks would not change when their task details shift from one type to another. You would also need to implement some utility code to convert between the different task types, as appropriate.
So example use might look like:
Task theTask = new Task( ... );
theTask.ConvertToEmail(); // internally establishes this as an email task
EmailDetail detail = (EmailDetail)theTask.Detail;
detail.EmailAddress = "[email protected]";
theTask.ConvertToFax(); // may transfer or lose some detail...
FaxDetail faxDetail = (FaxDetail)theTask.Detail;
faxDetail.FaxDate = DateTime.Now;
// and so on.
The primary disadvantage to the approach above is that consumers of the Task
class must use runtime checks to determine the type of detail associated with the task before operating on it; which also then requires casting of the detail property everywhere:
Task someTask = ...;
if( someTask.Detail is EmailDetail )
{
EmailDetail detail = (EmailDetail)someTask.Detail;
/* operate on email detail ... */
}
else if( someTask.Detail is FaxDetail )
{
FaxDetail detail = (FaxDetail)someTask.Detail;
/* operate on fax detail ... */
}
As the number of different subtypes grows, this approach becomes harder to maintain and evolve. If the number of subtypes is small, and likely to be stable over time, then it may be a reasonable choice.
In general, it's difficult to model situations like these - and you often have to compromise based on what persistence provider you use, how many different detail types they are, and what use cases you intend to support involving conversions from one detail type to another.
Another design approach that is often employed in such cases is Key-Value-Coding. This approch uses a dictionary of keys/values to model the various data elements of different kinds of details. This allows details to be very flexible, at the cost of less compile-time safety. I try to avoid this approach when possible, but sometimes it does model certain problem domains better.
It's actually possible to combine key-value coding with a more strongly typed approach. This allows details to expose their properties (usually for read-only purposes) without requiring the caller to perform runtime checks or casts:
abstract class TaskDetail
{
public abstract object this[string key] { get; }
}
public class FaxDetail : TaskDetail
{
public string FaxNumber { get; set; }
public DateTime DateSent { get; set; }
public override object this[string key]
{
get
{
switch( key )
{
case "FaxNumber": return FaxNumber;
case "DateSent": return DateSent;
default: return null;
}
}
}
}
public class EmailDetail : TaskDetail
{
public string EmailAddress { get; set; }
public DateTime DateSent { get; set; }
public override object this[string key]
{
get
{
switch( key )
{
case "EmailAddress": return EmailAddress;
case "DateSent": return DateSent;
default: return null;
}
}
}
}
// now we can operate against TaskDetails using a KVC approach:
Task someTask;
object dateSent = someTask.Detail["DateSent"]; // both fax/email have a DateSent
if( dateSent != null )
// ...