Update SOA, ideas, chapters, ...
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Fred Pauchet 2022-03-10 18:36:01 +01:00
parent acc6831726
commit 3370f5b20c
44 changed files with 598 additions and 272 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

5
quotes.adoc Normal file
View File

@ -0,0 +1,5 @@
[quote, Robert C. Martin, Clean Architecture]
A computer program is a detailed description of the policy by which inputs are transformed into outputs.
[quote,Ferdinand A. Porsche,Design Lead 911]
Design is not simply art, it is elegance of function.

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-02-21T17:54:02.057Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.5.1 Chrome/96.0.4664.110 Electron/16.0.7 Safari/537.36" etag="-JaFVMhHEClFmUan4B2S" version="16.5.1" type="device"><diagram id="60iCHSiT1cAfRgwXAf6L" name="Page-1">7VfRbtowFP0aHjcRQkt5LIF1mqg0jWmT9lKZ5JK4OL6RcwOkX7/rxCEEBmpVob1UQsg+vo59zzk5gp4fpLsHI7LkESNQvUE/2vX8aW8wGA7H/G2BsgZGw34NxEZGNXQALOQL1KDXoIWMIHdYDRGiIpl1wRC1hpA6mDAGt92yFaqoA2Qihs41LLAIhYKTst8yoqRG7wajFv8KMk6ak71b1/BShOvYYKHdeRo11CupaB7jjswTEeH2APJnPT8wiFSP0l0AyrLaZezLmdX9lQ1oes2Gkmb3P9fPhd58W/95KaaL6OHXp0aAjVCF48LdlsqGHIiYKzdFQwnGqIWateikIgDsOX2etTVzxIxBj8FnICqd8KIgZCihVLnVfA0UJm5SX8CeerZTB+VYmBAutee8JEwMdKHubq8HOxwwBTIl7zOgBMlN9x7CeS3e17Wk88Dx/hYNTiSYIK5PZMi3MlVCW75XqKlRxDImlIw1j0MmCAwDGzAk2d73biGVUVQpFSZSRXNRYmHpyInt28wmCRr5wk8We1WYN3KaDcadioXd6fQ2kHPN90Ye7wh6FLtO4Vzk5IAQlRJZLpdVJxZJWSmpJ0iEqYPO2aRrOsch9w27y7Y5ldltaF5Ul2EcYvV82yaC1+RacpAG4/6VjOH/492c1Jv4rv69HXJrg1tl5VsaHsVUcWGrSBLT2hTmZM4VhoIgRlM+HT/3yIDMLB3pQQbXEKBCNt20yj42p1TqCGr8qWBFl9yZZyKUOp5XZdNhi/xwbFsIeftKVUma8EbQlRdIkGhtlCE3UMlxM+EPCxT0P9/0briNgOdeO+ePLTcUoOZ2hKy8AuzRLVifvttz51/5UyM2zhu/zni310qkuxPjBc4jH6n0n1PJG/VfZ47RtVJp/K5U0iI9CqWPlLlWypz5VfPGlBm+PWV42v6urdYO/jb4s78=</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-02-21T17:57:51.400Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.5.1 Chrome/96.0.4664.110 Electron/16.0.7 Safari/537.36" etag="stiXMf6NY2XbX0NICuCm" version="16.5.1" type="device"><diagram id="60iCHSiT1cAfRgwXAf6L" name="Page-1">7VjbjtowEP0aHluRBFh4XALdqmKlqlS9vVQmMYkXJ4OcCZD9+k4SmxCyULYsqA9ICNnH48ucOTM2tBw32jwotgwfweeyZbf9TcsZtWy70xnQdw5kJXDXaZdAoIRfQjvAVDzzErQMmgqfJxorIQSQKJZ10IM45h7WMKYUrOtmc5B+DViygDeAqcdkE/0ufAxLtG/fVfhHLoLQ7Gz1tMMz5i0CBWms94sh5uVIxMwy2sckZD6sdyBn3HJcBYBlK9q4XOas1hn7cGB0e2TFYzxlQobj+6+LpzRefVr8ek5HU//h2zu9yorJVFMxBFjoA2Nm+EnWIpKMXHOGc4hxqkcs6jMpgpjaHh2DKwJWXKEgau/1QCR8P7ceeqGQ/oRlkObnTZCoM71hCEo808pM6mVpWKEWij2oWUzzmQS3CVU8IZvPhgRrD3pkm5rhhCWoAQ+kZMtEzApPciRiKhDxEBAh0lCI0fZAC45eaPbII859baU5JL/55mBwrG3IKYk4RBxVRiZmQqecofPH7mt9rSs1Wianwh0lGh0ynQDBduVKCdTQYniFMJyGMFr2sJxEOnPu8ya5ZvdkHr6ZolaABRe5FQokWo1hgqqhKKIK9whGBQvuggRS0ahIJFKbkHIPMoKTfI7H5JYsmSfiYFKYjToV8kXTl0NA0+eySMuQJvK4CC4yZJUulkCeFvx2h/ShKLjt991Wl9xwqW9Vffrk5gpdiMkdJorgcxLdmufCO1tEh3O4qSwjpROVZLA3V1K/oSSXIQ9AL30rM1csM3a7W6szltU/TR13l1LH4Kw6E7PoVmauVGb6b1NmrAsJyeq+oKQ9NXCfXn26S/SFEEDM5LhC68xVJhOApQafOGKmKwZLEerqKrfM9zmeoXQsSJXHjzlkHsJUL/ixwAxeDozikqFY1Q/y9rT3/gfaiVyV/cglThmiuz+14ovOaFPrZbVsuG64DlToK4Wr+eCf0YP/t3e7kv/lSt69N1q2M+973PMalwyNzPrdTvcyvxUsk3F/K72DS93hln3WJV4I8DRTI9OG+Suv/d2IHIrh7QHwYu04+wXQe31to271P0kxtvM3lDP+Aw==</diagram></mxfile>

View File

@ -2,7 +2,7 @@
Cédric Declerfayt <jaguarondi27@gmail.com>; Fred Pauchet <fred@grimbox.be>
:doctype: book
:toc:
:toclevels: 2
:toclevels: 1
:sectnums:
:chapter-label: Chapitre
:bibtex-file: source/references.bib
@ -25,11 +25,15 @@ It allows reusers to distribute, remix, adapt, and build upon the material in an
https://creativecommons.org/licenses/by-nc/4.0/?ref=chooser-v1
La seule exception concerne les morceaux de code (non attribués), disponibles sous licence https://mit-license.org/[MIT].
[preface]
== Préface
"The only way to go fast, is to go well"
-- Robert C. Martin
[quote,Robert C. Martin]
The only way to go fast, is to go well
Nous n'allons pas vous mentir: il existe enormément de tutoriaux très bien réalisés sur "_Comment réaliser une application Django_" et autres "_Déployer votre code en 2 minutes_".
Nous nous disions juste que ces tutoriaux restaient relativement haut-niveaux et se limitaient à un contexte donné, sans réellement préparer à la maintenance et au suivi de l'application nouvellement développée.
@ -75,6 +79,10 @@ Nous aborderons également la supervision et la mise à jour d'une application e
=== Pour qui ?
Avant tout, pour moi.
Comme le disait le Pr Richard Feynman: "_Si vous ne savez pas expliquer quelque chose simplement, c'est que vous ne l'avez pas compris_".
footnote:[Et comme l'ajoutait Aurélie Jean dans de L'autre côté de la machine: _"Si personne ne vous pose de questions suite à votre explication, c'est que vous n'avez pas été suffisamment clair !"_ cite:[other_side]]
Ce livre s'adresse autant au néophyte qui souhaite se lancer dans le développement Web qu'à l'artisan qui a besoin d'un aide-mémoire et qui ne se rappelle plus toujours du bon ordre des paramètres, ou à l'expert qui souhaiterait avoir un aperçu d'une autre technologie que son domaine privilégié de compétences.
Beaucoup de concepts présentés peuvent être oubliés ou restés inconnus jusqu'au moment où ils seront réellement nécessaires.
@ -106,8 +114,24 @@ CAUTION: Ces éléments indiquent des points d'attention. Les retenir vous fera
WARNING: Les avertissements indiquent un (potentiel) danger ou des éléments pouvant amener des conséquences pas spécialement sympathiques.
Les morceaux de code source seront présentés de la manière suivante:
[source,python,highlight=6]
----
# <folder>/<fichier>.<extension> <1>
def function(param):
""" <2>
"""
callback() <3>
----
<1> L'emplacement du fichier ou du morceau de code présenté, sous forme de commentaire
<2> Des commentaires, si cela s'avère nécessaire
<3> Les parties importantes ou récemment modifiées seront surlignées.
TIP: La plupart des commandes qui seront présentées dans ce livre le seront depuis un shell sous GNU/Linux.
Certaines d'entre elles pourraient devoir être adaptées si vous utilisez un autre système d'exploitation (macOS) ou n'importe quelle autre grosse bouse commerciale.
=== Let's keep in touch
include::part-1-workspace/_main.adoc[]

