web-dev-qa-db-fra.com

Liskov, rectangles, carrés et objets nuls

Je continue à penser que j'ai la tête enveloppée autour du principe de substitution de Liskov, puis je réalise que je ne le fais pas. Voici où j'ai rassemblé Stackexchange:

  1. Sous-classement carré à partir du rectangle viole le LSP car si vous changez la largeur d'un carré, les changements de hauteur.
  2. Les objets nuls ne violent pas le LSP.

Ceux-ci semblent se contredire à moi. Si c'est mauvais:

square.setWidth(40);
square.setHeight(50);
width = square.getWidth(); // Returns 50 instead of 40. Liskov hates that.

Alors pourquoi n'est-ce pas mauvais?

nullRectangle.setWidth(40);
nullRectangle.setHeight(50);
width = nullRectangle.getWidth(); // Returns 0 or null. Isn't that worse?

Qu'est-ce que j'oublie ici?

5
M. Christian

Ce que vous avez trouvé est le cas simplement Edge où Null Object pourrait ne pas être une bonne solution.

Le point de null object est de créer un comportement identique à ce que vous avez vérifié null auparavant.

Dans votre cas, si ce code est attendu Comportement:

Rectangle nullRectangle = null;
int width = 0;
if (nullRectangle != null)
{
    nullRectangle.setWidth(40);
    nullRectangle.setHeight(50);
    width = nullRectangle.getWidth();
}
// width is 0 if nullRectangle is null

Dix null object Comme décrit dans votre code est un substitut logique.

Mais l'idée d'avoir "NULL Rectangle" n'a pas beaucoup de sens du point de vue de la modélisation. NULL objet n'est vraiment utilisable que comme mise en œuvre des abstractions, où appeler une méthode ne signifie pas que l'appelant attend toujours quelque chose. Si l'appelant de toute méthode de l'abstraction voit "Aucune opération" ou "Aucune donnée renvoyée" comme résultat valide, alors null object peut être utilisé. Si plutôt une valeur non nulle est attendue comme retour, l'objet NULL viole véritablement le LSP.

4
Euphoric

Il y a déjà une réponse acceptée, mais je voulais souligner quelques choses.

Les objets doivent avoir un comportement qu'ils effectuent. Sinon, il y a peu de raisons d'utiliser un sur une structure. Vos exemples d'objets n'ont aucun comportement, seules les données que vous avez apportées plus compliquées pour accéder via Getters and Setters. Votre exemple viole principalement Liskov parce qu'il n'est pas orienté objet de toute façon. Il lisait et rédige des données sur la procédure avec une structure spécifique.

Je veux aussi mentionner que "Null Objects" ne signifie pas null pointeurs. Ils signifient une implémentation de la classe avec un comportement par défaut. Voir cette vidéo .

Alors qu'est-ce que le LSP pourrait ressembler?

Si je pense à Shape2D en termes de quel comportement je veux que ce soit pour moi, une chose qui me vient à l'esprit est le calcul de la zone. Parce que cela sera différent entre les formes.

public class Shape2D
{
    public virtual double GetArea() { return 0.0; }
}

Ici, j'utilise Shape2D comme son propre objet NULL - une forme qui n'a pas de surface. Il peut être prolongé par sous-classement et remplacement GetArea().

Retour double Comme la zone est un choix douteux. Parce qu'il y a d'autres implications avec la zone, comme l'unité de mesure ou qu'il peut être infini. Peut-être qu'une classe de zone devrait également être créée si c'était du code de production.

Créons donc quelques implémentations de la zone en 2 dimensions - ayant des choses.

public class Square : Shape2D
{
    private double _sideLength;
    public Square(double sideLength) { _sideLength = sideLength; }
    public override double GetArea()
        { return _sideLength ^ 2 }
}

public class Rectangle : Shape2D
{
    private double _length;
    private double _height;
    public Rectangle(double length, double height) { ... }
    public override double GetArea()
        { return _length * _height; }
}

public class Circle : Shape2D
{
    private double _radius;
    public Circle(double radius) { _radius = radius; }
    public override double GetArea()
        { return Math.Pi * (_radius ^ 2); }
}

