From f26ef70f5958c7c04d2389e0814480799ee7fd6d Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Thu, 30 Dec 2021 19:02:41 +0100 Subject: [PATCH] Improve models and create SOA --- code_samples/shapes.py | 46 +++ source/diagrams/books-foreign-keys-example | 1 + .../books-foreign-keys-example.drawio.png | Bin 0 -> 29733 bytes source/django/models.adoc | 14 - source/glossary.adoc | 2 + source/images/rest/api-first-example.png | Bin 0 -> 15802 bytes .../images/rest/drf-filters-and-searches.png | Bin 0 -> 8394 bytes source/images/rest/models.png | Bin 0 -> 23005 bytes .../part-1-workspace/environment/_index.adoc | 167 +++++++- .../maintainable-applications/_index.adoc | 2 - .../maintainable-applications/solid.adoc | 354 +++++++++++----- source/part-3-data-model/_index.adoc | 10 +- source/part-3-data-model/admin.adoc | 10 + source/part-3-data-model/models.adoc | 201 ++++------ .../_main.adoc | 4 + .../querysets.adoc | 72 ++-- .../rest.adoc | 378 ++++++++++++++++++ .../{tests.adoc => trees.adoc} | 0 source/references.bib | 21 + 19 files changed, 1002 insertions(+), 280 deletions(-) create mode 100644 code_samples/shapes.py create mode 100644 source/diagrams/books-foreign-keys-example create mode 100644 source/diagrams/books-foreign-keys-example.drawio.png delete mode 100644 source/django/models.adoc create mode 100644 source/images/rest/api-first-example.png create mode 100644 source/images/rest/drf-filters-and-searches.png create mode 100644 source/images/rest/models.png create mode 100644 source/part-4-services-oriented-applications/rest.adoc rename source/part-4-services-oriented-applications/{tests.adoc => trees.adoc} (100%) diff --git a/code_samples/shapes.py b/code_samples/shapes.py new file mode 100644 index 0000000..cbfaac0 --- /dev/null +++ b/code_samples/shapes.py @@ -0,0 +1,46 @@ + +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 + + +class Geometry: + + + 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): + + + raise NoSuchShapeException() diff --git a/source/diagrams/books-foreign-keys-example b/source/diagrams/books-foreign-keys-example new file mode 100644 index 0000000..12998be --- /dev/null +++ b/source/diagrams/books-foreign-keys-example @@ -0,0 +1 @@ +7Zlrb5swFIZ/DeqnRmAIST6mpJdJ7VQp0/bZAQesGpvZJpf9+h1zy4VW6dDaRCpVpcDrY4PP89rYYLlBurmXOEueRESYhexoY7kzCyHHsRH8GGVbKTYalkosaVRpO2FO/5A6sFJzGhF1EKiFYJpmh2IoOCehPtCwlGJ9GLYU7PCqGY5JS5iHmLXVXzTSSamO0WinPxAaJ/WVHX9SlqS4Dq56ohIcifWe5N5abiCF0OVRugkIM9mr81LWu3ujtLkxSbh+TwU9w9n46ddWUVuspGb2T+/7ddXKCrO86rCFfAbt3SzgIDYHAdZW4FrTSSwkBRR1uawDPl6BTi12WpFMva0JSZHziJhO2lC8Tqgm8wyHpnQNpgQt0SmDM6epvSJSk82beXQaOuBrIlKi5RZC6gp+WaN2tFuernfu8L0KeXLgjErElSPjpuUdNDiouP0DQ3Sa4TTCmcaaCq7MHeRmwFBeYk3xOZB+EbO4h2bx2mZx0CtmQfZHmcU9bZZ5SAkPyfUdDY1jLoPqF/GLM5mcNIwz+kzDeKcN81ROJBEtfyH0eom5xkrT31DpIih/Ef+4w0ubcIbvnnDMo4lo0x5fCgnrp949n+yeoXNp7hm33DPLOWklai8NJgEU1u+PeEHYs1C0eIa5s4XQWqQQgBmNjRBCUogEgZnIGxy+xEW+A8GELJp1l8XfXqPTqq4WJvlKS/HSbAtQo+y1YNtj+87Aa1b/5iTCKmmwQklmupFuYrOHGlChRgMK+xk1iESYp3Cb6v/gPV66Ttp4ERoM23zdj8I7aeF9QPeox9sN7+jS8Drt7eUtTGjyykz19zjtB3JH0p4/qN+j1NtQ//y023uLByyL+34GZoCop92Ftn/0wsE+P+n2puBHQsyYlgTrYmxrtdj2vDvxHnuD0dHgHp0feXsdHzCCOUiBiPqJvOOC27u4R7bfXnETBTmmPLbM2v94IzPDGl9/A3Bc0RV5NWSaZQxYVa8fe6N0McrR2s4Znt8po5ZTHg3/OVyckxzo21HxuQCnJuN8obJXd8JTzgnON70xur3wmVzcDNLesz9eYaXgn5rHhRRbzHranWh7znjwiUsDON19nyzK9j7zurd/AQ== \ No newline at end of file diff --git a/source/diagrams/books-foreign-keys-example.drawio.png b/source/diagrams/books-foreign-keys-example.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..33678a784786dbb72b0a5df37b3bad4b9957b798 GIT binary patch literal 29733 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 literal 0 HcmV?d00001 diff --git a/source/django/models.adoc b/source/django/models.adoc deleted file mode 100644 index 5418728..0000000 --- a/source/django/models.adoc +++ /dev/null @@ -1,14 +0,0 @@ -== Modélisation - -On va aborder la modélisation des objets en elle-même, qui s'apparente à la conception de la base de données. - -Django utilise un modèle https://fr.wikipedia.org/wiki/Mapping_objet-relationnel[ORM] - c'est-à-dire que chaque objet peut s'apparenter à une table SQL, mais en ajoutant une couche propre au paradigme orienté objet. Il sera ainsi possible de définir facilement des notions d'héritage (tout en restant dans une forme d'héritage simple), la possibilité d'utiliser des propriétés spécifiques, des classes intermédiaires, ... - -L'avantage de tout ceci est que tout reste au niveau du code. Si l'on revient sur la méthodologie des douze facteurs, ce point concerne principalement la minimisation de la divergence entre les environnements d'exécution. Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande, dans la mesure où *tout est embarqué au niveau du code*. - -Assez de blabla, on démarre ! - -=== Le modèle et les validateurs - - -=== \ No newline at end of file diff --git a/source/glossary.adoc b/source/glossary.adoc index 39743de..f53627b 100644 --- a/source/glossary.adoc +++ b/source/glossary.adoc @@ -5,6 +5,8 @@ http:: _HyperText Transfer Protocol_, ou plus généralement le protocole utilis IaaS:: _Infrastructure as a Service_, où un tiers vous fournit des machines (généralement virtuelles) que vous devrez ensuite gérer en bon père de famille. L'IaaS propose souvent une API, qui vous permet d'intégrer la durée de vie de chaque machine dans vos flux - en créant, augmentant, détruisant une machine lorsque cela s'avère nécessaire. +MVC:: Le modèle _Model-View-Controler_ est un patron de conception autorisant un faible couplage entre la gestion des données (le _Modèle_), l'affichage et le traitement de celles (la _Vue_) et la glue entre ces deux composants (au travers du _Contrôleur_). https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller[Wikipédia] + 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...). diff --git a/source/images/rest/api-first-example.png b/source/images/rest/api-first-example.png new file mode 100644 index 0000000000000000000000000000000000000000..c8c46f0e6180a7937932418544da6fe7c935adce GIT binary patch literal 15802 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 literal 0 HcmV?d00001 diff --git a/source/images/rest/drf-filters-and-searches.png b/source/images/rest/drf-filters-and-searches.png new file mode 100644 index 0000000000000000000000000000000000000000..370dcb57437bbc5852ac0f96bddaa68ca1a3634c GIT binary patch literal 8394 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 literal 0 HcmV?d00001 diff --git a/source/images/rest/models.png b/source/images/rest/models.png new file mode 100644 index 0000000000000000000000000000000000000000..6778f35da1271ae134a6bf2249b64b783cefb5a4 GIT binary patch literal 23005 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 literal 0 HcmV?d00001 diff --git a/source/part-1-workspace/environment/_index.adoc b/source/part-1-workspace/environment/_index.adoc index 8162d7e..2614980 100644 --- a/source/part-1-workspace/environment/_index.adoc +++ b/source/part-1-workspace/environment/_index.adoc @@ -11,16 +11,12 @@ Et malgré ces quelques points, Python reste un langage généraliste accessible Il fonctionne avec un système d'améliorations basées sur des propositions: les PEP, ou "**Python Enhancement Proposal**". Chacune d'entre elles doit être approuvée par le http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life[Benevolent Dictator For Life]. - - NOTE: Le langage Python utilise un typage dynamique appelé https://fr.wikipedia.org/wiki/Duck_typing[*duck typing*]: "_When I see a bird that quacks like a duck, walks like a duck, has feathers and webbed feet and associates with ducks — I’m certainly going to assume that he is a duck_" Source: http://en.wikipedia.org/wiki/Duck_test[Wikipedia]. Les morceaux de code que vous trouverez ci-dessous seront développés pour Python3.9+ et Django 3.2+. Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure. - - === Eléments de langage En fonction de votre niveau d'apprentissage du langage, plusieurs ressources pourraient vous aider: @@ -31,6 +27,92 @@ A ce jour, c'est le concentré de sujets liés au langage le plus intéressant q En parallèle, si vous avez besoin d'un aide-mémoire ou d'une liste exhaustive des types et structures de données du langage, référez-vous au lien suivant: https://gto76.github.io/python-cheatsheet/[Python Cheat Sheet]. +==== Protocoles de langage + +(((dunder))) + +Le modèle de données du langage spécifie un ensemble de méthodes qui peuvent être surchargées. +Ces méthodes suivent une convention de nommage et leur nom est toujours encadré par un double tiret souligné; d'où leur nom de "_dunder methods_" ou "_double-underscore methods_". +La méthode la plus couramment utilisée est la méthode `__init__()`, qui permet de surcharger l'initialisation d'une instance de classe. + +[source,python] +---- +class CustomUserClass: + def __init__(self, initiatization_argument): + ... +---- + +cite:{expert_python, 142-144} + +Ces méthodes, utilisées seules ou selon des combinaisons spécifiques, constituent les _protocoles de langage_. +Une liste complètement des _dunder methods_ peut être trouvée dans la section `Data Model` de https://docs.python.org/3/reference/datamodel.html[la documentation du langage Python]. + +All operators are also exposed as ordinary functions in the operators module. +The documentation of that module gives a good overview of Python operators. +It can be found at https://docs.python.org/3.9/library/operator.html + + +If we say that an object implements a specific language protocol, it means that it is compatible with a specific part of the Python language syntax. + +The following is a table of the most common protocols within the Python language. + +Protocol nameMethodsDescriptionCallable protocol__call__()Allows objects to be called with parentheses:instance()Descriptor protocols__set__(), __get__(), and __del__()Allows us to manipulate the attribute access pattern of classes (see the Descriptors section)Container protocol__contains__()Allows us to test whether or not an object contains some value using the in keyword:value in instance + +Python in Comparison with Other LanguagesIterable protocol__iter__()Allows objects to be iterated using the forkeyword:for value in instance: ...Sequence protocol__getitem__(),__len__()Allows objects to be indexed with square bracket syntax and queried for length using a built-in function:item = instance[index]length = len(instance)Each operator available in Python has its own protocol and operator overloading happens by implementing the dunder methods of that protocol. Python provides over 50 overloadable operators that can be divided into five main groups:• Arithmetic operators • In-place assignment operators• Comparison operators• Identity operators• Bitwise operatorsThat's a lot of protocols so we won't discuss all of them here. We will instead take a look at a practical example that will allow you to better understand how to implement operator overloading on your own + + +The `__add__()` method is responsible for overloading the `+` (plus sign) operator and here it allows us to add +two matrices together. +Only matrices of the same dimensions can be added together. +This is a fairly simple operation that involves adding all matrix elements one by one to form a new matrix. + +The `__sub__()` method is responsible for overloading the `–` (minus sign) operator that will be responsible for matrix subtraction. +To subtract two matrices, we use a similar technique as in the – operator: + +[source,python] +---- +def __sub__(self, other): + if (len(self.rows) != len(other.rows) or len(self.rows[0]) != len(other.rows[0])): + raise ValueError("Matrix dimensions don't match") + return Matrix([[a - b for a, b in zip(a_row, b_row)] for a_row, b_row in zip(self.rows, other.rows) ]) +---- + +And the following is the last method we add to our class: + +[source,python] +---- +def __mul__(self, other): + if not isinstance(other, Matrix): + raise TypeError(f"Don't know how to multiply {type(other)} with Matrix") + + if len(self.rows[0]) != len(other.rows): + raise ValueError("Matrix dimensions don't match") + + rows = [[0 for _ in other.rows[0]] for _ in self.rows] + + for i in range(len (self.rows)): + for j in range(len (other.rows[0])): + for k in range(len (other.rows)): + rows[i][j] += self.rows[i][k] * other.rows[k][j] + + return Matrix(rows) +---- + +The last overloaded operator is the most complex one. +This is the `*` operator, which is implemented through the `__mul__()` method. +In linear algebra, matrices don't have the same multiplication operation as real numbers. +Two matrices can be multiplied if the first matrix has a number of columns equal to the number of rows of the second matrix. +The result of that operation is a new matrix where each element is a dot product of the corresponding row of the first matrix +and the corresponding column of the second matrix. +Here we've built our own implementation of the matrix to present the idea of operators overloading. +Although Python lacks a built-in type for matrices, you don't need to build them from scratch. +The NumPy package is one of the best Python mathematical packages and among others provides native support for matrix algebra. +You can easily obtain the NumPy package from PyPI + + +En fait, l'intérêt concerne surtout la représentation de nos modèles, puisque chaque classe du modèle est représentée par +la définition d'un objet Python. +Nous pouvons donc utiliser ces mêmes *dunder methods* (*double-underscores methods*) pour étoffer les protocoles du langage. === The Zen of Python @@ -94,11 +176,15 @@ Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous v === PEP257 - Docstring Conventions Python étant un langage interprété fortement typé, il est plus que conseillé, au même titre que les tests unitaires que nous verrons plus bas, de documenter son code. -Cela impose une certaine rigueur, mais améliore énormément la qualité (et la reprise) du code par une tierce personne. +Cela impose une certaine rigueur, mais améliore énormément la qualité, la compréhension et la reprise du code par une tierce personne. Cela implique aussi de **tout** documenter: les modules, les paquets, les classes, les fonctions, méthodes, ... -Tout doit avoir un *docstring* associé :-). +Ce qui peut également aller à contrecourant d'autres pratiques cite:[clean_code,53-74]; il y a une juste mesure à prendre entre "tout documenter" et "tout bien documenter": -WARNING: Documentation: be obsessed! +* Inutile d'ajouter des watermarks, auteurs, ... Git ou tout VCS s'en sortira très bien et sera beaucoup plus efficace que n'importe quelle chaîne de caractères que vous pourriez indiquer et qui sera fausse dans six mois, +* Inutile de décrire quelque chose qui est évident; documenter la méthode `get_age()` d'une personne n'aura pas beaucoup d'intérêt +* S'il est nécessaire de décrire un comportement au sein-même d'une fonction, c'est que ce comportement pourrait être extrait dans une nouvelle fonction (qui, elle, pourra être documentée) + +WARNING: Documentation: be obsessed! Mais *le code reste la référence* Il existe plusieurs types de conventions de documentation: @@ -107,8 +193,8 @@ Il existe plusieurs types de conventions de documentation: . Google Style (parfois connue sous l'intitulé `Napoleon`) . ... -Les https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings[conventions proposées par Google] nous semblent plus faciles à lire que du RestructuredText, mais sont parfois moins bien intégrées que les docstrings officiellement supportées (typiquement, par exemple par https://clize.readthedocs.io/en/stable/[clize] qui ne reconnait que du RestructuredText). -L'exemple donné dans les styleguide est celui-ci: +Les https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings[conventions proposées par Google] nous semblent plus faciles à lire que du RestructuredText, mais sont parfois moins bien intégrées que les docstrings officiellement supportées (par exemple, https://clize.readthedocs.io/en/stable/[clize] ne reconnait que du RestructuredText; https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/[l'auto-documentation] de Django également). +L'exemple donné dans les guides de style de Google est celui-ci: [source,python] ---- @@ -150,18 +236,75 @@ C'est-à-dire: . Une courte ligne d'introduction, descriptive, indiquant ce que la fonction ou la méthode réalise. Attention, la documentation ne doit pas indiquer _comment_ la fonction/méthode est implémentée, mais ce qu'elle fait concrètement (et succintement). . Une ligne vide -. Une description plus complète et plus verbeuse +. Une description plus complète et plus verbeuse, si vous le jugez nécessaire . Une ligne vide -. La description des arguments et paramètres, des valeurs de retour (+ exemples) et les exceptions qui peuvent être levées. +. La description des arguments et paramètres, des valeurs de retour, des exemples et les exceptions qui peuvent être levées. Un exemple (encore) plus complet peut être trouvé https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html#example-google[dans le dépôt sphinxcontrib-napoleon]. +Et ici, nous tombons peut-être dans l'excès de zèle: + +[source,python] +---- +def module_level_function(param1, param2=None, *args, **kwargs): + """This is an example of a module level function. + + Function parameters should be documented in the ``Args`` section. The name + of each parameter is required. The type and description of each parameter + is optional, but should be included if not obvious. + + If \*args or \*\*kwargs are accepted, + they should be listed as ``*args`` and ``**kwargs``. + + The format for a parameter is:: + + name (type): description + The description may span multiple lines. Following + lines should be indented. The "(type)" is optional. + + Multiple paragraphs are supported in parameter + descriptions. + + Args: + param1 (int): The first parameter. + param2 (:obj:`str`, optional): The second parameter. Defaults to None. + Second line of description should be indented. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + bool: True if successful, False otherwise. + + The return type is optional and may be specified at the beginning of + the ``Returns`` section followed by a colon. + + The ``Returns`` section may span multiple lines and paragraphs. + Following lines should be indented to match the first line. + + The ``Returns`` section supports any reStructuredText formatting, + including literal blocks:: + + { + 'param1': param1, + 'param2': param2 + } + + Raises: + AttributeError: The ``Raises`` section is a list of all exceptions + that are relevant to the interface. + ValueError: If `param2` is equal to `param1`. + + """ + if param1 == param2: + raise ValueError('param1 may not be equal to param2') + return True +---- Pour ceux que cela pourrait intéresser, il existe https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring[une extension pour Codium], comme nous le verrons juste après, qui permet de générer automatiquement le squelette de documentation d'un bloc de code: .autodocstring image::images/environment/python-docstring-vscode.png[] -NOTE: Nous le verrons plus loin, Django permet de rendre la documentation immédiatement accessible depuis son interface d'administration. +NOTE: Nous le verrons plus loin, Django permet de rendre la documentation immédiatement accessible depuis son interface d'administration. Toute information pertinente peut donc lier le code à un cas d'utilisation concret. ==== Linters diff --git a/source/part-1-workspace/maintainable-applications/_index.adoc b/source/part-1-workspace/maintainable-applications/_index.adoc index 95718a8..47e7fd4 100644 --- a/source/part-1-workspace/maintainable-applications/_index.adoc +++ b/source/part-1-workspace/maintainable-applications/_index.adoc @@ -14,8 +14,6 @@ include::12-factors.adoc[] include::maintainable-applications.adoc[] include::mccabe.adoc[] -=== Robustesse et flexibilité - include::solid.adoc[] === Intégrées diff --git a/source/part-1-workspace/maintainable-applications/solid.adoc b/source/part-1-workspace/maintainable-applications/solid.adoc index c6abf40..5544bed 100644 --- a/source/part-1-workspace/maintainable-applications/solid.adoc +++ b/source/part-1-workspace/maintainable-applications/solid.adoc @@ -1,71 +1,134 @@ -. SRP - Single responsibility principle +=== Robustesse et flexibilité + +> Un code mal pensé entraîne nécessairement une perte d'énergie et de temps. +> Il est plus simple de réfléchir, au moment de la conception du programme, à une architecture permettant une meilleure maintenabilité que de devoir corriger un code "sale" _a posteriori_. +> C'est pour aider les développeurs à rester dans le droit chemin que les principes SOLID ont été énumérés. GNU/Linux Magazine HS 104 cite:{gnu_linux_mag_hs_104, 26-44} + +Les principes SOLID, introduit par Robert C. Martin dans les années 2000 sont les suivants: + +. SRP - Single responsibility principle - Principe de Responsabilité Unique . OCP - Open-closed principle . LSP - Liskov Substitution . ISP - Interface ségrégation principle -. DIP - Dependency Inversion Principle +. DIP - Dependency Inversion Principle -En plus de ces principes de développement, il faut ajouter des principes architecturaux au niveau des composants: +En plus de ces principes de développement, il faut ajouter des principes au niveau des composants, puis un niveau plus haut, au niveau, au niveau architectural : -. Reuse/release équivalence principle, -. Common Closure Principle, -. Common Reuse Principle. +. Reuse/release équivalence principle, +. Common Closure Principle, +. Common Reuse Principle. -La même réflexion sera appliquée au niveau architectural, et se basera sur ces mêmes primitives. ==== Single Responsibility Principle -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. -Traduit autrement, SRP nous dit qu’une classe ne devrait pas contenir plus d’une raison de changer. -Traduit encore autrement, «  for software systems to be easy to change, they must be designed to allow the behavior to change -by adding new code instead of changing existing code. +Le principe de responsabilité unique conseille de disposer de concepts ou domaines d'activité qui ne s'occupent chacun que d'une et une seule chose. +Ceci rejoint un peu la https://en.wikipedia.org/wiki/Unix_philosophy[Philosophie Unix], documentée par Doug McIlroy et qui demande de "_faire une seule chose, mais le faire bien_" cite:{unix_philosophy}. +Une classe ou un élément de programmtion ne doit donc pas avoir plus d'une raison de changer. -Aussi : A module should be responsible to one and only one actor +Il est également possible d'étendre ce principe en fonction d'acteurs: -Le principe de responsabilité unique diffère d’une vue logique qui tendrait intuitivement à centraliser le maximum de code. -A l’inverse, il faut plutôt penser à différencier les acteurs. -Un exemple donné consiste à identifier le CFO, CTO et COO qui ont tous besoin de données et informations relatives à une même -base de données des membres du personnel, mais ils ont chacun besoin de données différents -(ou en tout cas, d’une représentation différente de ces données). -Nous sommes d’accord qu’il s’agit à chaque fois de données liées aux employés, mais elles vont un cran plus loin et -pourraient nécessiter des ajustements spécifiques en fonction de l’acteur concerné. +> A module should be responsible to one and only one actor cite:{clean_architecture} -L’idée sous-jacente est simplement d’identifier dès maintenant les différents acteurs, en vue de -prévoir une modification qui pourrait être demandée par l’un d’entre eux et qui pourrait avoir un -impact sur les données utilisées par les autres. +Plutôt que de centraliser le maximum de code à un seul endroit ou dans une seule classe par convenance ou commodité footnote:[Aussi appelé _God-Like object_], le principe de responsabilité unique suggère que chaque classe soit responsable d'un et un seul concept. +Une autre manière de voir les choses consiste à différencier les acteurs ou les intervenants: imaginez avoir une classe représentant des données de membres du personnel. +Ces données pourraient être demandées par trois acteurs, le CFO, le CTO et le COO: ceux-ci ont tous besoin de données et d'informations relatives à une même base de données centralisées, mais ont chacun besoin d'une représentation différente ou de traitements distincts. cite:{clean_architecture} +Nous sommes d’accord qu’il s’agit à chaque fois de données liées aux employés, mais elles vont un cran plus loin et pourraient nécessiter des ajustements spécifiques en fonction de l’acteur concerné. +L’idée sous-jacente est simplement d’identifier dès que possible les différents acteurs, en vue de prévoir une modification qui pourrait être demandée par l’un d’entre eux. +Dans le cas d'un élément de code centralisé, une modification induite par un des acteurs pourrait ainsi avoir un impact sur les données utilisées par les autres. -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, ...). +[source,python] +---- +class Document: + def __init__(self, title, content, published_at): + self.title = title + self.content = content + self.published_at = published_at -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 qui augmente la testabilité du code. + def render(self, format_type): + if format_type == "XML": + return """ + + {} + {} + {} + """.format( + self.title, + self.content, + self.published_at.isoformat() + ) -Au niveau des composants, le SRP deviendra le CCP. -Au niveau architectural, ce sera la définition des frontières (boundaries). + if format_type == "Markdown": + import markdown + return markdown.markdown(self.content) + + raise ValueError("Format type '{}' is not known".format(format_type)) +---- + +Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, ...), nous devrons modifier la classe `Document`, ce qui n'est pas vraiment intuitif. +Une bonne pratique consisterait à créer une classe de rendu par type de format à gérer: + +[source,python] +---- +class Document: + def __init__(self, title, content, published_at): + self.title = title + self.content = content + self.published_at = published_at + +class DocumentRenderer: + def render(self, document): + if format_type == "XML": + return """ + + {} + {} + {} + """.format( + self.title, + self.content, + self.published_at.isoformat() + ) + + if format_type == "Markdown": + import markdown + return markdown.markdown(self.content) + + raise ValueError("Format type '{}' is not known".format(format_type)) +---- + +A présent, lorsque nous devrons ajouter un nouveau format de prise en charge, nous irons modifier la classe `DocumentRenderer`, sans que la classe `Document` ne soit impactée. +En même temps, le jour où une instance de type `Document` sera liée à un champ `author`, rien ne dit que le rendu devra en tenir compte; nous modifierons donc notre classe pour y ajouter le nouveau champ sans que cela n'impacte nos différentes manières d'effectuer un rendu. + +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 de structurer le code permet de centraliser la configuration d'un type d'évènement à un seul endroit, ce qui augmente ainsi la testabilité globale du projet. + +Lorsque nous verrons les composants, le SRP deviendra le CCP. +Au niveau architectural, ce sera la définition des frontières (boundaries). ==== Open Closed -L’objectif est de rendre le système facile à étendre, en évitant que l’impact d’une modification ne soit trop grand. +> For software systems to be easy to change, they must be designed to allow the behavior to change by adding new code instead of changing existing code. -Les exemples parlent d’eux-mêmes: des données doivent être présentées dans une page web. -Et demain, ce seras dans un document PDF. -Et après demain, ce sera dans un tableur Excel. -La source de ces données restent la même (au travers d’une couche de présentation), mais la mise en forme diffère à chaque fois. +L’objectif est de rendre le système facile à étendre, en évitant que l’impact d’une modification ne soit trop grand. -L’application n’a pas à connaître les détails d’implémentation: elle doit juste permettre une forme d’extension, -sans avoir à appliquer une modification (ou une grosse modification) sur son cœur. +Les exemples parlent d’eux-mêmes: des données doivent être présentées dans une page web. +Et demain, ce seras dans un document PDF. +Et après demain, ce sera dans un tableur Excel. +La source de ces données restent la même (au travers d’une couche de présentation), mais la mise en forme diffère à chaque fois. -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. +L’application n’a pas à connaître les détails d’implémentation: elle doit juste permettre une forme d’extension, +sans avoir à appliquer une modification (ou une grosse modification) sur son cœur. + +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: * Une classe `Customer`, pour laquelle la méthode `GetDiscount` ne renvoit rien; * Une classe `SilverCustomer`, pour laquelle la méthode revoit une réduction de 10%; * Une classe `GoldCustomer`, pour laquelle la même méthode renvoit une réduction de 20%. -Si nous rencontrons un nouveau type de client, il suffit de créer une nouvelle sous-classe. +Si nous rencontrons un nouveau type de client, il suffit de créer une nouvelle sous-classe. Cela évite d'avoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction d'une autre variable (ici, le type de client). Nous passerions ainsi de: @@ -98,7 +161,7 @@ class Customer(): def get_discount(self) -> int: return 0 - + class SilverCustomer(Customer): def get_discount(self) -> int: return 10 @@ -114,48 +177,95 @@ class GoldCustomer(Customer): 10 ---- -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`]. +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._". -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. +*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. + +Nous pouvons également appliquer ceci à notre exemple sur les rendus de document, où le code suivant + +[source,python] +---- +class DocumentRenderer: + def render(self, document): + if format_type == "XML": + return """ + + {} + {} + {} + """.format( + document.title, + document.content, + document.published_at.isoformat() + ) + + if format_type == "Markdown": + import markdown + return markdown.markdown(document.content) + + raise ValueError("Format type '{}' is not known".format(format_type)) +---- + +devient le suivant: + +[source,python] +---- +class Renderer: + def render(self, document): + raise NotImplementedError + +class XmlRenderer(Renderer): + def render(self, document) + return """ + + {} + {} + {} + """.format( + document.title, + document.content, + document.published_at.isoformat() + ) + +class MarkdownRenderer(Renderer): + def render(self, document): + import markdown + return markdown.markdown(document.content) +---- + +Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons simplement une nouvelle classe de rendu qui héritera de `Renderer`. 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]. ==== Liskov Substitution NOTE: Dans Clean Architecture, ce chapitre ci (le 9) est sans doute celui qui est le moins complet. -Je suis d’accord avec les exemples donnés, dans la mesure où la définition concrète d’une classe doit dépendre -d’une interface correctement définie (et que donc, faire hériter un carré d’un rectangle, n’est pas adéquat -dans le mesure où cela induit l’utilisateur en erreur), mais il y est aussi question de la définition d'un -style architectural pour une interface REST, mais sans donner de solution… +Je suis d’accord avec les exemples donnés, dans la mesure où la définition concrète d’une classe doit dépendre d’une interface correctement définie (et que donc, faire hériter un carré d’un rectangle, n’est pas adéquat dans le mesure où cela induit l’utilisateur en erreur), mais il y est aussi question de la définition d'un style architectural pour une interface REST, mais sans donner de solution... -Le principe de substitution fait qu'une classe héritant d'une autre classe doit se comporter de la même manière que cette dernière. +Le principe de substitution fait qu'une classe héritant d'une autre classe doit se comporter de la même manière que cette dernière. Il n'est pas question que la sous-classe n'implémente pas certaines méthodes, alors que celles-ci sont disponibles sa classe parente. > [...] 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. +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`": +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`"_, nous allons nous retrouver avec ceci: [source,python] ---- -class Duck(): +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): @@ -164,27 +274,75 @@ class Lion(Duck): ---- +Le principe de substitution de Liskov suggère qu'une classe doit toujours pouvoir être considérée comme une instance de sa classe parent, et *doit pouvoir s'y substituer*. +Dans notre exemple, cela signifie que nous pourrons tout à fait accepter qu'un lion se comporte comme un canard et adore manger des plantes, insectes, graines, algues et du poisson. Miam ! Nous 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). +Pour revenir à nos exemples de rendus de documents, nous aurions pu faire hériter notre `MarkdownRenderer` de la classe `XmlRenderer`: -==== Interface Segregation +[source,python] +---- +class XmlRenderer: + def render(self, document) + return """ + + {} + {} + {} + """.format( + document.title, + document.content, + document.published_at.isoformat() + ) -L’objectif est de limiter la nécessité de compilation en n’exposant que les opérations nécessaires -à l’exécution d’une classe, et pour éviter d’avoir à redéployer l’ensemble d’une application. -En Python, c’est inféré lors de l’exécution, et donc pas vraiment d’application pour notre contexte d'étude. -De manière générale, les langages dynamiques sont plus flexibles et moins couples que les langages statiquement -typés, pour lesquels il serait possible de mettre à jour une DLL ou un JAR sans que cela n’ait d’impact sur le -reste de l’application. +class MarkdownRenderer(XmlRenderer): + def render(self, document): + import markdown + return markdown.markdown(document.content) +---- + +Mais lorsque nous ajouterons une fonction d'entête, notre rendu en Markdown héritera irrémédiablement de cette même méthode: + +[source,python] +---- +class XmlRenderer: + def header(self): + return """""" + + def render(self, document) + return """{} + + {} + {} + {} + """.format( + self.header(), + document.title, + document.content, + document.published_at.isoformat() + ) + +class MarkdownRenderer(XmlRenderer): + def render(self, document): + import markdown + return markdown.markdown(document.content) +---- + +A nouveau, lorsque nous invoquerons la méthode `header()` sur une instance de type `MarkdownRenderer`, nous obtiendrons un bloc de déclaration XML (``) pour un fichier Markdown. + + +==== Interface Segregation Principle + +Le principe de ségrégation d'interface suggère de limiter la nécessité de recompiler un module, en n’exposant que les opérations nécessaires à l’exécution d’une classe. +Ceci évite d’avoir à redéployer l’ensemble d’une application. > The lesson here is that depending on something that carries baggage that you don’t need can cause you troubles that you didn’t except. -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. +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. +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 : @@ -192,30 +350,32 @@ Pour contrer ceci, on aurait alors une première interface permettant la lecture * A : lecture * B (héritant de A) : lecture (par A) et écriture. -Mais ceci s'applique finalement à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, -les variables déclarées, ... +Mais ceci s'applique finalement à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, +les variables déclarées, ... + +En Python, ce comportement est inféré lors de l’exécution, et donc pas vraiment d’application pour notre contexte d'étude: de manière plus générale, les langages dynamiques sont plus flexibles et moins couplés que les langages statiquement typés, pour lesquels l'application de ce principe-ci permettrait de mettre à jour une DLL ou un JAR sans que cela n’ait d’impact sur le reste de l’application. ==== Dependency inversion Principle -Dans une architecture conventionnelle, les composants de haut-niveau dépendent directement des composants de bas-niveau. +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. -> The dependency inversion principle tells us that the most flexible systems are those in which source code dependencies +> The dependency inversion principle tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions. -L’objectif est que les interfaces soient stables, et de réduire au maximum les modifications qui pourraient y être appliquées. +L’objectif est que les interfaces soient stables, et de réduire au maximum les modifications qui pourraient y être appliquées. De la meme manière, il convient d’éviter de surcharger des fonctions ou des classes concrètes (= non abstraites). -En termes architecturaux, ce principe définira les frontières dont il a déjà été question, -en demandant à ce qu’une délimitation ne se base que sur une dépendance qui soit … -cela permet notamment une séparation claire au niveau des frontières, en inversant le flux de dépendance et en faisant en sorte (par exemple) que les règles métiers n’aient aucune connaissance des interfaces graphiques qui les exploitent. Ces interfaces pouvant être desktop, web, … cela n’a pas vraiment d’importance. Cela autorise une for.e d’immunité entre les composants. +En termes architecturaux, ce principe définira les frontières dont il a déjà été question, +en demandant à ce qu’une délimitation ne se base que sur une dépendance qui soit … +cela permet notamment une séparation claire au niveau des frontières, en inversant le flux de dépendance et en faisant en sorte (par exemple) que les règles métiers n’aient aucune connaissance des interfaces graphiques qui les exploitent. Ces interfaces pouvant être desktop, web, … cela n’a pas vraiment d’importance. Cela autorise une for.e d’immunité entre les composants. -Ne pas oublier qu’écrire du nouveau code est toujours plus facile que de modifier du code existant. +Ne pas oublier qu’écrire du nouveau code est toujours plus facile que de modifier du code existant. > The database is really nothing more than a big bucket of bits where we store our data on a long term basis (the database is a detail, chap 30, page 281) D’un point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, s’il s’agit d’un disque magnétique, de ram, … en fait, on ne devrait même pas savoir s’il y a un disque du tout. -Le composant de haut-niveau peut définir qu'il s'attend à avoir un `Publisher`, afin de publier du contenu vers un emplacement particulier. +Le composant de haut-niveau peut définir qu'il s'attend à avoir un `Publisher`, afin de publier du contenu vers un emplacement particulier. Plusieurs implémentation de cette interface peuvent alors être mise en place: * Une publication par SSH @@ -235,24 +395,24 @@ L'injection de dépendances est un patron de programmation qui suit le principe === au niveau des composants -De la même manière que pour les principes définis ci-dessus, -Mais toujours en faisant attention qu’une fois que les frontières sont implémentés, elles sont coûteuses à maintenir. -Cependant, il ne s’agit pas une décision à réaliser une seule fois, puisque cela peut être réévalué. +De la même manière que pour les principes définis ci-dessus, +Mais toujours en faisant attention qu’une fois que les frontières sont implémentés, elles sont coûteuses à maintenir. +Cependant, il ne s’agit pas une décision à réaliser une seule fois, puisque cela peut être réévalué. -Et de la même manière que nous devons délayer au maximum les choix architecturaux et techniques, +Et de la même manière que nous devons délayer au maximum les choix architecturaux et techniques, -> but this is not a one time decision. You don’t simply decide at the start of a project which boundaries to implémentent and which to ignore. Rather, you watch. You pay attention as the system evolves. You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries don’t exist. -> at that point, you weight the costs of implementing those boundaries versus the cost of ignoring them and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing becomes less than the cost of ignoring. +> but this is not a one time decision. You don’t simply decide at the start of a project which boundaries to implémentent and which to ignore. Rather, you watch. You pay attention as the system evolves. You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries don’t exist. +> at that point, you weight the costs of implementing those boundaries versus the cost of ignoring them and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing becomes less than the cost of ignoring. -En gros, il faut projeter sur la capacité à s’adapter en minimisant la maintenance. +En gros, il faut projeter sur la capacité à s’adapter en minimisant la maintenance. Le problème est qu’elle ne permettait aucune adaptation, et qu’à la première demande, l’architecture se plante complètement sans aucune malléabilité. -==== Reuse/release equivalence principle +==== Reuse/release equivalence principle [quote] ---- -Classes and modules that are grouped together into a component should be releasable together +Classes and modules that are grouped together into a component should be releasable together -- (Chapitre 13, Component Cohesion, page 105) ---- @@ -260,13 +420,13 @@ Classes and modules that are grouped together into a component should be releasa (= l’équivalent du SRP, mais pour les composants) -> If two classes are so tightly bound, either physically or conceptually, that they always change together, then they belong in the same component +> If two classes are so tightly bound, either physically or conceptually, that they always change together, then they belong in the same component Il y a peut-être aussi un lien à faire avec « Your code as a crime scene » 🤟 La définition exacte devient celle-ci: « gather together those things that change at the same times and for the same reasons. Separate those things that change at different times or for different reasons ». - ==== CRP + ==== CRP … que l’on résumera ainsi: « don’t depend on things you don’t need » 😘 Au niveau des composants, au niveau architectural, mais également à d’autres niveaux. @@ -274,11 +434,11 @@ Au niveau des composants, au niveau architectural, mais également à d’autres ==== SDP (Stable dependency principle) qui définit une formule de stabilité pour les composants, en fonction de sa faculté à être modifié et des composants qui dépendent de lui: au plus un composant est nécessaire, au plus il sera stable (dans la mesure où il lui sera difficile de changer). En C++, cela correspond aux mots clés #include. -Pour faciliter cette stabilité, il convient de passer par des interfaces (donc, rarement modifiées, par définition). +Pour faciliter cette stabilité, il convient de passer par des interfaces (donc, rarement modifiées, par définition). -En Python, ce ratio pourrait être calculé au travers des import, via les AST. +En Python, ce ratio pourrait être calculé au travers des import, via les AST. ==== SAP -(= Stable abstraction principle) pour la définition des politiques de haut niveau vs les composants plus concrets. -SAP est juste une modélisation du OCP pour les composants: nous plaçons ceux qui ne changent pas ou pratiquement pas le plus haut possible dans l’organigramme (ou le diagramme), et ceux qui changent souvent plus bas, dans le sens de stabilité du flux. Les composants les plus bas sont considérés comme volatiles +(= Stable abstraction principle) pour la définition des politiques de haut niveau vs les composants plus concrets. +SAP est juste une modélisation du OCP pour les composants: nous plaçons ceux qui ne changent pas ou pratiquement pas le plus haut possible dans l’organigramme (ou le diagramme), et ceux qui changent souvent plus bas, dans le sens de stabilité du flux. Les composants les plus bas sont considérés comme volatiles diff --git a/source/part-3-data-model/_index.adoc b/source/part-3-data-model/_index.adoc index cf8c49c..27cfe2b 100644 --- a/source/part-3-data-model/_index.adoc +++ b/source/part-3-data-model/_index.adoc @@ -1,12 +1,12 @@ -= (meta)Data Model += Principes fondamentaux -Dans ce chapitre, nous allons parler de plusieurs concepts utiles au développement rapide d'une application. +Dans ce chapitre, nous allons parler de plusieurs concepts fondamentaux au développement rapide d'une application. Nous parlerons de modélisation, de métamodèle, de migrations, d'administration auto-générée, de traductions et de cycle de vie des données. -Django est un framework Web proposant une très bonne intégration des composants et une flexibilité bien pensée: chacun des composants permet de définir son contenu de manière poussée, en respectant des contraintes logiques et faciles à retenir. -Pour un néophyte, la courbe d'apprentissage sera relativement ardue; à côté de concepts clés de Django, il conviendra également d'assimiler correctement les structures de données du langage Python, le cycle de vie d'une requête HTTP +Django est un framework Web qui propose une très bonne intégration des composants et une flexibilité bien pensée: chacun des composants permet de définir son contenu de manière poussée, en respectant des contraintes logiques et faciles à retenir, et en gérant ses dépendances de manière autonome. +Pour un néophyte, la courbe d'apprentissage sera relativement ardue: à côté de concepts clés de Django, il conviendra également d'assimiler correctement les structures de données du langage Python, le cycle de vie des requêtes HTTP et le B.A-BA des principes de sécurité. -En restant dans les sentiers battus, votre projet suivra un dérivé du patron de conception `MVC` (Modèle-Vue-Controleur), où la variante concerne les termes utilisés: Django les nomme respectivement Modèle-Template-Vue et leur contexte d'utilisation. +En restant dans les sentiers battus, votre projet suivra un patron de conception dérivé du modèle `MVC` (Modèle-Vue-Controleur), où la variante concerne les termes utilisés: Django les nomme respectivement Modèle-Template-Vue et leur contexte d'utilisation. Dans un *pattern* MVC classique, la traduction immédiate du **contrôleur** est une **vue**. Et comme on le verra par la suite, la **vue** est en fait le **template**. diff --git a/source/part-3-data-model/admin.adoc b/source/part-3-data-model/admin.adoc index ccf33d3..5844db4 100644 --- a/source/part-3-data-model/admin.adoc +++ b/source/part-3-data-model/admin.adoc @@ -265,3 +265,13 @@ if rows_updated = 0: else: self.message_user(request, "{} élément(s) mis à jour".format(rows_updated)) ---- + +=== La documentation + +Nous l'avons dit plus haut, l'administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données. +Pour cela, il suffit de suivre les bonnes pratiques, puis https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/[d'activer la documentation à partir des URLs]: + +[source,python] +---- + +---- diff --git a/source/part-3-data-model/models.adoc b/source/part-3-data-model/models.adoc index c9f859f..b85a0b6 100644 --- a/source/part-3-data-model/models.adoc +++ b/source/part-3-data-model/models.adoc @@ -1,29 +1,29 @@ == 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 relationnelle, et 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, sous forme d'une 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. +Django utilise un paradigme de persistence des données 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 respectant une approche 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 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, à tel point que *ne pas utiliser cette brique de fonctionnalités* peut remettre en question le choix du framework. +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 à cet ORM à: -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, 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, -3. A une interface d'administration auto-générée -4. A un mécanisme de formulaires HTML qui soit complet, pratique à utiliser, orienté objet et facile à faire évoluer, -5. De définir des notions d'héritage (tout en restant dans une forme d'héritage simple). +1. Des migrations de données, +2. Un découplage complet entre le moteur de données relationnel et le modèle de données, +3. Une interface d'administration auto-générée +4. Un mécanisme de formulaires HTML qui soit complet, pratique à utiliser, orienté objet et facile à faire évoluer, +5. Une définition des notions d'héritage (tout en restant dans une forme d'héritage simple). Comme tout ceci reste au niveau du code, cela suit également la méthodologie des douze facteurs, concernant la minimisation des divergences entre environnements d'exécution: comme tout se trouve au niveau du code, il n'est plus nécessaire d'avoir un DBA qui doive démarrer un script sur un serveur au moment de la mise à jour, de recevoir une release note de 512 pages en PDF reprenant les modifications ou de nécessiter l'intervention de trois équipes différentes lors d'une modification majeure du code. 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. +=== Active Records -L'exemple ci-dessous (en Java) présente trois structure de données, qui exposent chacune ses propres champs: +Il faut noter que 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. + +L'exemple ci-dessous présente trois structure de données, qui exposent chacune leurs propres champs: [source,python] ---- @@ -105,24 +105,27 @@ class Circle(Shape): ---- 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. +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 que de l'isoler 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. +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 entre 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 leur 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 du code. 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. +En résumé, toutes les méthodes qui font qu'une instance sait **comment** interagir avec la base de données. -=== Types de champs, clés étrangères et relations +=== Types de champs + + +=== Relations et clés étrangères 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. +Django, de son côté, ajoute une couche de typage statique exigé par le lien sous-jacent avec le moteur de base de données relationnelle. 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`. @@ -137,24 +140,60 @@ 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) ---- +Par défaut, et si aucune propriété ne dispose d'un attribut `primary_key=True`, Django s'occupera d'ajouter un champ `id` grâce à son héritage de la classe `models.Model`. +Les autres champs nous permettent d'identifier une catégorie (`Category`) par un nom, tandis qu'un livre (`Book`) le sera par ses propriétés `title` et une clé de relation vers une catégorie. +Un livre est donc lié à une catégorie, tandis qu'une catégorie est associée à plusieurs livres. +image::diagrams/books-foreign-keys-example.drawio.png[] -. ForeignKey -. ManyToManyField -. OneToOneField +En termes de code d'initialisation, cela revient écrire ceci: -Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (**ForeignKey**) d'une classe A vers une classe B. -Pour représenter d'autres types de relations, il existe également les champs de type *ManyToManyField*, afin de représenter une relation N-N. Les champs de type *OneToOneField*, pour représenter une relation 1-1. +[source,python] +---- +from library.models import Book, Category -Dans notre modèle ci-dessus, nous n'avons jusqu'à présent eu besoin que des relations 1-N: +movies = Category.objects.create(name="Adaptations au cinéma") +medieval = Category.objects.create(name="Médiéval-Fantastique") +science_fiction = Category.objects.create(name="Sciences-fiction") +computers = Category.objects.create(name="Sciences Informatiques") -. La première entre les listes de souhaits et les souhaits; -. La seconde entre les souhaits et les parts. +books = { + "Harry Potter": movies, + "The Great Gatsby": movies, + "Dune": science_fiction, + "H2G2": science_fiction, + "Ender's Game": science_fiction, + "Le seigneur des anneaux": medieval, + "L'Assassin Royal", medieval, + "Clean code": computers, + "Designing Data-Intensive Applications": computers +} + +for book_title, category in books.items: + Book.objects.create(name=book_title, category=category) +---- + +Nous nous rendons rapidement compte qu'un livre peut appartenir à plusieurs catégories: _Dune_ a été adapté au cinéma en 1973 et en 2021, de même que _Le Seigneur des Anneaux_, _The Great Gatsby_, et sans doute que nous pourrons étoffer notre bibliothèque avec une catégorie spéciale "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga _Harry Potter_. +En clair, notre modèle n'est pas adapté, et nous devons le modifier pour que notre clé étrangère accepte plusieurs valeurs. +Ceci peut être fait au travers d'un champ de type `ManyToMany`, c'est-à-dire qu'un livre peut être lié à plusieurs catégories, et qu'une catégorie peut être liée à plusieurs livres. + +[source,python,highlight=6] +---- +class Category(models.Model): + name = models.CharField(max_length=255) + +class Book(models.Model): + title = models.CharField(max_length=255) + category = models.ManyManyField(Category, on_delete=models.CASCADE) +---- + +Notre code d'initialisation reste par contre identique: Django s'occupe parfaitement de gérer la transition. + +==== Accès aux relations [source,python] ---- @@ -172,6 +211,7 @@ Depuis le code, à partir de l'instance de la classe `Item`, on peut donc accéd Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut `related_name` afin de nommer la relation inverse. + [source,python] ---- # wish/models.py @@ -186,8 +226,7 @@ class Item(models.Model): NOTE: Si, dans une classe A, plusieurs relations sont liées à une classe B, Django ne saura pas à quoi correspondra la relation inverse. Pour palier à ce problème, nous fixons une valeur à l'attribut `related_name`. -Par facilité (et pas conventions), prenez l'habitude de toujours ajouter cet attribut. -Votre modèle gagnera en cohérence et en lisibilité. +Par facilité (et par conventions), prenez l'habitude de toujours ajouter cet attribut: votre modèle gagnera en cohérence et en lisibilité. Si cette relation inverse n'est pas nécessaire, il est possible de l'indiquer (par convention) au travers de l'attribut `related_name="+"`. A partir de maintenant, nous pouvons accéder à nos propriétés de la manière suivante: @@ -207,16 +246,28 @@ A partir de maintenant, nous pouvons accéder à nos propriétés de la manière [] ---- +==== N+1 Queries + + +=== Unicité + + +=== Indices + + +==== Conclusions + +Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (**ForeignKey**) d'une classe A vers une classe B. +Pour représenter d'autres types de relations, il existe également les champs de type *ManyToManyField*, afin de représenter une relation N-N. +Il existe également un type de champ spécial pour les clés étrangères, qui est le Les champs de type *OneToOneField*, pour représenter une relation 1-1. ==== Metamodèle et introspection - - -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`. +Comme 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`. En plus de cela, il faut bien tenir compte des propriétés `Meta` de la classe: si elle contient déjà un ordre par défaut, celui-ci sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe. -[source,python] +[source,python,highlight=5] ---- class Wish(models.Model): name = models.CharField(max_length=255) @@ -224,7 +275,7 @@ class Wish(models.Model): class Meta: ordering = ('name',) <1> ---- -<1> On définit un ordre par défaut, directement au niveau du modèle. Cela ne signifie pas qu'il ne sera pas possible de modifier cet ordre (la méthode `order_by` existe et peut être chaînée à n'importe quel queryset). D'où l'intérêt de tester ce type de comportement, dans la mesure où un `top 1` dans votre code pourrait être modifié simplement par cette petite information. +<1> Nous définissons un ordre par défaut, directement au niveau du modèle. Cela ne signifie pas qu'il ne sera pas possible de modifier cet ordre (la méthode `order_by` existe et peut être chaînée à n'importe quel _queryset_). D'où l'intérêt de tester ce type de comportement, dans la mesure où un `top 1` dans votre code pourrait être modifié simplement par cette petite information. Pour sélectionner un objet au pif : `return Category.objects.order_by("?").first()` @@ -269,85 +320,7 @@ class Runner(models.Model): -==== Protocoles de langage (*dunder methods*) -The Python data model specifies a lot of specially named methods that can be overridden in your custom classes to provide -them with additional syntax capabilities. -You can recognize these methods by their specific naming conventions that wrap the method name with double underscores. -Because of this, they are sometimes referred to as dunder methods. -It is simply shorthand for double underscores.The most common and obvious example of such dunder methods is __init__(), -which is used for class instance initialization: - -[source,python] ----- -class CustomUserClass: - def __init__(self, initiatization_argument): - ... ----- - -En fait, l'intérêt concerne surtout la représentation de nos modèles, puisque chaque classe du modèle est représentée par -la définition d'un objet Python. -Nous pouvons donc utiliser ces mêmes *dunder methods* (*double-underscores methods*) pour étoffer les protocoles du langage. - -These methods, either alone or when defined in a specific combination, constitute the so-called language protocols. -If we say that an object implements a specific language protocol, it means that it is compatible with a specific part of the Python language syntax. The following is a table of the most common protocols within the Python language.Protocol nameMethodsDescriptionCallable protocol__call__()Allows objects to be called with parentheses:instance()Descriptor protocols__set__(), __get__(), and __del__()Allows us to manipulate the attribute access pattern of classes (see the Descriptors section)Container protocol__contains__()Allows us to test whether or not an object contains some value using the in keyword:value in instance -Python in Comparison with Other LanguagesIterable protocol__iter__()Allows objects to be iterated using the forkeyword:for value in instance: ...Sequence protocol__getitem__(),__len__()Allows objects to be indexed with square bracket syntax and queried for length using a built-in function:item = instance[index]length = len(instance)Each operator available in Python has its own protocol and operator overloading happens by implementing the dunder methods of that protocol. Python provides over 50 overloadable operators that can be divided into five main groups:• Arithmetic operators • In-place assignment operators• Comparison operators• Identity operators• Bitwise operatorsThat's a lot of protocols so we won't discuss all of them here. We will instead take a look at a practical example that will allow you to better understand how to implement operator overloading on your own - -A full list of available dunder methods can be found in the Data model section of the official Python documentation available -at https://docs.python.org/3/reference/datamodel.html. -All operators are also exposed as ordinary functions in the operators module. -The documentation of that module gives a good overview of Python operators. -It can be found at https://docs.python.org/3.9/library/operator.html - -The `__add__()` method is responsible for overloading the `+` (plus sign) operator and here it allows us to add -two matrices together. -Only matrices of the same dimensions can be added together. -This is a fairly simple operation that involves adding all matrix elements one by one to form a new matrix. - -The `__sub__()` method is responsible for overloading the `–` (minus sign) operator that will be responsible for matrix subtraction. -To subtract two matrices, we use a similar technique as in the – operator: - -[source,python] ----- -def __sub__(self, other): - if (len(self.rows) != len(other.rows) or len(self.rows[0]) != len(other.rows[0])): - raise ValueError("Matrix dimensions don't match") - return Matrix([[a - b for a, b in zip(a_row, b_row)] for a_row, b_row in zip(self.rows, other.rows) ]) ----- - -And the following is the last method we add to our class: - -[source,python] ----- -def __mul__(self, other): - if not isinstance(other, Matrix): - raise TypeError(f"Don't know how to multiply {type(other)} with Matrix") - - if len(self.rows[0]) != len(other.rows): - raise ValueError("Matrix dimensions don't match") - - rows = [[0 for _ in other.rows[0]] for _ in self.rows] - - for i in range(len (self.rows)): - for j in range(len (other.rows[0])): - for k in range(len (other.rows)): - rows[i][j] += self.rows[i][k] * other.rows[k][j] - - return Matrix(rows) ----- - -The last overloaded operator is the most complex one. -This is the `*` operator, which is implemented through the `__mul__()` method. -In linear algebra, matrices don't have the same multiplication operation as real numbers. -Two matrices can be multiplied if the first matrix has a number of columns equal to the number of rows of the second matrix. -The result of that operation is a new matrix where each element is a dot product of the corresponding row of the first matrix -and the corresponding column of the second matrix. -Here we've built our own implementation of the matrix to present the idea of operators overloading. -Although Python lacks a built-in type for matrices, you don't need to build them from scratch. -The NumPy package is one of the best Python mathematical packages and among others provides native support for matrix algebra. -You can easily obtain the NumPy package from PyPI - -(Voir Expert Python Programming, 4th Edition, page 142-144) ==== Constructeurs diff --git a/source/part-4-services-oriented-applications/_main.adoc b/source/part-4-services-oriented-applications/_main.adoc index 739ddfe..6c01a15 100644 --- a/source/part-4-services-oriented-applications/_main.adoc +++ b/source/part-4-services-oriented-applications/_main.adoc @@ -13,6 +13,10 @@ include::querysets.adoc[] include::urls.adoc[] +include::rest.adoc[] + +include::trees.adoc[] + == Conclusions De part son pattern `MVT`, Django ne fait pas comme les autres frameworks. diff --git a/source/part-4-services-oriented-applications/querysets.adoc b/source/part-4-services-oriented-applications/querysets.adoc index 77d741d..672e703 100644 --- a/source/part-4-services-oriented-applications/querysets.adoc +++ b/source/part-4-services-oriented-applications/querysets.adoc @@ -4,67 +4,67 @@ * https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator * http://blog.etianen.com/blog/2013/06/08/django-querysets/ -L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants: +L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants: -* Les managers, qui consistent en un point d'entrée pour accéder aux objets persistants -* Les querysets, qui permettent de filtrer des ensembles ou sous-ensemble d'objets. Les querysets peuvent s'imbriquer, pour ajouter -d'autres filtres à des filtres existants. +* Les `managers`, qui consistent en un point d'entrée pour accéder aux objets persistants +* Les `querysets`, qui permettent de filtrer des ensembles ou sous-ensemble d'objets. Les querysets peuvent s'imbriquer, pour ajouter +d'autres filtres à des filtres existants, et fonctionnent comme un super jeu d'abstraction pour accéder à nos données (persistentes). -Ces deux propriétés vont de paire; par défaut, chaque classe de votre modèle propose un attribut `objects`, qui correspond - à un manager (ou un gestionnaire, si vous préférez). +Ces deux propriétés vont de paire; par défaut, chaque classe de votre modèle propose un attribut `objects`, qui correspond + à un manager (ou un gestionnaire, si vous préférez). Ce gestionnaire constitue l'interface par laquelle vous accéderez à la base de données. Mais pour cela, vous aurez aussi besoin d'appliquer certains requêtes ou filtres. Et pour cela, vous aurez besoin des `querysets`, qui consistent en des ... ensembles de requêtes :-). Si on veut connaître la requête SQL sous-jacente à l'exécution du queryset, il suffit d'appeler la fonction str() sur la propriété `query`: - + [source,python] ---- queryset = Wishlist.objects.all() - + print(queryset.query) ---- - + Conditions AND et OR sur un queryset - + Pour un `AND`, il suffit de chaîner les conditions. ** trouver un exemple ici ** :-) - + Mais en gros : bidule.objects.filter(condition1, condition2) - + Il existe deux autres options : combiner deux querysets avec l'opérateur `&` ou combiner des Q objects avec ce même opérateur. - + Soit encore combiner des filtres: - + [source,python] ---- from core.models import Wish -Wish.objects <1> +Wish.objects <1> Wish.objects.filter(name__icontains="test").filter(name__icontains="too") <2> ---- -<1> Ca, c'est notre manager. -<2> Et là, on chaîne les requêtes pour composer une recherche sur tous les souhaits dont le nom contient (avec une casse insensible) la chaîne "test" et dont le nom contient la chaîne "too". - -Pour un 'OR', on a deux options : - +<1> Ca, c'est notre manager. +<2> Et là, on chaîne les requêtes pour composer une recherche sur tous les souhaits dont le nom contient (avec une casse insensible) la chaîne "test" et dont le nom contient la chaîne "too". + +Pour un 'OR', on a deux options : + . Soit passer par deux querysets, typiuqment `queryset1 | queryset2` . Soit passer par des `Q objects`, que l'on trouve dans le namespace `django.db.models`. - + [source,python] ---- from django.db.models import Q - + condition1 = Q(...) condition2 = Q(...) - + bidule.objects.filter(condition1 | condition2) ---- L'opérateur inverse (_NOT_) - + Idem que ci-dessus : soit on utilise la méthode `exclude` sur le queryset, soit l'opérateur `~` sur un Q object; - - -Ajouter les sujets suivants : - + + +Ajouter les sujets suivants : + . Prefetch . select_related @@ -90,28 +90,28 @@ Cette propriété a une double utilité: ==== Requêtes DANGER: Les requêtes sont sensibles à la casse, **même** si le moteur de base de données ne l'est pas. -C'est notamment le cas pour Microsoft SQL Server; faire une recherche directement via les outils de Microsoft ne retournera pas +C'est notamment le cas pour Microsoft SQL Server; faire une recherche directement via les outils de Microsoft ne retournera pas obligatoirement les mêmes résultats que les managers, qui seront beaucoup plus tatillons sur la qualité des recherches par rapport aux filtres paramétrés en entrée. ==== Jointures Pour appliquer une jointure sur un modèle, nous pouvons passer par les méthodes `select_related` et `prefetch_related`. -Il faut cependant faire **très** attention au prefetch related, qui fonctionne en fait comme une grosse requête dans laquelle -nous trouvons un `IN (...)`. -Càd que Django va récupérer tous les objets demandés initialement par le queryset, pour ensuite prendre toutes les clés primaires, -pour finalement faire une deuxième requête et récupérer les relations externes. +Il faut cependant faire **très** attention au prefetch related, qui fonctionne en fait comme une grosse requête dans laquelle +nous trouvons un `IN (...)`. +Càd que Django va récupérer tous les objets demandés initialement par le queryset, pour ensuite prendre toutes les clés primaires, +pour finalement faire une deuxième requête et récupérer les relations externes. Au final, si votre premier queryset est relativement grand (nous parlons de 1000 à 2000 éléments, en fonction du moteur de base de données), la seconde requête va planter et vous obtiendrez une exception de type `django.db.utils.OperationalError: too many SQL variables`. -Nous pourrions penser qu'utiliser un itérateur permettrait de combiner les deux, mais ce n'est pas le cas... +Nous pourrions penser qu'utiliser un itérateur permettrait de combiner les deux, mais ce n'est pas le cas... Comme l'indique la documentation: Note that if you use iterator() to run the query, prefetch_related() calls will be ignored since these two optimizations do not make sense together. -Ajouter un itérateur va en fait forcer le code à parcourir chaque élément de la liste, pour l'évaluer. +Ajouter un itérateur va en fait forcer le code à parcourir chaque élément de la liste, pour l'évaluer. Il y aura donc (à nouveau) autant de requêtes qu'il y a d'éléments, ce que nous cherchons à éviter. [source,python] @@ -125,5 +125,5 @@ informations = ( ---- === Aggregate vs. Annotate - + https://docs.djangoproject.com/en/3.1/topics/db/aggregation/ \ No newline at end of file diff --git a/source/part-4-services-oriented-applications/rest.adoc b/source/part-4-services-oriented-applications/rest.adoc new file mode 100644 index 0000000..fa3f21d --- /dev/null +++ b/source/part-4-services-oriented-applications/rest.adoc @@ -0,0 +1,378 @@ +== Application Programming Interface + +Au niveau du modèle, nous allons partir de quelque chose de très simple: des personnes, des contrats, des types de contrats, et un service d'affectation. +Quelque chose comme ceci: + +[source,python] +---- +# models.py + +from django.db import models + + +class People(models.Model): + CIVILITY_CHOICES = ( + ("M", "Monsieur"), + ("Mme", "Madame"), + ("Dr", "Docteur"), + ("Pr", "Professeur"), + ("", "") + ) + + last_name = models.CharField(max_length=255) + first_name = models.CharField(max_length=255) + civility = models.CharField( + max_length=3, + choices=CIVILITY_CHOICES, + default="" + ) + + def __str__(self): + return "{}, {}".format(self.last_name, self.first_name) + + +class Service(models.Model): + label = models.CharField(max_length=255) + + def __str__(self): + return self.label + + +class ContractType(models.Model): + label = models.CharField(max_length=255) + short_label = models.CharField(max_length=50) + + def __str__(self): + return self.short_label + + +class Contract(models.Model): + people = models.ForeignKey(People, on_delete=models.CASCADE) + date_begin = models.DateField() + date_end = models.DateField(blank=True, null=True) + contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE) + service = models.ForeignKey(Service, on_delete=models.CASCADE) + + def __str__(self): + if self.date_end is not None: + return "A partir du {}, jusqu'au {}, dans le service {} ({})".format( + self.date_begin, + self.date_end, + self.service, + self.contract_type + ) + + return "A partir du {}, à durée indéterminée, dans le service {} ({})".format( + self.date_begin, + self.service, + self.contract_type + ) +---- + +image::images/rest/models.png[] + +## Configuration + +La configuration des points de terminaison de notre API est relativement touffue. +Il convient de: + +1. Configurer les sérialiseurs, càd. les champs que nous souhaitons exposer au travers de l'API, +2. Configurer les vues, càd le comportement de chacun des points de terminaison, +3. Configurer les points de terminaison eux-mêmes, càd les URLs permettant d'accéder aux ressources. +4. Et finalement ajouter quelques paramètres au niveau de notre application. + +### Sérialiseurs + +```python +# serializers.py + +from django.contrib.auth.models import User, Group +from rest_framework import serializers + +from .models import People, Contract, Service + + +class PeopleSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = People + fields = ("last_name", "first_name", "contract_set") + + +class ContractSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Contract + fields = ("date_begin", "date_end", "service") + + +class ServiceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Service + fields = ("name",) + +``` + +### Vues + +```python +# views.py + +from django.contrib.auth.models import User, Group +from rest_framework import viewsets +from rest_framework import permissions + +from .models import People, Contract, Service +from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer + + +class PeopleViewSet(viewsets.ModelViewSet): + queryset = People.objects.all() + serializer_class = PeopleSerializer + permission_class = [permissions.IsAuthenticated] + + +class ContractViewSet(viewsets.ModelViewSet): + queryset = Contract.objects.all() + serializer_class = ContractSerializer + permission_class = [permissions.IsAuthenticated] + + +class ServiceViewSet(viewsets.ModelViewSet): + queryset = Service.objects.all() + serializer_class = ServiceSerializer + permission_class = [permissions.IsAuthenticated] + +``` + +### URLs + +```python +# urls.py + +from django.contrib import admin +from django.urls import path, include + +from rest_framework import routers + +from core import views + + +router = routers.DefaultRouter() +router.register(r"people", views.PeopleViewSet) +router.register(r"contracts", views.ContractViewSet) +router.register(r"services", views.ServiceViewSet) + +urlpatterns = [ + path("api/v1/", include(router.urls)), + path('admin/', admin.site.urls), +] +``` + +### Paramètres + +```python +# settings.py + +INSTALLED_APPS = [ + ... + "rest_framework", + ... +] + +... + +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10 +} + +``` + +A ce stade, en nous rendant sur l'URL `http://localhost:8000/api/v1`, nous obtiendrons ceci: + +image::images/rest/api-first-example.png[] + + +## Modèles et relations + +Plus haut, nous avons utilisé une relation de type `HyperlinkedModelSerializer`. +C'est une bonne manière pour autoriser des relations entre vos instances à partir de l'API, mais il faut reconnaître que cela reste assez limité. +Pour palier à ceci, il existe [plusieurs manières de représenter ces relations](https://www.django-rest-framework.org/api-guide/relations/): soit *via* un hyperlien, comme ci-dessus, soit en utilisant les clés primaires, soit en utilisant l'URL canonique permettant d'accéder à la ressource. +La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats): + +```json +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "last_name": "Bond", + "first_name": "James", + "contract_set": [ + "http://localhost:8000/api/v1/contracts/1/", + "http://localhost:8000/api/v1/contracts/2/" + ] + } + ] +} +``` + +à ceci: + +```json +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "last_name": "Bond", + "first_name": "James", + "contract_set": [ + { + "date_begin": "2019-01-01", + "date_end": null, + "service": "http://localhost:8000/api/v1/services/1/" + }, + { + "date_begin": "2009-01-01", + "date_end": "2021-01-01", + "service": "http://localhost:8000/api/v1/services/1/" + } + ] + } + ] +} +``` + +La modification se limite à **surcharger** la propriété, pour indiquer qu'elle consiste en une instance d'un des sérialiseurs existants. +Nous passons ainsi de ceci + +```python +class ContractSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Contract + fields = ("date_begin", "date_end", "service") + + +class PeopleSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = People + fields = ("last_name", "first_name", "contract_set") +``` + +à ceci: + +```python +class ContractSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Contract + fields = ("date_begin", "date_end", "service") + + +class PeopleSerializer(serializers.HyperlinkedModelSerializer): + contract_set = ContractSerializer(many=True, read_only=True) + + class Meta: + model = People + fields = ("last_name", "first_name", "contract_set") +``` + +Nous ne faisons donc bien que redéfinir la propriété `contract_set` et indiquons qu'il s'agit à présent d'une instance de `ContractSerializer`, et qu'il est possible d'en avoir plusieurs. +C'est tout. + +## Filtres et recherches + +A ce stade, nous pouvons juste récupérer des informations présentes dans notre base de données, mais à part les parcourir, il est difficile d'en faire quelque chose. + +Il est possible de jouer avec les URLs en définissant une nouvelle route ou avec les paramètres de l'URL, ce qui demanderait alors de programmer chaque cas possible - sans que le consommateur ne puisse les déduire lui-même. Une solution élégante consiste à autoriser le consommateur à filtrer les données, directement au niveau de l'API. +Ceci peut être fait. Il existe deux manières de restreindre l'ensemble des résultats retournés: + +1. Soit au travers d'une recherche, qui permet d'effectuer une recherche textuelle, globale et par ensemble à un ensemble de champs, +2. Soit au travers d'un filtre, ce qui permet de spécifier une valeur précise à rechercher. + +Dans notre exemple, la première possibilité sera utile pour rechercher une personne répondant à un ensemble de critères. Typiquement, `/api/v1/people/?search=raymond bond` ne nous donnera aucun résultat, alors que `/api/v1/people/?search=james bond` nous donnera le célèbre agent secret (qui a bien entendu un contrat chez nous...). + +Le second cas permettra par contre de préciser que nous souhaitons disposer de toutes les personnes dont le contrat est ultérieur à une date particulière. + +Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de proposer immédiatement les champs, et donc d'informer le consommateur des possibilités: + +image::images/rest/drf-filters-and-searches.png[] + + +### Recherches + +La fonction de recherche est déjà implémentée au niveau de Django-Rest-Framework, et aucune dépendance supplémentaire n'est nécessaire. +Au niveau du `viewset`, il suffit d'ajouter deux informations: + +```python +... +from rest_framework import filters, viewsets +... + +class PeopleViewSet(viewsets.ModelViewSet): + ... + filter_backends = [filters.SearchFilter] + search_fields = ["last_name", "first_name"] + ... +``` + +### Filtres + +Nous commençons par installer [le paquet `django-filter`](https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend) et nous l'ajoutons parmi les applications installées: + +```bash +λ pip install django-filter +Collecting django-filter + Downloading django_filter-2.4.0-py3-none-any.whl (73 kB) + |████████████████████████████████| 73 kB 2.6 MB/s +Requirement already satisfied: Django>=2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from django-filter) (3.1.7) +Requirement already satisfied: asgiref<4,>=3.2.10 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (3.3.1) +Requirement already satisfied: sqlparse>=0.2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (0.4.1) +Requirement already satisfied: pytz in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (2021.1) +Installing collected packages: django-filter +Successfully installed django-filter-2.4.0 +``` + +Une fois l'installée réalisée, il reste deux choses à faire: + +1. Ajouter `django_filters` parmi les applications installées: +2. Configurer la clé `DEFAULT_FILTER_BACKENDS` à la valeur `['django_filters.rest_framework.DjangoFilterBackend']`. + +Vous avez suivi les étapes ci-dessus, il suffit d'adapter le fichier `settings.py` de la manière suivante: + +```python +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] +} +``` + +Au niveau du viewset, il convient d'ajouter ceci: + +```python +... +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import viewsets +... + +class PeopleViewSet(viewsets.ModelViewSet): + ... + filter_backends = [DjangoFilterBackend] + filterset_fields = ('last_name',) + ... +``` + +A ce stade, nous avons deux problèmes: + +1. Le champ que nous avons défini au niveau de la propriété `filterset_fields` exige une correspondance exacte. Ainsi, `/api/v1/people/?last_name=Bon` ne retourne rien, alors que `/api/v1/people/?last_name=Bond` nous donnera notre agent secret préféré. +2. Il n'est pas possible d'aller appliquer un critère de sélection sur la propriété d'une relation. Notre exemple proposant rechercher uniquement les relations dans le futur (ou dans le passé) tombe à l'eau. + +Pour ces deux points, nous allons définir un nouveau filtre, en surchargeant une nouvelle classe dont la classe mère serait de type `django_filters.FilterSet`. + +TO BE CONTINUED. + +A noter qu'il existe un paquet [Django-Rest-Framework-filters](https://github.com/philipn/django-rest-framework-filters), mais il est déprécié depuis Django 3.0, puisqu'il se base sur `django.utils.six` qui n'existe à présent plus. Il faut donc le faire à la main (ou patcher le paquet...). + diff --git a/source/part-4-services-oriented-applications/tests.adoc b/source/part-4-services-oriented-applications/trees.adoc similarity index 100% rename from source/part-4-services-oriented-applications/tests.adoc rename to source/part-4-services-oriented-applications/trees.adoc diff --git a/source/references.bib b/source/references.bib index 46a7363..7738144 100644 --- a/source/references.bib +++ b/source/references.bib @@ -58,6 +58,22 @@ year = {2015}, publisher = {Packt Publishing} } +@book{unix_philosophy, + author = {Eric S. Raymond}, + year = {2004}, + title = {Basics of the Unix Philosophy, The Art of Unix Programming}, + publisher = {Addison-Wesley Professional}, + isbn = {0-13-142901-9}, +} +@book{data_intensive, + title = {Designing Data Intensive Applications}, + booktitle = {The Big Ideas Behind Reliable, Scalable and Maintainable Systems}, + year = {2017}, + author = {Martin Kleppmann}, + publisher = {O'Reilly}, + isbn = {978-1-449-37332-0}, + release = {Fifteenth release - 2021-03-26} +} @misc{agiliq_admin, title = {Django Admin Cookbook, How to do things with Django admin}, year = {2018}, @@ -74,4 +90,9 @@ year = {2017}, note = {Last visited in 2021}, url = {https://simpleisbetterthancomplex.com/series/beginners-guide/1.11/} +} +@misc{gnu_linux_mag_hs_104, + title = {Les cinq règles pour écrire du code maintenable} + year = {2019}, + url = {https://boutique.ed-diamond.com/les-hors-series/1402-gnulinux-magazine-hs-104.html} } \ No newline at end of file