Restart data model, by integrating Clean Code practices

This commit is contained in:
Fred Pauchet 2021-12-15 19:13:53 +01:00
parent c75fd76775
commit 43ee80f983
2 changed files with 123 additions and 9 deletions

View File

@ -8,3 +8,5 @@ IaaS:: _Infrastructure as a Service_, où un tiers vous fournit des machines (g
ORM:: _Object Relational Mapper_, où une instance est directement (ou à proximité) liée à un mode de persistance de données.
PaaS:: _Platform as a Service_, qui consiste à proposer les composants d'une plateforme (Redis, PostgreSQL, ...) en libre service et disponibles à la demande (quoiqu'après avoir communiqué son numéro de carte de crédit...).
POO:: La _Programmation Orientée Objet_ est un paradigme de programmation informatique. Elle consiste en la définition et l'interaction de briques logicielles appelées objets ; un objet représente un concept, une idée ou toute entité du monde physique, comme une voiture, une personne ou encore une page d'un livre. Il possède une structure interne et un comportement, et il sait interagir avec ses pairs. Il s'agit donc de représenter ces objets et leurs relations ; l'interaction entre les objets via leurs relations permet de concevoir et réaliser les fonctionnalités attendues, de mieux résoudre le ou les problèmes. Dès lors, l'étape de modélisation revêt une importance majeure et nécessaire pour la POO. C'est elle qui permet de transcrire les éléments du réel sous forme virtuelle. https://fr.wikipedia.org/wiki/Programmation_orient%C3%A9e_objet[Wikipédia]

View File