View File

@ -13,13 +13,13 @@ En pratique, les points ci-dessus permettront de monter facilement un nouvel env
Pour reprendre de manière très brute les différentes idées derrière cette méthode, nous avons:
*#1 - Une base de code unique, suivie par un système de contrôle de versions*.
*#1 - Une base de code unique, suivie par un système de contrôle de versions*.
Chaque déploiement de l'application se basera sur cette source, afin de minimiser les différences que l'on pourrait trouver entre deux environnements d'un même projet. On utilisera un dépôt Git - Github, Gitlab, Gitea, ... Au choix.
image::images/diagrams/12-factors-1.png[align=center]
Comme l'explique Eran Messeri footnote:[The DevOps Handbook, Part V, Chapitre 20, Convert Local Discoveries into Global Improvements], ingénieur dans le groupe Google Developer Infrastructure, un des avantages d'utiliser un dépôt unique de sources, est qu'il permet un accès facile et rapide à la forme la plus à jour du code, sans aucun besoin de coordination.
Comme l'explique Eran Messeri, ingénieur dans le groupe Google Developer Infrastructure, un des avantages d'utiliser un dépôt unique de sources, est qu'il permet un accès facile et rapide à la forme la plus à jour du code, sans aucun besoin de coordination. footnote:[The DevOps Handbook, Part V, Chapitre 20, Convert Local Discoveries into Global Improvements]
Ce dépôt ne sert pas seulement au code source, mais également à d'autres artefacts et formes de connaissance:
* Standards de configuration (Chef recipes, Puppet manifests, ...)
@ -30,7 +30,7 @@ Ce dépôt ne sert pas seulement au code source, mais également à d'autres art
* Tutoriaux
*#2 - Déclarez explicitement les dépendances nécessaires au projet, et les isoler du reste du système lors de leur installation*
*#2 - Déclarez explicitement les dépendances nécessaires au projet, et les isoler du reste du système lors de leur installation*
Chaque installation ou configuration doit toujours être faite de la même manière, et doit pouvoir être répétée quel que soit l'environnement cible.
@ -39,9 +39,9 @@ Dans notre cas, cela pourra être fait au travers de https://pypi.org/project/pi
Mais dans tous les cas, chaque application doit disposer d'un environnement sain, qui lui est assigné, et vu le peu de ressources que cela coûte, il ne faut pas s'en priver.
Chaque dépendance pouvant être déclarée et épinglée dans un fichier, il suffira de créer un nouvel environment vierge, puis d'utiliser ce fichier comme paramètre pour installer les prérequis au bon fonctionnement de notre application et vérifier que cet environnement est bien reproductible.
Chaque dépendance pouvant être déclarée et épinglée dans un fichier, il suffira de créer un nouvel environment vierge, puis d'utiliser ce fichier comme paramètre pour installer les prérequis au bon fonctionnement de notre application et vérifier que cet environnement est bien reproductible.
WARNING: Il est important de bien "épingler" les versions liées aux dépendances de l'application. Cela peut éviter des effets de bord comme une nouvelle version d'une librairie dans laquelle un bug aurait pu avoir été introduit.footnote:[Au conditionnel du futur plus-que-parfait antérieur. Mais ça arrive. Et tout le temps au mauvais moment.]
WARNING: Il est important de bien "épingler" les versions liées aux dépendances de l'application. Cela peut éviter des effets de bord comme une nouvelle version d'une librairie dans laquelle un bug aurait pu avoir été introduit. Parce qu'il arrive que ce genre de problème apparaisse, et lorsque ce sera le cas, ce sera systématiquement au mauvais moment.
*#3 - Sauver la configuration directement au niveau de l'environnement*
@ -52,19 +52,26 @@ Nous voulons éviter d'avoir à recompiler/redéployer l'application parce que:
. la base de données a été déplacée
. ...
En pratique, toute information susceptible de modifier un lien applicatif doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable.
En allant un pas plus loin, cela permettra de paramétrer facilement un container, en modifiant une variable de configuration qui spécifierait la base de données sur laquelle l'application devra se connecter.
En pratique, toute information susceptible de modifier un lien vers une ressource annexe doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable.
En allant un pas plus loin, ceci de paramétrer facilement un environnement (par exemple, un container), simplement en modifiant une variable de configuration qui spécifierait la base de données sur laquelle l'application devra se connecter.
Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, ...) sera définie directement au niveau de l'hôte - à aucun moment, nous ne devons trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code.
Au moment de développer une nouvelle fonctionnalité, réfléchissez si l'un des composants utilisés risquerait de subir une modification: ce composant peut concerner une nouvelle chaîne de connexion, un point de terminaison nécessaire à télécharger des données officielles ou un chemin vers un répertoire partagé pour y déposer un fichier.
*#4 - Traiter les ressources externes comme des ressources attachées*
Nous parlons de bases de données, de services de mise en cache, d'API externes, ...
L'application doit être capable d'effectuer des changements au niveau de ces ressources sans que son code ne soit modifié. Nous parlons alors de *ressources attachées*, dont la présence est nécessaire au bon fonctionnement de l'application, mais pour lesquelles le *type* n'est pas obligatoirement défini.
Nous voulons par exemple "une base de données" et "une mémoire cache", et pas "une base MariaDB et une instance Memcached". De cette manière, les ressources peuvent être attachées et détachées d'un déploiement à la volée.
Nous voulons par exemple "une base de données" et "une mémoire cache", et pas "une base MariaDB et une instance Memcached".
De cette manière, les ressources peuvent être attachées et détachées d'un déploiement à la volée.
Si une base de données ne fonctionne pas correctement (problème matériel?), l'administrateur pourrait simplement restaurer un nouveau serveur à partir d'une précédente sauvegarde, et l'attacher à l'application sans que le code source ne soit modifié. une solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ...) directement via des variables d'environnement.
Si une base de données ne fonctionne pas correctement (problème matériel ?), l'administrateur pourrait simplement restaurer un nouveau serveur à partir d'une précédente sauvegarde, et l'attacher à l'application sans que le code source ne soit modifié. une solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ...) directement _via_ des variables d'environnement.
image::images/12factors/attached-resources.png[align=center]
Nous serons ravis de pouvoir simplement modifier une chaîne `sqlite:////tmp/my-tmp-sqlite.db'` en `psql://user:pass@127.0.0.1:8458/db` lorsque ce sera nécessaire, sans avoir à recompiler ou redéployer les modifications.
*#5 - Séparer proprement les phases de construction, de mise à disposition et d'exécution*
@ -72,41 +79,47 @@ Si une base de données ne fonctionne pas correctement (problème matériel?), l
. La *mise à disposition* (_release_) associe cet ensemble à une configuration prête à être exécutée,
. tandis que la phase d'*exécution* (_run_) démarre les processus nécessaires au bon fonctionnement de l'application.
image::images/12factors/release.png[align=center]
Parmi les solutions possibles, nous pourrions nous pourrions nous baser sur les _releases_ de Gitea, sur un serveur d'artefacts ou sur https://fr.wikipedia.org/wiki/Capistrano_(logiciel)[Capistrano].
*#6 - Les processus d'exécution ne doivent rien connaître ou conserver de l'état de l'application*
Toute information stockée en mémoire ou sur disque ne doit pas altérer le comportement futur de l'application, par exemple après un redémarrage non souhaité.
Pratiquement, si l'application devait rencontrer un problème, nous pourrions la redémarrer sur un autre serveur. Toute information qui aurait été stockée durant l'exécution de l'application sur le premier hôte serait donc perdue.
Pratiquement, si l'application devait rencontrer un problème, l'objectif est de pouvoir la redémarrer rapidement sur un autre serveur (par exemple suite à un problème matériel).
Toute information qui aurait été stockée durant l'exécution de l'application sur le premier hôte serait donc perdue.
Si une réinitialisation devait être nécessaire, l'application ne devra pas compter sur la présence d'une information au niveau du nouveau système.
La solution consiste donc à jouer sur les variables d'environnement (cf. #3) et sur les informations que l'on pourra trouver au niveau des ressources attachées (cf #4).
Il serait également difficile d'appliquer une mise à l'échelle de l'application si une donnée indispensable à son fonctionnement devait se trouver sur une seule machine où elle est exécutée.
Il serait également difficile d'appliquer une mise à l'échelle de l'application, en ajoutant un nouveau serveur d'exécution, si une donnée indispensable à son fonctionnement devait se trouver sur la seule machine où elle est actuellement exécutée.
*#7 - Autoriser la liaison d'un port de l'application à un port du système hôte*
Les applications 12-factors sont auto-contenues et peuvent fonctionner en autonomie totale.
L'idée est qu'elles puissent être joignables grâce à un mécanisme de ponts, où l'hôte effectue la redirection vers l'un des ports ouverts par l'application, typiquement, en HTTP ou via un autre protocole.
Les applications 12-factors sont auto-contenues et peuvent fonctionner en autonomie totale.
Elles doivent être joignables grâce à un mécanisme de ponts, où l'hôte qui s'occupe de l'exécution effectue lui-même la redirection vers l'un des ports ouverts par l'application, typiquement, en HTTP ou via un autre protocole.
image::images/diagrams/12-factors-7.png[align=center]
*#8 - Faites confiance aux processus systèmes pour l'exécution de l'application*
Comme décrit plus haut, l'application doit utiliser des processus _stateless_ (sans état).
Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des processus particuliers pour certaines tâches: requêtes HTTP _via_ des processus Web; _long-running_ jobs pour des processus asynchrones, ...
Comme décrit plus haut (cf. #6), l'application doit utiliser des processus _stateless_ (sans état).
Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des processus particuliers pour certaines tâches: requêtes HTTP _via_ des processus Web; _long-running_ jobs pour des processus asynchrones, ...
Si cela existe au niveau du système, ne vous fatiguez pas: utilisez le.
image::images/12factors/process-types.png[align=center]
*#9 - Améliorer la robustesse de l'application grâce à des arrêts élégants et à des démarrages rapides*
Par "arrêt élégant", nous voulons surtout éviter le `kill -9 <pid>` ou tout autre arrêt brutal d'un processus qui nécessiterait une intervention urgente du superviseur.
De cette manière, les requêtes en cours pourront se terminer au mieux, tandis que le démarrage rapide de nouveaux processus améliorera la balance d'un processus en cours d'extinction vers des processus tout frais.
De cette manière, les requêtes en cours peuvent se terminer au mieux, tandis que le démarrage rapide de nouveaux processus améliorera la balance d'un processus en cours d'extinction vers des processus tout frais.
L'intégration de ces mécanismes dès les premières étapes de développement limitera les perturbations et facilitera la prise en compte d'arrêts inopinés (problème matériel, redémarrage du système hôte, etc.).
*#10 - Conserver les différents environnements aussi similaires que possible, et limiter les divergences entre un environnement de développement et de production*
L'exemple donné est un développeur qui utilise macOS, NGinx et SQLite, tandis que l'environnement de production tourne sur une CentOS avec Apache2 et PostgreSQL.
L'idée derrière ce concept limite les divergences entre environnements, facilite les déploiements et limite la casse et la découverte de modules non compatibles dès les premières phases de développement.
L'exemple donné est un développeur qui utilise macOS, NGinx et SQLite, tandis que l'environnement de production tourne sur une CentOS avec Apache2 et PostgreSQL.
Faire en sorte que tous les environnements soient les plus similaires possibles limite les divergences entre environnements, facilite les déploiements et limite la casse et la découverte de modules non compatibles dès les premières phases de développement.
Pour vous donner un exemple tout bête, SQLite utilise un https://www.sqlite.org/datatype3.html[mécanisme de stockage dynamique], associée à la valeur plutôt qu'au schéma, _via_ un système d'affinités. Un autre moteur de base de données définira un schéma statique et rigide, où la valeur sera déterminée par son contenant.
Un champ `URLField` proposé par Django a une longeur maximale par défaut de https://docs.djangoproject.com/en/3.1/ref/forms/fields/#django.forms.URLField[200 caractères].
@ -116,22 +129,22 @@ Conserver des environements similaires limite ce genre de désagréments.
*#11 - Gérer les journeaux d'évènements comme des flux*
Une application ne doit jamais se soucier de l'endroit où ses évènements seront écrits, mais simplement de les envoyer sur la sortie `stdout`.
Une application ne doit jamais se soucier de l'endroit où ses évènements seront écrits, mais simplement de les envoyer sur la sortie `stdout`.
De cette manière, que nous soyons en développement sur le poste d'un développeur avec une sortie console ou sur une machine de production avec un envoi vers une instance https://www.graylog.org/[Greylog] ou https://sentry.io/welcome/[Sentry], le routage des journaux sera réalisé en fonction de sa nécessité et de sa criticité, et non pas parce que le développeur l'a spécifié en dur dans son code.
Cette phase est critique, dans la mesure où les journaux d'exécution sont la seule manière pour une application de communiquer son état vers l'extérieur: recevoir une erreur interne de serveur est une chose; pouvoir obtenir un minimum d'informations, voire un contexte de plantage complet en est une autre.
*#12 - Isoler les tâches administratives du reste de l'application*
Evitez qu'une migration ne puisse être démarrée depuis une URL de l'application, ou qu'un envoi massif de notifications ne soit accessible pour n'importe quel utilisateur: les tâches administratives ne doivent être accessibles qu'à un administrateur.
Evitez qu'une migration ne puisse être démarrée depuis une URL de l'application, ou qu'un envoi massif de notifications ne soit accessible pour n'importe quel utilisateur: les tâches administratives ne doivent être accessibles qu'à un administrateur.
Les applications 12facteurs favorisent les langages qui mettent un environnement REPL (pour _Read_, _Eval_, _Print_ et _Loop_) à disposition (au hasard: https://pythonprogramminglanguage.com/repl/[Python] ou https://kotlinlang.org/[Kotlin]), ce qui facilite les étapes de maintenance.
=== Design for operations through codified non-functional requirements
=== Concevoir pour l'opérationnel
Pour paraphraser une section du DevOps Handbook (Part V, Chapitre 20, Convert Local Discoveries into Global Improvements (page 293-294), une application devient nettement plus maintenable dès lors que l'équipe de développement suit de près les différentes étapes de sa conception, de la demande jusqu'à son aboutissement en production.
Au fur et à mesure que le code est délibérément construit pour être maintenable, nous gagnons en rapidité, en qualité et en fiabilité de déploiement et les tâches liées aux opérations en sont facilitées.
Ces prérequis sont les suivants:
Une application devient nettement plus maintenable dès lors que l'équipe de développement suit de près les différentes étapes de sa conception, de la demande jusqu'à son aboutissement en production. cite:[devops_handbook(293-294)]
Au fur et à mesure que le code est délibérément construit pour être maintenable, l'équipe gagne en rapidité, en qualité et en fiabilité de déploiement, ce qui facilite les tâches opérationnelles:
* Activation d'une télémétrie suffisante dans les applications et les environnements.
* Activation d'une télémétrie suffisante dans les applications et les environnements.
* Conservation précise des dépendances nécessaires
* Résilience des services et plantage élégant (i.e. *sans finir sur un SEGFAULT avec l'OS dans les choux et un écran bleu*)
* Compatibilité entre les différentes versions (n+1, ...)
@ -139,3 +152,5 @@ Ces prérequis sont les suivants:
* Activation de la recherche dans les logs
* Traces des requêtes provenant des utilisateurs, indépendamment des services utilisés
* Centralisation de la configuration (*via* ZooKeeper, par exemple)

View File

@ -19,23 +19,21 @@ Lapplication des principes présentés et agrégés ci-dessous permet surtout
sans aller jusquau « *You Ain't Gonna Need It* » (ou *YAGNI*), qui consiste à surcharger tout développement avec des fonctionnalités non demandées, juste « au cas ou ».
Pour paraphraser une partie de lintroduction du livre _Clean Architecture_ cite:[clean_architecture]:
[quote]
[quote,Robert C. Martin, Clean Architecture, cite:[clean_architecture]]
Getting software right is hard: it takes knowledge and skills that most young programmers dont take the time to develop.
It requires a level of discipline and dedication that most programmers never dreamed theyd need.
Mostly, it takes a passion for the craft and the desire to be a professional.
Le développement d'un logiciel nécessite une rigueur d'exécution et des connaissances précises dans des
domaines extrêmement variés.
Le développement d'un logiciel nécessite une rigueur d'exécution et des connaissances précises dans des domaines extrêmement variés.
Il nécessite également des intentions, des (bonnes) décisions et énormément d'attention.
Indépendamment de l'architecture que vous aurez choisie, des technologies que vous aurez patiemment évaluées et mises en place,
une architecture et une solution peuvent être cassées en un instant, en même temps que tout ce que vous aurez construit,
dès que vous en aurez détourné le regard.
Indépendamment de l'architecture que vous aurez choisie, des technologies que vous aurez patiemment évaluées et mises en place, une architecture et une solution peuvent être cassées en un instant, en même temps que tout ce que vous aurez construit, dès que vous en aurez détourné le regard.
Un des objectifs ici est de placer les barrières et les gardes-fous (ou plutôt, les "*garde-vous*"), afin de péréniser au maximum les acquis, stabiliser les bases de tous les environnements (du développement à la production) qui pourraient accueillir notre application et fiabiliser les étapes de communication.
Un des objectifs ici est de placer les barrières et les gardes-fous (ou plutôt, les "*garde-vous*"), afin de péréniser au maximum les acquis, stabiliser les bases de tous les environnements (du développement à la production) qui accueilliront notre application et fiabiliser ainsi chaque étape de communication.
Dans cette partie, nous allons parler de *méthodes de travail*, avec comme objectif d'éviter que l'application ne tourne que sur notre machine et que chaque déploiement ne soit une plaie à gérer.
Dans cette partie-ci, nous parlerons de *méthodes de travail*, avec comme objectif d'éviter que l'application ne tourne que sur notre machine et que chaque déploiement ne soit une plaie à gérer.
Chaque mise à jour doit être réalisable de la manière la plus simple possible, et chaque étape doit être rendue la plus automatisée/automatisable possible.
Dans son plus simple élément, une application pourrait être mise à jour simplement en envoyant son code sur un dépôt centralisé: ce déclencheur doit démarrer une chaîne de vérification d'utilisabilité/fonctionnalités/débuggabilité/sécurité, pour immédiatement la mettre à disposition de nouveaux utilisateurs si toute la chaîne indique que tout est OK.
D'autres mécanismes fonctionnent également, mais au plus les actions nécessitent d'actions humaines, voire d'intervenants humains, au plus la probabilité qu'un problème survienne est grande.
Dans une version plus manuelle, cela pourrait se résumer à ces trois étapes (la dernière étant formellement facultative):
@ -43,12 +41,27 @@ Dans une version plus manuelle, cela pourrait se résumer à ces trois étapes (
. Prévoir un rollback si cela plante (et si cela a planté, préparer un post-mortem de l'incident pour qu'il ne se produise plus)
. Se préparer une tisane en regardant nos flux RSS (pour peu que cette technologie existe encore...).
NOTE: La plupart des commandes qui seront présentées dans ce livre le seront depuis un shell sous GNU/Linux.
Certaines d'entre elles pourraient devoir être adaptées si vous utilisez un autre système d'exploitation (macOS)
ou n'importe quelle autre grosse bouse commerciale.
include::clean_code.adoc[]
include::maintainable-applications/_index.adoc[]
include::mccabe.adoc[]
include::environment/_index.adoc[]
== Fiabilité, évolutivité et maintenabilité
include::django/_index.adoc[]
include::12-factors.adoc[]
include::solid.adoc[]
== Architecture
include::clean_architecture.adoc[]
== Tests et intégration
[quote, Robert C. Martin, Clean Architecture, page 203, Inner circle are policies, page 250, Chapitre 28 - The Boundaries]
Tests are part of the system.
You can think of tests as the outermost circle in the architecture.
Nothing within in the system depends on the tests, and the tests always depend inward on the components of the system ».
include::python.adoc[]
include::start-new-django-project.adoc[]

View File

@ -2,19 +2,9 @@
If you think good architecture is expensive, try bad architecture
[quote, Robert C. Martin, Clean Architecture]
A computer program is a detailed description of the policy by which inputs are transformed into outputs.
Au delà des principes dont il est question plus haut, cest dans les ressources proposées et les cas démontrés que lon comprend leur intérêt: plus que de la définition dune architecture adéquate, cest surtout dans la facilité de maintenance dune application que ces principes sidentifient.
Au delà des principes SOLID dont il est question plus haut,
cest à nouveau dans les ressources proposées et les cas démontrés que lon comprend leur intérêt:
plus que de la définition dune architecture adéquate, cest surtout dans la facilité de maintenance
dune application que ces principes sidentifient.
Derrière une bonne architecture, il y a aussi un investissement quant aux ressources qui seront nécessaires
à faire évoluer lapplication: ne pas investir dès quon le peut va juste lentement remplir la case de la dette technique.
Une bonne architecture va également rendre le système facile à lire, facile à développer, facile à maintenir et facile à déployer.
Une bonne architecture va rendre le système facile à lire, facile à développer, facile à maintenir et facile à déployer.
L'objectif ultime étant de minimiser le coût de maintenance et de maximiser la productivité des développeurs.
Un des autres objectifs d'une bonne architecture consiste également à se garder le plus doptions possibles,
et à se concentrer sur les détails (le type de base de données, la conception concrète, ...),
@ -22,14 +12,16 @@ le plus tard possible, tout en conservant la politique principale en ligne de mi
Cela permet de délayer les choix techniques à « plus tard », ce qui permet également de concrétiser ces choix
en ayant le plus dinformations possibles cite:[clean_architecture(137-141)]
Derrière une bonne architecture, il y a aussi un investissement quant aux ressources qui seront nécessaires à faire évoluer lapplication: ne pas investir dès quon le peut va juste lentement remplir la case de la dette technique.
Une architecture ouverte et pouvant être étendue na dintérêt que si le développement est suivi et que les gestionnaires (et architectes) sengagent à économiser du temps et de la qualité lorsque des changements seront demandés pour lévolution du projet.
==== Politiques et règles métiers
=== Politiques et règles métiers
TODO: Un p'tit bout à ajouter sur les méthodes de conception ;)
==== Considération sur les frameworks
=== Considération sur les frameworks
[quote, Robert C. Martin, Clean Architecture, p. 199]
Frameworks are tools to be used, not architectures to be conformed to.
@ -78,7 +70,7 @@ dimplémentation des frontières, dans la mesure où un service nest jamai
Une application monolotihique sera tout aussi fonctionnelle quune application découpée en microservices.
(Services: great and small, page 243).
==== Un point sur l'inversion de dépendances
=== Un point sur l'inversion de dépendances
Dans la partie SOLID, nous avons évoqué plusieurs principes de développement.
Django est un framework qui évolue, et qui a pu présenter certains problèmes liés à l'un de ces principes.

View File

@ -1,10 +1,72 @@
=== Maintenance
== Poésie de la programmation
=== Complexité de McCabe
La https://fr.wikipedia.org/wiki/Nombre_cyclomatique[complexité cyclomatique] (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section.
Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite.
Par exemple:
[source,python]
----
if True == False:
pass # never happens
# continue ...
----
TODO: faut vraiment reprendre un cas un peu plus lisible. Là, c'est naze.
La condition existe, mais nous ne passerons jamais dedans.
A l'inverse, le code suivant aura une complexité moisie à cause du nombre de conditions imbriquées:
[source,python]
----
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
----
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de cinq:
. le cas par défaut (a est différent de b, rien ne se passe),
. le cas où `a` est égal à `b`, mais où `b` est différent de `c`
. le cas où `a` est égal à `b`, `b` est égal à `c`, mais `c` est différent de `d`
. le cas où `a` est égal à `b`, `b` est égal à `c`, `c` est égal à `d`, mais `d` est différent de `e`
. le cas où `a` est égal à `b`, `b` est égal à `c`, `c` est égal à `d` et `d` est égal à `e`
La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1.
Si nous rencontrons une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, nous allons devoir vérifier au moins chacun des cas pour nous assurer que la couverture est complète.
Nous devrions donc trouver:
. Un test où rien de se passe (`a != b`)
. Un test pour entrer dans la condition `a == b`
. Un test pour entrer dans la condition `b == c`
. Un test pour entrer dans la condition `c == d`
. Un test pour entrer dans la condition `d == e`
Nous avons donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc.
Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil.
Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
Il est important de noter que refactoriser un bloc pour en extraire une méthode n'améliorera pas la complexité cyclomatique globale de l'application.
Nous visons ici une amélioration *locale*.
=== Conclusion
[quote, Robert C. Martin]
The primary cost of maintenance is in spelunking and risk cite:[clean_architecture(139)]
En ayant connaissance de toutes les choses qui pourraient être modifiées par la suite,
lidée est de pousser le développement jusquau point où un service pourrait être nécessaire.
En ayant connaissance de toutes les choses qui pourraient être modifiées par la suite, lidée est de pousser le développement jusquau point où un service pourrait être nécessaire.
A ce stade, larchitecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe.
Nous nallons donc pas jusquau point où le service doit être créé (même sil peut ne pas être nécessaire),
ni à lextrême au fait dignorer quun service pourrait être nécessaire, mais nous aboutissons à une forme de compromis.
@ -16,11 +78,11 @@ jusquau stade où une modification ne pourra plus faire reculer léchéanc
En terme de découpe, les composants peuvent lêtre aux niveaux suivants:
@ code source
@ déploiement, au travers de dll, jar, linked libraries, … voire au travers de threads ou de processus locaux.
@ services
* Code source
* Déploiement, au travers de dll, jar, linked libraries, … voire au travers de threads ou de processus locaux.
* Services
Cette section se base sur deux ressources principales cite:[maintainable_software], qui répartit un ensemble de conseils parmi quatre niveaux de composants:
Cette section se base sur deux ressources principales cite:[maintainable_software] cite:[clean_code], qui répartissent un ensemble de conseils parmi quatre niveaux de composants:
* Les méthodes et fonctions
* Les classes
@ -32,9 +94,9 @@ Ces conseils sont valables pour n'importe quel langage.
==== Au niveau des méthodes et fonctions
* *Gardez vos méthodes/fonctions courtes*. Pas plus de 15 lignes, en comptant les commentaires. Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9% de plus de 60 lignes; pas plus de 22.3% de plus de 30 lignes, au plus 43.7% de plus de 15 lignes et au moins 56.3% en dessous de 15 lignes). Oui, c'est dur à tenir, mais faisable.
* *Conserver une complexité de McCabe en dessous de 5*, c'est-à-dire avec quatre branches au maximum. A nouveau, si une méthode présente une complexité cyclomatique de 15, la séparer en 3 fonctions avec une complexité de 5 conservera globalement le nombre 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.
* *Conserver une complexité de McCabe en dessous de 5*, c'est-à-dire avec quatre branches au maximum. A nouveau, si une méthode présente une complexité cyclomatique de 15, la séparer en 3 fonctions ayant chacune une complexité de 5 conservera la complexité globale à 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.
* *N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc.*, c'est juste mal: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées. C'est aussi une forme de régression.
* *Conservez de petites interfaces*. Quatre paramètres, pas plus. Au besoin, refactorisez certains paramètres dans une classe, qui sera plus facile à tester.
* *Conservez de petites interfaces*. Quatre paramètres, pas plus. Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester.
==== Au niveau des classes

View File

@ -1,24 +0,0 @@
== Architecture
[quote, Robert C. Martin, Clean Architecture, Chapitre 15, What is architecture ?, page 137]
A software system that is hard to develop is not likely to have a long and healthy lifetime
include::clean_architecture.adoc[]
=== Evolutions
include::12-factors.adoc[]
=== Maintenabilité
include::maintainable-applications.adoc[]
include::mccabe.adoc[]
include::solid.adoc[]
=== Intégrées
[quote, Robert C. Martin, Clean Architecture, page 203, Inner circle are policies, page 250, Chapitre 28 - The Boundaries]
Tests are part of the system.
You can think of tests as the outermost circle in the architecture.
Nothing within in the system depends on the tests, and the tests always depend inward on the components of the system ».

View File

@ -1,58 +0,0 @@
=== Complexité de McCabe
La https://fr.wikipedia.org/wiki/Nombre_cyclomatique[complexité cyclomatique] (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section.
Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite.
Par exemple:
[source,python]
----
if True == False:
pass # never happens
# continue ...
----
TODO: faut vraiment reprendre un cas un peu plus lisible. Là, c'est naze.
La condition existe, mais nous ne passerons jamais dedans.
A l'inverse, le code suivant aura une complexité moisie à cause du nombre de conditions imbriquées:
[source,python]
----
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
----
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de cinq:
. le cas par défaut (a est différent de b, rien ne se passe),
. le cas où `a` est égal à `b`, mais où `b` est différent de `c`
. le cas où `a` est égal à `b`, `b` est égal à `c`, mais `c` est différent de `d`
. le cas où `a` est égal à `b`, `b` est égal à `c`, `c` est égal à `d`, mais `d` est différent de `e`
. le cas où `a` est égal à `b`, `b` est égal à `c`, `c` est égal à `d` et `d` est égal à `e`
La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1.
Si nous rencontrons une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, nous allons devoir vérifier au moins chacun des cas pour nous assurer que la couverture est complète.
Nous devrions donc trouver:
. Un test où rien de se passe (`a != b`)
. Un test pour entrer dans la condition `a == b`
. Un test pour entrer dans la condition `b == c`
. Un test pour entrer dans la condition `c == d`
. Un test pour entrer dans la condition `d == e`
Nous avons donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc.
Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil.
Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
NOTE: A noter que refactoriser un bloc pour en extraire une méthode n'améliorera pas la complexité cyclomatique globale de l'application. Mais nous visons ici une amélioration *locale*.

View File

@ -1,18 +1,19 @@
=== 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
[quote]
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. cite:[gnu_linux_mag_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
. **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
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 :
@ -24,21 +25,27 @@ En plus de ces principes de développement, il faut ajouter des principes au niv
==== Single Responsibility Principle
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].
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.
Il est également possible d'étendre ce principe en fonction d'acteurs:
> A module should be responsible to one and only one actor.
cite:[clean_architecture]
[quote,Robert C. Martin]
A module should be responsible to one and only one actor. cite:[clean_architecture]
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 daccord quil sagit à 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 lacteur concerné.
Lidée sous-jacente est simplement didentifier dès que possible les différents acteurs, en vue de prévoir une modification qui pourrait être demandée par lun dentre 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.
Une manière de voir les choses consiste à différencier les acteurs ou les intervenants: imaginez disposer d'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 daccord quil sagit à 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 lacteur concerné et de la manière dont il souhaite disposer des données.
Dès que possible, identifiez les différents acteurs et demandeurs, en vue de prévoir les modifications qui pourraient être demandées par lun dentre 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.
Vous trouverez ci-dessous une classe `Document`, dont chaque instance est représentée par trois propriétés: son titre, son contenu et sa date de publication.
Une méthode `render` permet également de proposer (très grossièrement) un type de sortie et un format de contenu: `XML` ou `Markdown`.
[source,python]
----
@ -68,8 +75,9 @@ class Document:
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:
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, ...), il sera nécessaire de modifier la classe `Document`, ce qui n'est ni intuitif (_ce n'est pas le document qui doit savoir dans quels formats il peut être envoyés_), ni conseillé (_lorsque nous aurons quinze formats différents à gérer, il sera nécessaire d'avoir autant de conditions dans cette méthode_).
Une bonne pratique consiste à créer une nouvelle classe de rendu pour chaque type de format à gérer:
[source,python]
----
@ -104,12 +112,12 @@ A présent, lorsque nous devrons ajouter un nouveau format de prise en charge, n
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 action doit être prise en compte par une autre classe (ou un autre concept), qui s'occupera de définir elle-même l'emplacement où l'évènement sera enregistré, que ce soit dans une base de données, une instance Graylog ou 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).
Lorsque nous verrons les composants, le principe de responsabilité unique deviendra le CCP - Common Closure Principle.
Ensuite, lorsque nous verrons l'architecture de l'application, ce sera la définition des frontières (boundaries).
==== Open Closed
@ -187,7 +195,7 @@ En anglais, dans le texte : "_Putting in simple words, the “Customer” class
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
Nous pouvons également appliquer ceci à notre exemple sur les rendus de document, où le code suivant:
[source,python]
----
@ -241,7 +249,7 @@ class MarkdownRenderer(Renderer):
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].
Ce point sera très utile lorsque nous aborderons les https://docs.djangoproject.com/en/stable/topics/db/models/#proxy-models[modèles proxy].
==== Liskov Substitution

View File

@ -358,6 +358,13 @@ Entre deux versions d'une même librairie, des fonctions sont cassées, certaine
Avec les mécanismes d'intégration continue et de tests unitaires, nous verrons plus loin comment se prémunir d'un changement inattendu.
=== Gestion des différentes versions des Python
[source,shell]
----
pyenv install 3.10
----
=== Django
Comme nous l'avons vu ci-dessus, `django-admin` permet de créer un nouveau projet.

View File

@ -115,31 +115,7 @@ Il est possible de démarrer petit, et de suivre l'évolution des besoins en fon
include::logging.adoc[]
=== Sentry ! :-D
[source,python]
----
SENTRY_DSN = env("SENTRY_DSN", default=None)
if SENTRY_DSN is not None:
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[DjangoIntegration()],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
# If you wish to associate users to errors (assuming you are using
# django.contrib.auth) you may enable sending PII data.
send_default_pii=True,
ca_certs=<path_to_pem_file>,
)
----
=== Logging

View File

View File

@ -0,0 +1,3 @@
== Kubernetes
Voir ici https://www.youtube.com/watch?v=NAOsLaB6Lfc ( <!> La vidéo dure 5h... >_<)

View File

@ -1,6 +1,6 @@
= Principes fondamentaux
Dans ce chapitre, nous allons parler de plusieurs concepts fondamentaux au développement rapide d'une application.
Dans ce chapitre, nous allons parler de plusieurs concepts fondamentaux au développement rapide d'une application utilisant Django.
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 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.
@ -8,19 +8,20 @@ Pour un néophyte, la courbe d'apprentissage sera relativement ardue: à côté
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**.
Et comme nous le verrons par la suite, la **vue** est en fait le **template**.
La principale différence avec un modèle MVC concerne le fait que la vue ne s'occupe pas du routage des URLs; ce point est réalisé par un autre composant, interne au framework, graĉe aux différentes routes définies dans les fichiers `urls.py`.
* Le modèle (`models.py`) fait le lien avec la base de données et permet de définir les champs et leur type à associer à une table. _Grosso modo_*, une table SQL correspondra à une classe d'un modèle Django.
* La vue (`views.py`), qui joue le rôle de contrôleur: _a priori_, tous les traitements, la récupération des données, etc. doit passer par ce composant et ne doit (pratiquement) pas être généré à la volée, directement à l'affichage d'une page. En d'autres mots, la vue sert de pont entre les données gérées par la base et l'interface utilisateur.
* Le template, qui s'occupe de la mise en forme: c'est le composant qui va s'occuper de transformer les données en un affichage compréhensible (avec l'aide du navigateur) pour l'utilisateur.
* Le **modèle** (`models.py`) fait le lien avec la base de données et permet de définir les champs et leur type à associer à une table. _Grosso modo_*, une table SQL correspondra à une classe d'un modèle Django.
* La **vue** (`views.py`), qui joue le rôle de contrôleur: _a priori_, tous les traitements, la récupération des données, etc. doit passer par ce composant et ne doit (pratiquement) pas être généré à la volée, directement à l'affichage d'une page. En d'autres mots, la vue sert de pont entre les données gérées par la base et l'interface utilisateur.
* Le **template**, qui s'occupe de la mise en forme: c'est le composant qui s'occupe de transformer les données en un affichage compréhensible (avec l'aide du navigateur) pour l'utilisateur.
Pour reprendre une partie du schéma précédent, lorsqu'une requête est émise par un utilisateur, la première étape va consister à trouver une _route_ qui correspond à cette requête, c'est à dire à trouver la correspondance entre l'URL qui est demandée par l'utilisateur et la fonction du langage qui sera exécutée pour fournir le résultat attendu.
Cette fonction correspond au *contrôleur* et s'occupera de construire le *modèle* correspondant.
En simplifiant, Django suit bien le modèle MVC, et toutes ces étapes sont liées ensemble grâce aux différentes routes, définies dans les fichiers `urls.py`.
include::models.adoc[]
include::querysets.adoc[]
include::migrations.adoc[]
include::shell.adoc[]
@ -33,6 +34,8 @@ include::auth.adoc[]
include::settings.adoc[]
include::context_processors.adoc[]
include::tests.adoc[]
== Conclusions

