: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]]
