:PROPERTIES: :ID: b827c6e7-fe86-4301-a72c-dfaee85e142f :mtime: 20220910133550 :ctime: 20220908193040 :END: #+title: SOLID * Introduction * Cinq principes de design de classes (P.O.O), * Principes décrits par Robert J. Martin (a.k.a Uncle Bob) en 2000, * Acronyme créé par Michael Feathers. * Principes ** Responsabilité unique (Single responsability principle) /A class should have one and only one reason to change, meaning that a class should have only one job./ Exemple de classe réalisant différentes tâches: * Calcul du total de la facture (méthode ~calculate_total~), * Impression de la facture (méthode ~print_invoice~), * Sauvegarde de la facture (méthode ~save_to_file~). #+BEGIN_SRC python :results output class Invoice: def __init__(self, book, quantity, discount_rate, tax_rate, total): self.book = book self.quantity = quantity self.discount_rate = discount_rate self.tax_rate = tax_rate self.total = self.calculate_total() def calculate_total(self): price = (self.book.price - self.book.price * self.discount_rate) * self.quantity price_with_taxe = price * (1 + self.tax_rate) return price_with_taxe def print_invoice(self): print(f'{self.quantity}x {self.name} {self.price} $') print(f'Discount Rate: {self.discount_rate}') print(f'Tax Rate: {self.tax_rate}') print(f'Total: {self.total}') def save_to_file(self): # Creates a file with given name and writes the invoice ... #+END_SRC Les erreurs : * La logique d'impression d'une facture (méthode ~print_invoice~) devrait étre isolée : la seule raison pour laquelle la classe ~Invoice~ devrait étre mise à jour est le changement de logique de calul du total (méthode ~calculate_total~). * La logique de sauvegarde d'une facture (méthode ~save_to_file~) devrait elle aussi étre isolée : la classe ~Invoice~ ne devrait pas étre mise à jour suite au changement de méthode de sauvegarde (passage d'un fichier à une BDD par ex). Une implémetation respectant le principe de *responsabilité unique* : #+BEGIN_SRC python :results output class Invoice: def __init__(self, book, quantity, discount_rate, tax_rate, total): self.book = book self.quantity = quantity self.discount_rate = discount_rate self.tax_rate = tax_rate self.total = self.calculate_total() def calculate_total(self): price = (self.book.price - self.book.price * self.discount_rate) * self.quantity price_with_taxe = price * (1 + self.tax_rate) return price_with_taxe class InvoicePrinter: def __init__(self, invoice): self._invoice = invoice def print(self): print(f'{self._invoice.quantity}x {self._invoice.name} {self._invoice.price} $') print(f'Discount Rate: {self._invoice.discount_rate}') print(f'Tax Rate: {self._invoice.tax_rate}') print(f'Total: {self._invoice.total}') class InvoicePersistance: def __init__(self, invoice): self._invoice = invoice def save_to_file(self): # Creates a file with given name and writes the invoice ... #+END_SRC ** Ouvert-fermé (Open-close principle) /Objects or entities should be open for extension but closed for modification./ Une classe doit étre *ouverte pour l'extension* et *fermée pour les modifications* : * Il doit étre possible d'ajouter une fonctionnalité à une classe sans modifier le code existant, * Réalisé à l'aide d'interfaces et classes virtuelles, Exemple de classe ne respesctant pas le principe : #+BEGIN_SRC python :results output class InvoicePersistance: def __init__(self, invoice): self._invoice = invoice def save_to_file(self): # Creates a file with given name and writes the invoice ... def save_to_database(self): # Save the invoice to database ... #+END_SRC Il ne devrait pas étre nécessaire de modifier la classe ~InvoicePersistance~ lorsqu'une nouvelle manière de sauvegarder une ~Invoice~ est requise. Une implémetation respectant le principe *ouvert/fermé* : #+BEGIN_SRC python :results output from abc import abstractmethod class InvoicePersistance(): def __init__(self, invoice): self._invoice = invoice @abstractmethod def save(self): ... class FileInvoicePersistance(InvoicePersistance): def save(self): # Creates a file with given name and writes the invoice ... class DatabaseInvoicePersistance(InvoicePersistance): def save(self): # Save the invoice to database ... #+END_SRC ** Principe de substitution de Liskov /Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T./ Les classes filles doivent pouvoir être substituées par leur classe mère : * Pour une classe B, fille de A, il doit être possible de passer une instance de B à toute méthode s'attendant à avoir une instance de A, sans disfonctionnement. #+BEGIN_SRC python :results output class Rectangle: def __init__(self, width, height): self._width = width self._height = height @property def width(self): return self._width @width.setter def width(self, width): self._width = width @property def height(self): return self._height @height.setter def height(self, height): self._height = height def get_area(self): return self._width * self._height class Square(Rectangle): def __init__(self, size): super().__init__(size, size) @Rectangle.width.setter def width(self, size): self._width = size self._height = size @Rectangle.height.setter def height(self, size): self._width = size self._height = size def test_get_area(rectangle): width = rectangle.width rectangle.height = 10 print(f'Expected area of {10 * width}, got {rectangle.get_area()}') rectangle = Rectangle(2, 3) test_get_area(rectangle) square = Square(2) square.width = 5 test_get_area(square) #+END_SRC #+RESULTS: : Expected area of 20, got 20 : Expected area of 50, got 100 La classe ~Square~ ne respecte pas le principe car le comportement de la méthode ~get_area~ diffère selon l'instance de classe passée en paramètre. ** Principe de ségrégation d'interface (Interface segregation principle) /A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use./ Il s'agit d'un principe de séparation des interfaces : plusieurs interfaces spécifiques sont meilleures qu'une unique générale. Ainsi, seules les méthodes nécessaires sont implémentées et non l'intégralité de celles définies par l'interface générique. Exemple d'implémentation ne respectant pas le principe : l'interface ~ParkingLot~ englobe le stationnement ainsi que le paiement. Quid de son implémentation pour le cas d'un parking gratuit ? L'ideal serait d'isoler la partie paiement dans une interface dédiée. #+BEGIN_SRC python :results output from abc import abstractmethod class Car: ... class ParkingLot: @abstractmethod def park_car(self): # Decrease empty spot count by 1 ... @abstractmethod def unpark_car(self): # Increase empty spot count by 1 ... @abstractmethod def get_capacity(self): # Return car capacity ... @abstractmethod def calculate_fee(self, car): # Return the price based on the number of hours ... @abstractmethod def do_payment(self, car): # Do the payment process for the computed fee ... class FreeParking(ParkingLot): def park_car(self): ... def unpark_car(self): ... def get_capacity(self): ... def calculate_fee(self, car): # Unused method here... the parking is free. ... def do_payment(self, car): # Unused method here... the parking is free. ... #+END_SRC ** Principe d'inversion de dépendence (Dependency inversion principle) /Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions./ Les classes devraient être baséees sur des interfaces ou des classes abstraites plutôt que des classes concrètes, #+BEGIN_SRC python :results output class SqlConnection: def connect(self): ... class PasswordReminder: def __init__(self, connection: SqlConnection) -> None: ... #+END_SRC La classe ~PasswordReminder~ ne doit pas avoir de couplage fort avec la méthode employée pour obtenir le mot de passe. L'usage d'une interface permettrait d'abstraire la méthode de collecte. #+BEGIN_SRC python :results outputs from abc import abstractmethod class Connection: @abstractmethod def connect(self): ... class SqlConnection(Connection): def connect(self): ... class PasswordReminder: def __init__(self, connection: Connection) -> None: ... #+END_SRC * Références * [[https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf][Uncle bob design principes]] * [[https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/][SOLID principes explained in plain english - freecodecamp.org]]