View File

@ -0,0 +1,44 @@
== _Context Processors_
Mise en pratique: un _context processor_ sert _grosso-modo_ à peupler l'ensemble des données transmises des vues aux templates avec des données communes.
Un context processor est un peu l'équivalent d'un middleware, mais entre les données et les templates, là où le middleware va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
[source,python]
----
# core/context_processors.py
import subprocess
def git_describe(request) -> str:
return {
"git_describe": subprocess.check_output(
["git", "describe", "--always"]
).strip(),
"git_date": subprocess.check_output(
["git", "show", "-s", r"--format=%cd", r"--date=format:%d-%m-%Y"]
),
}
----
Ceci aura pour effet d'ajouter les deux variables `git_describe` et `git_date` dans tous les contextes de tous les templates de l'application.
[source,python,highlight=12]
----
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "templates"),],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"core.context_processors.git_describe"
],
},
},
]
----

View File

@ -1,4 +1,8 @@
== Forms
== Forms
[quote]
Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et que l'ensemble reste cohérent.
Il (le form) n'a pas à savoir que tu as implémenté des closure tables dans un graph dirigé acyclique.
Ou comment valider proprement des données entrantes.
@ -6,7 +10,7 @@ image::images/xkcd-327.png[]
Quand on parle de `forms`, on ne parle pas uniquement de formulaires Web. On pourrait considérer qu'il s'agit de leur objectif principal, mais on peut également voir un peu plus loin: on peut en fait voir les `forms` comme le point d'entrée pour chaque donnée arrivant dans notre application: il s'agit en quelque sorte d'un ensemble de règles complémentaires à celles déjà présentes au niveau du modèle.
L'exemple le plus simple est un fichier `.csv`: la lecture de ce fichier pourrait se faire de manière très simple, en récupérant les valeurs de chaque colonne et en l'introduisant dans une instance du modèle.
L'exemple le plus simple est un fichier `.csv`: la lecture de ce fichier pourrait se faire de manière très simple, en récupérant les valeurs de chaque colonne et en l'introduisant dans une instance du modèle.
Mauvaise idée. On peut proposer trois versions d'un même code, de la version simple (lecture du fichier csv et jonglage avec les indices de colonnes), puis une version plus sophistiquée (et plus lisible, à base de https://docs.python.org/3/library/csv.html#csv.DictReader[DictReader]), et la version +++ à base de form.
@ -95,7 +99,7 @@ On a d'un côté le {{ form.as_p }} ou {{ form.as_table }}, mais il y a beaucoup
Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. Cela permet de valider des données reçues en entrée et d'afficher (très) facilement des formulaires à compléter par l'utilisateur.
Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir, modifier les types de contrôleurs, les placer au pixel près, ... Tout ça demande énormément de temps. Et c'est là qu'intervient http://django-crispy-forms.readthedocs.io/en/latest/[Django-Crispy-Forms]. Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le *layout* et la présentation.
Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir, modifier les types de contrôleurs, les placer au pixel près, ... Tout ça demande énormément de temps. Et c'est là qu'intervient http://django-crispy-forms.readthedocs.io/en/latest/[Django-Crispy-Forms]. Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le *layout* et la présentation.
(c/c depuis le lien ci-dessous)

