Le principe de responsabilité unique définit que chaque concept ou domaine d'activité ne s'occupe que d'une et d'une seule chose.
En prenant l'exemple d'une méthode qui communique avec une base de données, ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement quelconque.
Cette action doit être prise en compte par une autre classe (ou un autre concept), qui s'occupera elle de définir l'emplacement où l'évènement sera enregistré (dans une base de données, une instance Graylog, un fichier, ...).
Cette manière d'organiser le code ajoute une couche de flemmardise (ie. Une fonction ou une méthode doit pouvoir se dire "_I don't care_" et s'occuper uniquement de ses propres oignons) sur certains concepts. Ceci permet de centraliser la configuration d'un type d'évènement à un seul endroit, ce augmente la testabilité du code.
Un des principes essentiels en programmation orientée objets concerne l'hé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 d'une instance, il est parfois préférable de définir une nouvelle sous-classe, qui surcharge une méthode bien précise.
Pour l'exemple, on pourrait ainsi définir trois classes:
En anglais, dans le texte : "Putting in simple words, the “Customer” class is now closed for any new modification but it’s 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 https://docs.djangoproject.com/en/3.1/topics/db/models/#proxy-models[modèles proxy].
> [...] 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: http://en.wikipedia.org/wiki/Liskov_substitution_principle[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: http://en.wikipedia.org/wiki/Liskov_substitution_principle[Wikipédia aussi])
Ce n'est donc pas parce qu'une classe **a besoin d'une méthode définie dans une autre classe** qu'elle 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 qu'une réflexion avancée (et sans doute un peu alcoolisée) nous dit que "Puisqu'un `Lion` marche aussi, faisons le hériter de notre classe `Canard`":
[source,python]
----
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).
Ce principe stipule qu'un client ne peut en aucun cas dépendre d'une méthode dont il n'a pas besoin.
Plus simplement, plutôt que de dépendre d'une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé d'exploser cette interface en plusieurs (plus petites) interfaces.
Ceci permet aux différents consommateurs de n'utiliser qu'un sous-ensemble précis d'interfaces, répondant chacune à un besoin précis.
Un exemple est d'avoir une interface permettant d'accé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 n'est pas souhaité/souhaitable.
Pour contrer ceci, on aurait alors une première interface permettant la lecture, tandis qu'une 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.
Dans une architecture conventionnelle, les composants de haut-niveau dépendent directement des composants de bas-niveau.
L'inversion de dépendances stipule que c'est le composant de haut-niveau qui possède la définition de l'interface dont il a besoin, et le composant de bas-niveau qui l'implémente.
* http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/[Dependency Injection is NOT the same as dependency inversion]
* http://en.wikipedia.org/wiki/Dependency_injection[Injection de dépendances]