gwift-book/source/part-1-workspace/maintainable-applications/solid.adoc

7.6 KiB
Raw Blame History

SOLID

  1. S : SRP (Single Responsibility

  2. O : Open closed

  3. L : LSP (Liskov Substitution)

  4. I : Interface Segregation

  5. D : Dependency Inversion

Single Responsibility Principle

Le principe de responsabilité unique définit que chaque concept ou domaine dactivité ne soccupe que dune et dune seule chose. En prenant lexemple dune méthode qui communique avec une base de données, ce ne sera pas à cette méthode à gérer linscription dune exception à un emplacement quelconque. Cette action doit être prise en compte par une autre classe (ou un autre concept), qui soccupera elle de définir lemplacement où lévènement sera enregistré (dans une base de données, une instance Graylog, un fichier, …​).

Cette manière dorganiser le code ajoute une couche de flemmardise (ie. Une fonction ou une méthode doit pouvoir se dire "I dont care" et soccuper uniquement de ses propres oignons) sur certains concepts. Ceci permet de centraliser la configuration dun type dévènement à un seul endroit, ce augmente la testabilité du code.

Open Closed

Un des principes essentiels en programmation orientée objets concerne lhéritage de classes et la surcharge de méthodes: plutôt que de partir sur une série de comparaisons pour définir le comportement dune instance, il est parfois préférable de définir une nouvelle sous-classe, qui surcharge une méthode bien précise. Pour lexemple, on pourrait ainsi définir trois classes:

  • Une classe Customer, pour laquelle la méthode GetDiscount ne renvoit rien;

  • Une classe SilverCustomer, pour laquelle la méthode revoit une réduction de 10%;

  • Une classe GoldCustomer, pour laquelle la même méthode renvoit une réduction de 20%.

Si nous rencontrons un nouveau type de client, il suffit de créer une nouvelle sous-classe. Cela évite davoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction dune autre variable (ici, le type de client).

Nous passerions ainsi de:

class Customer():
    def __init__(self, customer_type: str):
        self.customer_type = customer_type


def get_discount(customer: Customer) -> int:
    if customer.customer_type == "Silver":
        return 10
    elif customer.customer_type == "Gold":
        return 20
    return 0


>>> jack = Customer("Silver")
>>> jack.get_discount()
10

A ceci:

class Customer():
    def get_discount(self) -> int:
        return 0


class SilverCustomer(Customer):
    def get_discount(self) -> int:
        return 10


class GoldCustomer(Customer):
    def get_discount(self) -> int:
        return 20


>>> jack = SilverCustomer()
>>> jack.get_discount()
10

En anglais, dans le texte : "Putting in simple words, the “Customer” class is now closed for any new modification but its open for extensions when new customer types are added to the project.". En résumé: nous fermons la classe Customer à toute modification, mais nous ouvrons la possibilité de créer de nouvelles extensions en ajoutant de nouveaux types [héritant de Customer].

De cette manière, nous simplifions également la maintenance de la méthode get_discount, dans la mesure où elle dépend directement du type dans lequel elle est implémentée.

Ce point sera très utile lorsque nous aborderons les modèles proxy.

Liskov Substitution

Le principe de substitution fait quune classe héritant dune autre classe doit se comporter de la même manière que cette dernière. Il nest pas question que la sous-classe nimplémente pas certaines méthodes, alors que celles-ci sont disponibles sa classe parente.

[…​] if S is a subtype of T, then objects of type T in a computer program may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T), without altering any of the desirable properties of that program (correctness, task performed, etc.). (Source: Wikipédia).

Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S, where S is a subtype of T. (Source: Wikipédia aussi)

Ce nest donc pas parce quune classe a besoin dune méthode définie dans une autre classe quelle doit forcément en hériter. Cela bousillerait le principe de substitution, dans la mesure où une instance de cette classe pourra toujours être considérée comme étant du type de son parent.

Petit exemple pratique: si nous définissons une méthode walk et une méthode eat sur une classe Duck, et quune réflexion avancée (et sans doute un peu alcoolisée) nous dit que "Puisquun Lion marche aussi, faisons le hériter de notre classe `Canard`":

class Duck():
    def walk(self):
        print("Kwak")

    def eat(self, thing):
        if thing in ("plant", "insect", "seed", "seaweed", "fish"):
            return "Yummy!"

        raise IndigestionError("Arrrh")

class Lion(Duck):
    def walk(self):
        print("Roaaar!")

UnNous vous laissons tester la structure ci-dessus en glissant une antilope dans la boite à goûter du lion, ce qui nous donnera quelques trucs bizarres (et un lion atteint de botulisme).

Interface Segregation

Ce principe stipule quun client ne peut en aucun cas dépendre dune méthode dont il na pas besoin. Plus simplement, plutôt que de dépendre dune seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé dexploser cette interface en plusieurs (plus petites) interfaces. Ceci permet aux différents consommateurs de nutiliser quun sous-ensemble précis dinterfaces, répondant chacune à un besoin précis.

Un exemple est davoir une interface permettant daccéder à des éléments. Modifier cette interface pour permettre lécriture impliquerait que toutes les applications ayant déjà accès à la première, obtiendraient (par défaut) un accès en écriture, ce qui nest pas souhaité/souhaitable.

Pour contrer ceci, on aurait alors une première interface permettant la lecture, tandis quune deuxième (héritant de la première) permettrait lécriture. On aurait alors le schéma suivant :

  • A : lecture

  • B (héritant de A) : lecture (par A) et écriture.

Dependency inversion

Dans une architecture conventionnelle, les composants de haut-niveau dépendent directement des composants de bas-niveau. Linversion de dépendances stipule que cest le composant de haut-niveau qui possède la définition de linterface dont il a besoin, et le composant de bas-niveau qui limplémente.

Le composant de haut-niveau peut définir quil sattend à avoir un Publisher, afin de publier du contenu vers un emplacement particulier. Plusieurs implémentation de cette interface peuvent alors être mise en place:

  • Une publication par SSH

  • Une publication par FTP

  • Une publication

  • …​

Linjection de dépendances est un patron de programmation qui suit le principe dinversion de dépendances.