View File

@ -9,38 +9,212 @@ Lors d'une première approche, elles peuvent sembler un peu magiques, puisqu'ell
Une analyse en profondeur montrera qu'elles ne sont pas plus complexes à suivre et à comprendre qu'un ensemble de fonctions de gestion appliquées à notre application.
Prenons l'exemple de notre liste de souhaits; nous nous rendons (bêtement) compte que nous avons oublié d'ajouter un champ de `description` à une liste.
Historiquement, cette action nécessitait l'intervention d'un administrateur système ou d'une personne ayant accès au schéma de la base de données, à partir duquel ce-dit utilisateur pouvait jouer manuellement un script SQL.
Historiquement, cette action nécessitait l'intervention d'un administrateur système ou d'une personne ayant accès au schéma de la base de données, à partir duquel ce-dit utilisateur pouvait jouer manuellement un script SQL. (((sql)))
Cet enchaînement d'étapes nécessitait une bonne coordination d'équipe, mais également une bonne confiance dans les scripts à exécuter.
Et souvenez-vous (cf. ref-à-insérer), que l'ensemble des actions doit être répétable et automatisable.
Bref, dans les années '80, il convenait de jouer ceci après s'être connecté à la base de données:
Bref, dans les années '80, il convenait de jouer ceci après s'être connecté au serveur de base de données:
[source,sql]
----
ALTER TABLE WishList ADD COLUMN Description nvarchar(MAX);
----
Et là, nous nous rappelons qu'un utilisateur tourne sur Oracle et pas sur MySQL, et qu'il a donc besoin de son propre script d'exécution, parce que le type du nouveau champ n'est pas exactement le même entre les deux moteurs:
Et là, nous nous rappelons qu'un utilisateur tourne sur Oracle et pas sur MySQL, et qu'il a donc besoin de son propre script d'exécution, parce que le type du nouveau champ n'est pas exactement le même entre les deux moteurs différents:
[source,sql]
----
-- Firebird
ALTER TABLE Category ALTER COLUMN Name type varchar(2000)
-- MSSQL
ALTER TABLE Category ALTER Column Name varchar(2000)
-- Oracle
ALTER TABLE Category MODIFY Name varchar2(2000)
----
Bref, vous voyez le(s) problème(s):
En bref, les problèmes suivants apparaissent très rapidement:
1. Aucune autonomie
2. Aucune automatisation possible (à moins d'écrire un programme, qu'il faudra également maintenir et intégrer au niveau des tests)
3. Nécessiter de maintenir autant de scripts différents qu'il y a de moteurs de base de données supportés
4. Aucune possibilité de vérifier si le script a déjà été exécuté ou non (à moins de maintenir un programme supplémentaire, à nouveau)
5. ...
1. Aucune autonomie: il est nécessaire d'avoir les compétences d'une personne tierce pour avancer ou de disposer des droits administrateurs,
2. Aucune automatisation possible, à moins d'écrire un programme, qu'il faudra également maintenir et intégrer au niveau des tests
3. Nécessité de maintenir autant de scripts différents qu'il y a de moteurs de base de données supportés
4. Aucune possibilité de vérifier si le script a déjà été exécuté ou non, à moins, à nouveau, de maintenir un programme supplémentaire.
Les migrations résolvent la plupart de ces soucis: le framework embarque ses propres applications, dont les migrations, qui gèrent elles-mêmes l'arbre de dépendances entre les modifications devant être appliquées.
Une migration consiste donc à appliquer un ensemble de modifications (ou **opérations**), qui exercent un ensemble de transformations, pour que le schéma de base de données corresponde au modèle de l'application sous-jacente.
Les migrations (comprendre les "_migrations du schéma de base de données_") sont intimement liées à la représentation d'un contexte fonctionnel.
L'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre.
=== Fonctionement général
Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code.
Le moteur de migrations résout la plupart de ces soucis: le framework embarque ses propres applications, dont les migrations, qui gèrent elles-mêmes l'arbre de dépendances entre les modifications devant être appliquées.
Pour reprendre un des premiers exemples, nous avions créé un modèle contenant deux classes, qui correspondent chacun à une table dans un modèle relationnel:
[source,python]
----
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
----
Nous avions ensuite modifié la clé de liaison, pour permettre d'associer plusieurs catégories à un même livre, et inversément:
[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)
----
Chronologiquement, cela nous a donné une première migration consistant à créer le modèle initial, suivie d'une seconde migration après que nous ayons modifié le modèle pour autoriser des relations multiples.
migrations successives, à appliquer pour que la structure relationnelle corresponde aux attentes du modèle Django:
[source,python]
----
# library/migrations/0001_initial.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel( <1>
name="Category",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
],
),
migrations.CreateModel( <2>
name="Book",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"title",
models.CharField(max_length=255)),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="library.category",
),
),
],
),
]
----
<1> La migration crée un nouveau modèle intitulé "Category", possédant un champ `id` (auto-défini, puisque nous n'avons rien fait), ainsi qu'un champ `name` de type texte et d'une longue maximale de 255 caractères.
<2> Elle crée un deuxième modèle intitulé "Book", possédant trois champs: son identifiant auto-généré `id`, son titre `title` et sa relation vers une catégorie, au travers du champ `category`.
Un outil comme https://sqlitebrowser.org/[DB Browser For SQLite] nous donne la structure suivante:
image::images/db/migrations-0001-to-0002.png[]
La représentation au niveau de la base de données est la suivante:
image::images/db/link-book-category-fk.drawio.png[]
[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) <1>
----
<1> Vous noterez que l'attribut `on_delete` n'est plus nécessaire.
Après cette modification, la migration résultante à appliquer correspondra à ceci.
En SQL, un champ de type `ManyToMany` ne peut qu'être représenté par une table intermédiaire.
C'est qu'applique la migration en supprimant le champ liant initialement un livre à une catégorie et en ajoutant une nouvelle table de liaison.
[source,python]
----
# library/migrations/0002_remove_book_category_book_category.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField( <1>
model_name='book',
name='category',
),
migrations.AddField( <2>
model_name='book',
name='category',
field=models.ManyToManyField(to='library.Category'),
),
]
----
<1> La migration supprime l'ancienne clé étrangère...
<2> ... et ajoute une nouvelle table, permettant de lier nos catégories à nos livres.
image::images/db/migrations-0002-many-to-many.png[]
Nous obtenons à présent la représentation suivante en base de données:
image::images/db/link-book-category-m2m.drawio.png[]
=== Sous le capot
Une migration consiste à appliquer un ensemble de modifications (ou **opérations**), qui exercent un ensemble de transformations, pour que le schéma de base de données corresponde au modèle de l'application sous-jacente.
Les migrations (comprendre les "_migrations du schéma de base de données_") sont intimement liées à la représentation d'un contexte fonctionnel: l'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre.
Il est primordial que la structure de la base de données corresponde à ce à quoi l'application s'attend, sans quoi la probabilité que l'utilisateur tombe sur une erreur de type `django.db.utils.OperationalError` est (très) grande.
Typiquement, après avoir ajouté un nouveau champ `summary` à chacun de nos livres, et sans avoir appliqué de migrations, nous tombons sur ceci:
[source,shell,highlight=10]
----
>>> from library.models import Book
>>> Book.objects.all()
Traceback (most recent call last):
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/sqlite3/base.py", line 416, in execute
return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such column: library_book.summary
----
Pour éviter ce type d'erreurs, plusieurs stratégies peuvent être appliquées:
intégrer ici un point sur les updates db - voir designing data-intensive applications.
Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code, et doivent faire partie du dépôt central de sources.
Le développeur s'occupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, _squashées_, ... et feront partie intégrante du processus de mise à jour de l'application.
A noter que les migrations n'appliqueront de modifications que si le schéma est impacté.

