Add notes about Js, diffsitter and solid
This commit is contained in:
328
20220908193040-solid.org
Normal file
328
20220908193040-solid.org
Normal file
@@ -0,0 +1,328 @@
|
||||
: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]]
|
Reference in New Issue
Block a user