@ -1,15 +1,15 @@
== Modélisation
Ce chapitre aborde la modélisation des objets et les options qui y sont liées.
Avec Django, la modélisation est en lien direct avec la conception et le stockage de la base de données, la manière dont ces données s'agencent et communiquent entre elles.
Avec Django, la modélisation est en lien direct avec la conception et le stockage de la base de données relationnelle, et la manière dont ces données s'agencent et communiquent entre elles.
Django utilise un paradigme de type https://fr.wikipedia.org/wiki/Mapping_objet-relationnel[ORM] - c'est-à-dire que chaque type d'objet manipulé peut s'apparenter à une table SQL, tout en ajoutant une couche propre à la programmation orientée object.
Plus spécifiquement, l'ORM de Django suit le patron de conception https://en.wikipedia.org/wiki/Active_record_pattern[Active Records], comme ce que font par exemple https://rubyonrails.org/[Rails] pour Ruby ou https://docs.microsoft.com/fr-fr/ef/[EntityFramework] pour .Net.
Plus spécifiquement, l'ORM de Django suit le patron de conception https://en.wikipedia.org/wiki/Active_record_pattern[Active Records], comme le font par exemple https://rubyonrails.org/[Rails] pour Ruby ou https://docs.microsoft.com/fr-fr/ef/[EntityFramework] pour .Net.
Le modèle de données de Django est sans doute la (seule ?) partie qui soit tellement couplée au framework qu'un changement à ce niveau nécessitera une refonte complète de beaucoup d'autres briques de vos applications; là où un pattern de type https://www.martinfowler.com/eaaCatalog/repository.html[Repository] permettrait justement de découpler le modèle des données de l'accès à ces mêmes données, un pattern Active Record lie de manière extrêmement forte le modèle à sa persistence.
Architecturalement, c'est sans doute la plus grosse faiblesse de Django.
Conceptuellement, c'est pourtant la manière de faire qui permettra d'avoir quelque chose à présenter très rapidement: à partir du moment où vous aurez un modèle de données, vous aurez accès :
Conceptuellement, c'est pourtant la manière de faire qui permettra d'avoir quelque chose à présenter très rapidement: à partir du moment où vous aurez un modèle de données, vous aurez accès, grâce à Django:
1. Aux migrations de données,
2. A un découplage complet entre le moteur de données relationnel et le modèle de données,
@ -21,17 +21,128 @@ Comme tout ceci reste au niveau du code, cela suit également la méthodologie d
Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande.
_A contrario_, ces avantages sont balancés au travers d'un couplage extrêmement fort entre la modélisation et le reste du framework - à tel point que *ne pas utiliser cette brique de fonctionnalités* peut remettre en question le choix du framework.
En plus de ceci, l'implémentation d'Active Records reste une forme hybride entre une structure de données brutes et une classe: là où une classe va exposer ses données derrière une forme d'abstraction et n'exposer que les fonctions qui opèrent sur ces données, une structure de données ne va exposer que ses champs et propriétés, et ne va pas avoir de functions significatives.
=== Principes de modélisation
L'exemple ci-dessous (en Java) présente trois structure de données, qui exposent chacune ses propres champs:
[source,python]
----
class Square:
def __init__(self, top_left, side):
self.top_left = top_left
self.side = side
class Rectangle:
def __init__(self, top_left, height, width):
self.top_left = top_left
self.height = height
self.width = width
class Circle:
def __init__(self, center, radius):
self.center = center
self.radius = radius
----
Si nous souhaitons ajouter une fonctionnalité permettant de calculer l'aire pour chacune de ces structures, nous aurons deux possibilités:
1. Soit ajouter une classe de _visite_ qui ajoute cette fonction de calcul d'aire
2. Soit modifier notre modèle pour que chaque structure hérite d'une classe de type `Shape`, qui implémentera elle-même ce calcul d'aire.
Dans le premier cas, nous pouvons procéder de la manière suivante:
[source,python]
----
class Geometry:
PI = 3.141592653589793
def area(self, shape):
if isinstance(shape, Square):
return shape.side * shape.side
if isinstance(shape, Rectangle):
return shape.height * shape.width
if isinstance(shape, Circle):
return PI * shape.radius**2
raise NoSuchShapeException()
----
Dans le second cas, l'implémentation pourrait évoluer de la manière suivante:
[source,python]
----
class Shape:
def area(self):
pass
class Square(Shape):
def __init__(self, top_left, side):
self.__top_left = top_left
self.__side = side
def area(self):
return self.__side * self.__side
class Rectangle(Shape):
def __init__(self, top_left, height, width):
self.__top_left = top_left
self.__height = height
self.__width = width
def area(self):
return self.__height * self.__width
class Circle(Shape):
def __init__(self, center, radius):
self.__center = center
self.__radius = radius
def area(self):
PI = 3.141592653589793
return PI * self.__radius**2
----
On le voit: une structure brute peut être rendue abstraite au travers des notions de programmation orientée objet.
Dans l'exemple géométrique ci-dessus, repris de cite:[clean_code, 95-97], l'accessibilité des champs devient restreinte, tandis que la fonction `area()` bascule comme méthode d'instance plutôt qu'isolée au niveau d'un visiteur.
Nous ajoutons une abstraction au niveau des formes grâce à un héritage sur la classe `Shape`; indépendamment de ce que nous manipulerons, nous aurons la possibilité de calculer son aire.
Une structure de données permet de facilement gérer des champs et des propriétés, tandis qu'une classe gère et facilite l'ajout de fonctions et de méthodes.
Le problème d'Active Records est que chaque classe s'apparente à une table SQL et revient donc à gérer des _DTO_ ou _Data Transfer Object_, c'est-à-dire des objets de correspondance pure et simple les champs de la base de données et les propriétés de la programmation orientée objet, c'est-à-dire également des classes sans fonctions.
Or, chaque classe a également la possibilité d'exposer des possibilités d'interactions au niveau de la persistence, en https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.save[enregistrant ses propres données] ou en en autorisant la https://docs.djangoproject.com/en/stable/ref/models/instances/#deleting-objects[suppression].
Nous arrivons alors à un modèle hybride, mélangeant des structures de données et des classes d'abstraction, ce qui restera parfaitement viable tant que l'on garde ces principes en tête et que l'on se prépare à une éventuelle réécriture ultérieure.
Lors de l'analyse d'une classe de modèle, nous pouvons voir que Django exige un héritage de la classe `django.db.models.Model`.
Nous pouvons regarder les propriétés définies dans cette classe en analysant le fichier `lib\site-packages\django\models\base.py`.
Outre que `models.Model` hérite de `ModelBase` au travers de https://pypi.python.org/pypi/six[six] pour la rétrocompatibilité vers Python 2.7, cet héritage apporte notamment les fonctions `save()`, `clean()`, `delete()`, ...
En résumé, toutes les méthodes qui font qu'une instance est sait **comment** interagir avec la base de données.
=== Types de champs, clés étrangères et relations
Nous l'avons vu plus tôt, Python est un langage dynamique et fortement typé.
Django, de son côté, ajoute une couche de typage statique exigé par le lien avec le moteur de base de données relationnelle sous-jacent.
Dans le domaine des bases de données relationnelles, un point d'attention est de toujours disposer d'une clé primaire pour nos enregistrements.
Si aucune clé primaire n'est spécifiée, Django s'occupera d'en ajouter une automatiquement et la nommera (par convention) `id`.
Elle sera ainsi accessible autant par cette propriété que par la propriété `pk`.
==== Types de champs
Chaque champ du modèle est donc typé et lié, soit à une primitive, soit à une autre instance au travers de sa clé d'identification.
Grâce à toutes ces informations, nous sommes en mesure de représenter facilement des livres liés à des catégories:
[source,python]
----
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
author = models.CharField(max_length=255)
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
----
==== Clés étrangères et relations
. ForeignKey
. ManyToManyField
@ -97,11 +208,9 @@ A partir de maintenant, nous pouvons accéder à nos propriétés de la manière
----
==== Metamodèle
==== Metamodèle et introspection
Quand on prend une classe (par exemple, `Wishlist` que l'on a défini ci-dessus), on voit qu'elle hérite par défaut de `models.Model`. On peut regarder les propriétés définies dans cette classe en analysant le fichier `lib\site-packages\django\models\base.py`. On y voit notamment que `models.Model` hérite de `ModelBase` au travers de https://pypi.python.org/pypi/six[six] pour la rétrocompatibilité vers Python 2.7.
Cet héritage apporte notamment les fonctions `save()`, `clean()`, `delete()`, ... Bref, toutes les méthodes qui font qu'une instance est sait **comment** interagir avec la base de données. La base d'un https://en.wikipedia.org/wiki/Object-relational_mapping[ORM], en fait.
D'autre part, chaque classe héritant de `models.Model` possède une propriété `objects`. Comme on l'a vu dans la section **Jouons un peu avec la console**, cette propriété permet d'accéder aux objects persistants dans la base de données, au travers d'un `ModelManager`.
@ -156,6 +265,9 @@ class Runner(models.Model):
start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?") # this is new
----
==== Validateurs
==== Protocoles de langage (*dunder methods*)