View File

@ -1,17 +1,30 @@
== 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, sous forme d'une base de données relationnelle, et la manière dont ces données s'agencent et communiquent entre elles.
Cette modélisation va ériger les premières pierres de votre édifice
[quote,Aurélie Jean,De l'autre côté de la machine,Une rencontre sans fin]
_Le modèle n'est qu'une grande hypothèque.
Il se base sur des choix conscients et inconscients, et dans chacun de ces choix se cachent nos propres perceptions qui résultent de qui nous sommes, de nos connaissances, de nos profils scientifiques et de tant d'autres choses._
Comme expliqué par Aurélie Jean cite:[other_side], "_toute modélisation reste une approximation de la réalité_".
Plus tard dans ce chapitre, nous expliquerons les bonnes pratiques à suivre pour faire évoluer ces biais.
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 à:
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,
1. Des migrations de données et la possibilité de faire évoluer votre modèle,
2. Une abstraction entre votre modélisation et la manière dont les données sont représentées _via_ un moteur de base de données relationnelles,
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).
@ -21,7 +34,10 @@ Déployer une nouvelle instance de l'application pourra être réalisé directem
=== Active Records
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.
Il est important de noter que l'implémentation d'Active Records reste une forme hybride entre une structure de données brutes et une classe:
* 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:
@ -104,7 +120,8 @@ class Circle(Shape):
return PI * self.__radius**2
----
On le voit: une structure brute peut être rendue abstraite au travers des notions de programmation orientée objet.
Une structure de données 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 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.
@ -119,10 +136,7 @@ Nous pouvons regarder les propriétés définies dans cette classe en analysant
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 sait **comment** interagir avec la base de données.
=== Types de champs
=== Relations et clés étrangères
=== 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 sous-jacent avec le moteur de base de données relationnelle.
@ -145,12 +159,25 @@ class Book(models.Model):
----
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.
Les autres champs nous permettent d'identifier une catégorie (`Category`) par un nom (`name`), 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[]
En termes de code d'initialisation, cela revient écrire ceci:
A présent que notre structure dispose de sa modélisation, il nous faut informer le moteur de base de données de créer la structure correspondance:
[source,shell]
----
$ python manage.py makemigrations
Migrations for 'library':
library/migrations/0001_initial.py
- Create model Category
- Create model Book
----
Cette étape créera un fichier différentiel, explicitant les modifications à appliquer à la structure de données pour rester en corrélation avec la modélisation de notre application.
Nous pouvons écrire un premier code d'initialisation de la manière suivante:
[source,python]
----
@ -177,9 +204,14 @@ 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.
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_. Ces deux titres (au moins) peuvent appartenir à deux catégories distinctes.
* Pour _The Great Gatsby_, c'est l'inverse: nous l'avons initialement classé comme film, mais le livre existe depuis 1925.
* Nous pourrions sans doute également étoffer notre bibliothèque avec une catégorie spéciale "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga _Harry Potter_ et ses dérivés.
En clair, notre modèle n'est pas adapté, et nous devons le modifier pour qu'une occurrence puisse être liée à plusieurs catégories.
Au lieu d'utiliser un champ de type `ForeignKey`, nous utiliserons un champ de type `ManyToMany`, c'est-à-dire qu'à présent, un livre pourra être lié à plusieurs catégories, et qu'inversément, une même catégorie pourra être liée à plusieurs livres.
[source,python,highlight=6]
----

