Supposons que nous ayons une liste d'entités Task et un sous-type ProjectTask
. Les tâches peuvent être fermées à tout moment, sauf ProjectTasks
qui ne peut pas être fermé une fois qu'elles ont le statut Démarré. L'interface utilisateur doit garantir que l'option de fermeture d'un ProjectTask
démarré n'est jamais disponible, mais certaines garanties sont présentes dans le domaine:
public class Task
{
public Status Status { get; set; }
public virtual void Close()
{
Status = Status.Closed;
}
}
public class ProjectTask : Task
{
public override void Close()
{
if (Status == Status.Started)
throw new Exception("Cannot close a started Project Task");
base.Close();
}
}
Maintenant, lorsque vous appelez Close()
sur une tâche, il y a une chance que l'appel échoue s'il s'agit d'un ProjectTask
avec l'état démarré, alors qu'il ne le ferait pas s'il s'agissait d'une tâche de base. Mais ce sont les exigences de l'entreprise. Cela devrait échouer. Cela peut-il être considéré comme une violation du principe de substitution de Liskov ?
Oui, c'est une violation du LSP. Le principe de substitution de Liskov requiert que
Votre exemple rompt la première exigence en renforçant une condition préalable à l'appel de la méthode Close()
.
Vous pouvez le corriger en amenant la condition préalable renforcée au niveau supérieur de la hiérarchie d'héritage:
public class Task {
public Status Status { get; set; }
public virtual bool CanClose() {
return true;
}
public virtual void Close() {
Status = Status.Closed;
}
}
En stipulant qu'un appel de Close()
n'est valide que dans l'état où CanClose()
renvoie true
vous faites appliquer la condition préalable à Task
comme ainsi qu'à ProjectTask
, corrigeant la violation LSP:
public class ProjectTask : Task {
public override bool CanClose() {
return Status != Status.Started;
}
public override void Close() {
if (Status == Status.Started)
throw new Exception("Cannot close a started Project Task");
base.Close();
}
}
Oui. Cela viole le LSP.
Ma suggestion est d'ajouter la méthode/propriété CanClose
à la tâche de base, afin que toute tâche puisse dire si la tâche dans cet état peut être fermée. Cela peut également expliquer pourquoi. Et supprimez le virtuel de Close
.
Sur la base de mon commentaire:
public class Task {
public Status Status { get; private set; }
public virtual bool CanClose(out String reason) {
reason = null;
return true;
}
public void Close() {
String reason;
if (!CanClose(out reason))
throw new Exception(reason);
Status = Status.Closed;
}
}
public ProjectTask : Task {
public override bool CanClose(out String reason) {
if (Status != Status.Started)
{
reason = "Cannot close a started Project Task";
return false;
}
return base.CanClose(out reason);
}
}
Le principe de substitution de Liskov stipule qu'une classe de base doit être remplaçable par l'une de ses sous-classes sans altérer aucune des propriétés souhaitables du programme. Étant donné que seul ProjectTask
déclenche une exception à la fermeture, un programme devrait être modifié pour s'adapter à cela, si ProjectTask
devait être utilisé en remplacement de Task
. C'est donc une violation.
Mais si vous modifiez Task
en déclarant dans sa signature qu'il peut déclencher une exception lorsqu'il est fermé, vous ne violeriez pas le principe.
Une violation LSP nécessite trois parties. Le type T, le sous-type S et le programme P qui utilise T mais reçoit une instance de S.
Votre question a fourni T (Task) et S (ProjectTask), mais pas P. Donc votre question est incomplète et la réponse est nuancée: S'il existe un P qui ne s'attend pas à une exception alors, pour ce P, vous avez un LSP violation. Si chaque P attend une exception, il n'y a pas de violation LSP.
Cependant, vous avez une violation SRP . Le fait que l'état d'une tâche peut être modifié et la politique que certaines tâches dans certains états doivent ne pas être changé pour d'autres états, sont deux responsabilités très différentes.
Ces deux responsabilités changent pour des raisons différentes et devraient donc être dans des classes distinctes. Les tâches doivent gérer le fait d'être une tâche et les données associées à une tâche. TaskStatePolicy doit gérer la façon dont les tâches passent d'un état à l'autre dans une application donnée.
Ceci peut ou non être une violation du LSP.
Sérieusement. Écoutez-moi.
Si vous suivez le LSP, les objets de type ProjectTask
doivent se comporter comme les objets de type Task
sont censés se comporter.
Le problème avec votre code est que vous n'avez pas documenté comment les objets de type Task
doivent se comporter. Vous avez écrit du code, mais pas de contrat. J'ajouterai un contrat pour Task.Close
. Selon le contrat que j'ajoute, le code de ProjectTask.Close
Suit ou ne suit pas le LSP.
Étant donné le contrat suivant pour Task.Close, le code pour ProjectTask.Close
ne fait pas suivre le LSP:
// Behaviour: Moves the task to the closed state
// and does not throw any Exception.
// Default behaviour: Moves the task to the closed state
// and does not throw any Exception.
public virtual void Close()
{
Status = Status.Closed;
}
Étant donné le contrat suivant pour Task.Close, le code pour ProjectTask.Close
does suit le LSP:
// Behaviour: Moves the task to the closed status if possible.
// If this is not possible, this method throws an Exception
// and leaves the status unchanged.
// Default behaviour: Moves the task to the closed state
// and does not throw any Exception.
public virtual void Close()
{
Status = Status.Closed;
}
Les méthodes qui peuvent être remplacées doivent être documentées de deux manières:
Le "Comportement" documente ce sur quoi peut s'appuyer un client qui sait que l'objet destinataire est un Task
, mais ne sait pas de quelle classe il s'agit d'une instance directe. Il indique également aux concepteurs de sous-classes quels remplacements sont raisonnables et lesquels ne le sont pas.
Le "comportement par défaut" documente ce sur quoi peut s'appuyer un client qui sait que l'objet destinataire est une instance directe de Task
(c'est-à-dire ce que vous obtenez si vous utilisez new Task()
. Il indique également les concepteurs de sous-classes quel comportement sera hérité s'ils ne remplacent pas la méthode.
Maintenant, les relations suivantes devraient tenir:
Il ne s'agit pas d'une violation du principe de substitution de Liskov.
Le principe de substitution de Liskov dit:
Soit q (x) une propriété prouvable sur les objets x de type T . Soit S un sous-type de T . Le type S viole le principe de substitution Liskov si un objet y de type S existe, de sorte que q (y) n'est pas prouvable.
La raison pour laquelle votre implémentation du sous-type n'est pas une violation du principe de substitution de Liskov est assez simple: rien ne peut être prouvé sur ce que Task::Close()
fait réellement. Bien sûr, ProjectTask::Close()
lève une exception lorsque Status == Status.Started
, Mais il en est de même pour Status = Status.Closed
Dans Task::Close()
.
Oui, c'est une violation.
Je suggère que vous ayez votre hiérarchie à l'envers. Si tous les Task
ne peuvent pas être fermés, alors close()
n'appartient pas à Task
. Vous voulez peut-être une interface, CloseableTask
que tous les non -ProjectTasks
peuvent implémenter.
En plus d'être un problème LSP, il semble qu'il utilise des exceptions pour contrôler le flux du programme (je dois supposer que vous interceptez cette exception triviale quelque part et faites un flux personnalisé plutôt que de le laisser planter votre application).
Il semble que ce soit un bon endroit pour implémenter le modèle d'état pour TaskState et laisser les objets d'état gérer les transitions valides.
Il me manque ici une chose importante liée au LSP et à la conception par contrat - dans les conditions préalables, c'est l'appelant qui a la responsabilité de s'assurer que les conditions préalables sont remplies. Le code appelé, dans la théorie DbC, ne devrait pas vérifier la condition préalable. Le contrat doit spécifier quand une tâche peut être fermée (par exemple, CanClose renvoie True), puis le code appelant doit garantir que la condition préalable est remplie, avant d'appeler Close ().
Oui, c'est une violation claire de LSP.
Certaines personnes soutiennent ici que rendre explicite dans la classe de base que les sous-classes peuvent lever des exceptions rendrait cela acceptable, mais je ne pense pas que ce soit vrai. Peu importe ce que vous documentez dans la classe de base ou le niveau d'abstraction vers lequel vous déplacez le code, les conditions préalables seront toujours renforcées dans la sous-classe, car vous y ajoutez la partie "Impossible de fermer une tâche de projet démarrée". Ce n'est pas quelque chose que vous pouvez résoudre avec une solution de contournement, vous avez besoin d'un modèle différent, qui ne viole pas le LSP (ou nous devons assouplir la contrainte "les conditions préalables ne peuvent pas être renforcées").
Vous pouvez essayer le motif de décoration si vous souhaitez éviter la violation LSP dans ce cas. Ça pourrait marcher, je ne sais pas.