Maintenant à ce stade, vous vous demandez peut-être. "Pourquoi ne-je pas calculer la zone du constructeur?" ou "pourquoi pas seulement créer une méthode statique pour des calculs de zone différents?" Eh bien, qu'en est-il des formes 2D complexes? Comme un octogone concave défini par des vecteurs? Peut-être que la mise en œuvre de la forme particulière est-elle calculée pour calculer la zone? Je pense que finalement, cela nécessite une certaine intuition de déterminer quelle manière est correcte. Mais dans l'exemple ici, nous le calculons que lorsque demandé.

Dans la question initiale, vous avez changé la largeur du rectangle en modifiant des données privées sur le rectangle. Mais ici, vous créeriez un rectangle new avec différentes dimensions.

Donc, cela suit le LSP, car pour toute méthode qui prend un Shape2D, Vous pourriez passer dans un Square, Shape2D (Objet null) ou toute forme que vous pourriez implémenter dans le avenir comme ConcaveOctagon sans la méthode le sachant.

1
Kasey Speakman

Le "astuce" du problème du LSP de rectangle carré classique est que si nous définissons un rectangle comme un objet qui doit satisfaire au contrat ...

rectangle.setWidth(40);
rectangle.setHeight(50);
assert(rectangle.getWidth() == 40);

... Puis étendez-le à une place, puis nous violons ce contrat. En effet, nous mélangeons une définition personnalisée d'un rectangle et d'une définition classique. En tant que tel, nous essayons de rendre la définition classique d'une définition personnalisée qui ne fonctionne pas. En substance, c'est un tour du salon destiné à vous faire penser au comportement des sous-types et aux obligations contractuelles. Nous pourrions aussi facilement montrer que l'affirmation ci-dessus d'un objet rectangle échoue pour même un rectangle avec une nouvelle reformulation de ruse:

rectangle.setTopEdgeLength(40);
rectangle.setBottomEdgeLength(50);
assert(rectangle.getTopEdgeLength() == 40);

Quoi qu'il en soit, assez des énigmes, regardons le ...

Définition classique

Correctement, les carrés et les rectangles sont les deux types de quadrilatères, un quadrilatère étant un polygone avec 4 bords et 4 sommets. Classiquement, un carré est un rectangle, mais regardons les définitions de Wikipedia d'un rectangle et d'une carrée:

  • rectangle: un quadrilatère à quatre angles droits
  • carré: un quadrilatère à quatre angles droits et quatre côtés égaux

Remarquez quelque chose là-bas? Les seules communités communes entre un rectangle et un carré sont que les deux sont quadrilatères et les deux ont quatre angles droits. Aucune mention de toutes les règles autour de la longueur des côtés, comme celles-ci sont dérivées du contrat, ne faisant pas partie du contrat. Nous pouvons le démontrer avec quelques déclarations logiques simples:

  • si une forme est un quadrilatère et que la forme a quatre angles droits, les côtés opposés seront de longueur égale
  • si une forme est quadrilatérale et que la forme présente quatre angles droits et quatre côtés égaux, la longueur de n'importe quel côté sera égale à la longueur de tout autre côté (celui-ci est en fait une tautologie)

Si nous voulons maintenir la définition classique des rectangles et des carrés et les représentent en code avec le setWidth() et setHeight() Des méthodes en place, alors le contrat doit être:

square.setWidth(40);
square.setHeight(50);
assert(square.isQuadrilateral());
assert(square.hasFourRightAngles());

Si la largeur est toujours ce que nous avons définie dans la ligne 1 n'est pas pertinente, car cette règle ne fait pas partie des obligations contractuelles du rectangle d'objet parent.


Pour résumer, le problème initial est l'une des définitions contractuelles. Je ne dis pas que vous ne devriez pas avoir un objet rectangle qui satisfait rectangle.setWidth(40); rectangle.setHeight(50); assert(rectangle.getWidth() == 40);, mais si vous définissez votre objet rectangle de cette manière, alors lorsque vous l'étendez, vous devez vous assurer que le contrat est toujours valide, sinon sinon vous violez le LSP.

1
e_i_pi