View File

@ -1,6 +1,6 @@
== Templates
Avant de commencer à interagir avec nos données au travers de listes, formulaires et d'interfaces sophistiquées, quelques mots sur les templates: il s'agit en fait de *squelettes* de présentation, recevant en entrée un dictionnaire contenant des clés-valeurs et ayant pour but de les afficher selon le format que vous définirez.
Avant de commencer à interagir avec nos données au travers de listes, formulaires et d'interfaces sophistiquées, quelques mots sur les templates: il s'agit en fait de *squelettes* de présentation, recevant en entrée un dictionnaire contenant des clés-valeurs et ayant pour but de les afficher selon le format que vous définirez.
En intégrant un ensemble de *tags*, cela vous permettra de greffer les données reçues en entrée dans un patron prédéfini.
@ -87,7 +87,7 @@ La page HTML pour nos listes de souhaits devient alors:
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
</ul>
----
<1> On étend/hérite de notre page `base.html`
<2> On redéfinit le titre (mais on réutilise le titre initial en appelant `block.super`)
@ -107,7 +107,7 @@ templates/
└── list.html
----
Par défaut, Django cherchera les templates dans les répertoirer d'installation.
Par défaut, Django cherchera les templates dans les répertoirer d'installation.
Vous devrez vous éditer le fichier `gwift/settings.py` et ajouter, dans la variable `TEMPLATES`, la clé `DIRS` de la manière suivante:
[source,python]
@ -121,7 +121,7 @@ TEMPLATES = [
]
----
==== Fichiers statiques
==== Fichiers statiques
(à compléter)
@ -154,21 +154,26 @@ class Suggestion(models.Model):
Voir https://docs.djangoproject.com/fr/3.0/howto/custom-template-tags/
"""
return urlize(self.subject, autoescape=True)
----
==== Regroup By
(le truc super facile qui sert à mort dans plein de cas sans qu'on n'ait à se casser la tête).
=== Non-builtins
En plus des quelques tags survolés ci-dessus, il est également possible de construire ses propres tags. La structure est un peu bizarre, car elle consiste à ajouter un paquet dans une de vos applications, à y définir un nouveau module et à y définir un ensemble de fonctions. Chacune de ces fonctions correspondra à un tag appelable depuis vos templates.
Il existe trois types de tags *non-builtins*:
Il existe trois types de tags *non-builtins*:
1. *Les filtres* - on peut les appeler grâce au *pipe* `|` directement après une valeur dans le template.
1. *Les filtres* - on peut les appeler grâce au *pipe* `|` directement après une valeur dans le template.
2. *Les tags simples* - ils peuvent prendre une valeur ou plusieurs en paramètre et retourne une nouvelle valeur. Pour les appeler, c'est *via* les tags `{% nom_de_la_fonction param1 param2 ... %}`.
3. *Les tags d'inclusion*: ils retournent un contexte (ie. un dictionnaire), qui est ensuite passé à un nouveau template. Type `{% include '...' ... %}`.
Pour l'implémentation:
1. On prend l'application `wish` et on y ajoute un répertoire `templatetags`, ainsi qu'un fichier `__init__.py`.
1. On prend l'application `wish` et on y ajoute un répertoire `templatetags`, ainsi qu'un fichier `__init__.py`.
2. Dans ce nouveau paquet, on ajoute un nouveau module que l'on va appeler `tools.py`
3. Dans ce module, pour avoir un aperçu des possibilités, on va définir trois fonctions (une pour chaque type de tags possible).
@ -185,7 +190,7 @@ Pour plus d'informations, la https://docs.djangoproject.com/en/stable/howto/cust
[source,python]
----
# wish/tools.py
from django import template
from wish.models import Wishlist
@ -203,7 +208,7 @@ def add_xx(value):
[source,python]
----
# wish/tools.py
from django import template
from wish.models import Wishlist
@ -223,7 +228,7 @@ def current_time(format_string):
[source,python]
----
# wish/tools.py
from django import template
from wish.models import Wishlist
@ -234,19 +239,19 @@ register = template.Library()
@register.inclusion_tag('wish/templatetags/wishlists_list.html')
def wishlists_list():
return { 'list': Wishlist.objects.all() }
return { 'list': Wishlist.objects.all() }
----
=== Contexts Processors
Un `context processor` permet d'ajouter des informations par défaut à un contexte (le dictionnaire qu'on passe de la vue au template).
L'idée est d'ajouter une fonction à un module Python à notre projet, puis de le référencer parmi
les CONTEXT_PROCESSORS de nos paramètres généraux. Cette fonction doit peupler un dictionnaire, et les clés de ce dictionnaire seront
L'idée est d'ajouter une fonction à un module Python à notre projet, puis de le référencer parmi
les CONTEXT_PROCESSORS de nos paramètres généraux. Cette fonction doit peupler un dictionnaire, et les clés de ce dictionnaire seront
directement ajoutées à tout autre dictionnaire/contexte passé à une vue. Par exemple:
(cf. https://stackoverflow.com/questions/60515797/default-context-for-all-pages-django[StackOverflow] - à retravailler)
[source,python]
----
from product.models import SubCategory, Category
@ -258,7 +263,7 @@ def add_variable_to_context(request):
'categories': Category.objects.order_by("id").all(),
}
----
[source,python]
----
'OPTIONS': {

View File

@ -5,4 +5,6 @@
python manage.py makemessages
----
+ plein d'explications: traductions, génération, interface (urls, breadcrumbs, ...).
+ plein d'explications: traductions, génération, interface (urls, breadcrumbs, ...).
+ naive datetimes

View File

@ -5,15 +5,11 @@ L'objectif est de vous mettre la puce à l'oreille quant à la finalité du dév
Dans cette partie, nous aborderons les vues, la mise en forme, la mise en page, la définition d'une interface REST, la définition d'une interface GraphQL et le routage d'URLs.
include::views.adoc[]
include::templates.adoc[]
include::querysets.adoc[]
include::rest.adoc[]
include::urls.adoc[]
include::rest.adoc[]
include::localization.adoc[]
include::trees.adoc[]

View File

@ -0,0 +1,9 @@
== i18n / l10n
La localisation (_l10n_) et l'internationalization (_i18n_) sont deux concepts proches, mais différents:
* Internationalisation: _Preparing the software for localization. Usually done by developers._
* Localisation: _Writing the translations and local formats. Usually done by translators._
L'internationalisation est donc le processus permettant à une application d'accepter une forme de localisation.
La seconde ne va donc pas sans la première, tandis que la première ne fait qu'autoriser la seconde.

View File

@ -1,5 +1,12 @@
== Application Programming Interface
NOTE: https://news.ycombinator.com/item?id=30221016&utm_term=comment vs Django Rest Framework
NOTE: Expliquer pourquoi une API est intéressante/primordiale/la première chose à réaliser/le cadet de nos soucis.
NOTE: Voir peut-être aussi https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/
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:

View File

@ -0,0 +1,28 @@
== Monitoring et activités
[quote,Terry Pratchett,Les Anales du Disque-Monde]
Les magiciens du Disque-Monde ont, de leur côté, calculé que les chances uniques sur un million se produisent 9 fois sur 10.
[source,python]
----
SENTRY_DSN = env("SENTRY_DSN", default=None)
if SENTRY_DSN is not None:
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[DjangoIntegration()],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
# If you wish to associate users to errors (assuming you are using
# django.contrib.auth) you may enable sending PII data.
send_default_pii=True,
ca_certs=<path_to_pem_file>,
)
----

View File

@ -1,4 +0,0 @@
Sessions
========
[Ne pas oublier d'en parler...]

View File

@ -47,12 +47,11 @@ urlpatterns = [
]
----
NOTE: Dans la mesure du possible, essayez toujours de **nommer** chaque expression.
TIP: Dans la mesure du possible, essayez toujours de **nommer** chaque expression.
Cela permettra notamment de les retrouver au travers de la fonction `reverse`, mais permettra également de simplifier vos templates.
A présent, on doit tester que l'URL racine de notre application mène bien vers la fonction `wish_views.wishlists`.
Prenons par exemple l'exemple de Twitter : quand on accède à une URL, elle est de la forme `https://twitter.com/<user>`.
Sauf que les pages `about` et `help` existent également.
Pour implémenter ce type de précédence, il faudrait implémenter les URLs de la manière suivante:
@ -68,8 +67,7 @@ Une dernière solution serait de maintenir une liste d'authorité des noms d'uti
D'où l'importance de bien définir la séquence de déinition de ces routes, ainsi que des espaces de noms.
L'idée des espaces de noms ou _namespaces_ est de définir un _sous-répertoire_ dans lequel on trouvera nos nouvelles routes.
Cette manière de procéder permet notamment de répondre au problème ci-dessous, en définissant un sous-dossier type `https://twitter.com/users/<user>`.
Note sur les namespaces.
De là, découle une autre bonne pratique: l'utilisation de _breadcrumbs_ (https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template) ou de guidelines de navigation.

View File

@ -3,6 +3,8 @@
Pour commencer, nous allons nous concentrer sur la création d'un site ne contenant qu'une seule application, même si en pratique le site contiendra déjà plusieurs applications fournies pas django, comme nous le verrons plus loin.
NOTE: Don't make me think, or why I switched from JS SPAs to Ruby On Rails https://news.ycombinator.com/item?id=30206989&utm_term=comment
Pour prendre un exemple concret, nous allons créer un site permettant de gérer des listes de souhaits, que nous appellerons `gwift` (pour `GiFTs and WIshlisTs` :)).
La première chose à faire est de définir nos besoins du point de vue de l'utilisateur, c'est-à-dire ce que nous souhaitons qu'un utilisateur puisse faire avec l'application.
@ -14,4 +16,4 @@ include::gwift/_main.adoc[]
include::khana/_main.adoc[]
include::legacy/_index_.adoc[]
include::legacy/_main.adoc[]

View File

@ -1,5 +1 @@
= Ressources et bibliographie
include::code-snippets.adoc[]
include::legacy.adoc[]

View File

@ -7,6 +7,11 @@
year = {2018},
type = {Book}
}
@book{
other_side,
author = {Aurélie Jean},
title = {De l'autre côté de la machine}
}
@book{clean_code,
title = {Clean Code, a Handbook of Agile Software Craftmanship},
author = {Robert C. Martin},