L'héritage est pas nécessairement la meilleure façon de modéliser des problèmes où les instances de types peuvent changer au fil du temps.
Vous pouvez utiliser la composition à la place. Quelque chose comme:
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 { ... }
Les tâches ne changeraient pas lorsque les détails de leurs tâches passeraient d'un type à un autre. Vous devrez également implémenter un code d'utilitaire pour convertir entre les différents types de tâches, le cas échéant.
Donc exemple d'utilisation pourrait ressembler à:
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.
Le principal inconvénient de cette approche est ci-dessus que les consommateurs de la classe Task
doivent utiliser des contrôles d'exécution pour déterminer le type de détail associé à la tâche avant de faire fonctionner sur elle ; qui a également nécessite alors coulée de la propriété détail partout:
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 ... */
}
Comme le nombre de différents sous-types se développe, cette approche devient plus difficile de maintenir et d'évoluer. Si le nombre de sous-types est faible et susceptible d'être stable au fil du temps, il peut s'agir d'un choix raisonnable. En général, il est difficile de modéliser des situations comme celles-ci - et vous devez souvent faire des compromis en fonction du fournisseur de persistance utilisé, du nombre de types de détails et des cas d'utilisation impliquant des conversions d'un détail tapez à un autre.
Une autre approche de conception souvent utilisée dans de tels cas est Key-Value-Coding. Cette approche utilise un dictionnaire de clés/valeurs pour modéliser les différents éléments de données de différents types de détails. Cela permet aux détails d'être très flexibles, au prix de moins de sécurité au moment de la compilation. J'essaie d'éviter cette approche lorsque cela est possible, mais parfois, elle modélise mieux certains domaines de problèmes.
Il est en fait possible de combiner le codage de valeurs-clés avec une approche plus fortement typée.Cela permet à des détails d'exposer leurs propriétés (généralement à des fins de lecture seule) sans nécessiter l'appelant d'effectuer des contrôles d'exécution ou lance:
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)
// ...