Review Easter holidays notes (part 2)
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Fred Pauchet 2022-04-30 20:53:42 +02:00
parent e5012f7066
commit f577872744
16 changed files with 135 additions and 10202 deletions

3
chapters/kubernetes.tex Normal file
View File

@ -0,0 +1,3 @@
\chapter{Kubernetes}
Voir ici https://www.youtube.com/watch?v=NAOsLaB6Lfc ( La vidéo dure 5h... )

View File

@ -1,12 +1,11 @@
\chapter{Démarrer un nouveau projet}
Django fonctionne sur un
\href{https://docs.djangoproject.com/en/dev/internals/release-process/}{roulement de trois versions mineures pour une version majeure}, clôturé par une version LTS (\emph{Long Term Support}).
\section{Gestion des dépendances}
Comme nous en avons déjà discuté, PIP est la solution que nous avons choisie pour la gestion de nos dépendances.
Comme nous en avons déjà discuté, Poetry est la solution que nous avons choisie pour la gestion de nos dépendances.
Pour installer une nouvelle librairie, vous pouvez simplement passer par la commande \texttt{pip\ install\ \textless{}my\_awesome\_library\textgreater{}}.
Dans le cas de Django, et après avoir activé l'environnement, nous pouvons à présent y installer Django. Comme expliqué ci-dessus, la librairie restera indépendante du reste du système, et ne polluera aucun autre projet. nous exécuterons donc la commande suivante:
@ -79,9 +78,9 @@ L'utilité de ces fichiers est définie ci-dessous:
Indiquer qu'il est possible d'avoir plusieurs structures de dossiers et
qu'il n'y a pas de "magie" derrière toutes ces commandes.
La seule condition est que les chemins référencés soient cohérents par rapport à la structure sous-jacente.
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel
nous stockerons les dépendances et un fichier README:
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel nous stockerons les dépendances et un fichier README:
TODO
@ -132,12 +131,11 @@ Nous faisons ici une distinction entre un \textbf{projet} et une \textbf{applica
\begin{itemize}
\item
\textbf{Un projet} représente l'ensemble des applications, paramètres,
pages HTML, middlewares, dépendances, etc., qui font que votre code
fait ce qu'il est sensé faire.
\textbf{Un projet} représente l'ensemble des applications, paramètres, middlewares, dépendances, ..., qui font que votre code fait ce qu'il est sensé faire.
Il s'agit grosso modo d'un câblage de tous les composants entre eux.
\item
\textbf{Une application} est un contexte d'exécution, idéalement
autonome, d'une partie du projet.
\textbf{Une application} est un contexte d'exécution (vues, comportements, pages HTML, ...), idéalement autonome, d'une partie du projet.
Une application est supposée avoir une portée de réutilisation, même s'il ne sera pas toujours possible de viser une généricité parfaite.
\end{itemize}
Pour \texttt{gwift}, nous aurons:
@ -145,7 +143,7 @@ Pour \texttt{gwift}, nous aurons:
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-gwift.png}
\caption{Django Projet vs Applications}
\caption{Projet Django vs Applications}
\end{figure}
\begin{enumerate}
@ -171,11 +169,13 @@ Pour \texttt{khana}, nous pourrions avoir quelque chose comme ceci:
\end{figure}
En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la gestion des utilisateurs et la possibilité qu'ils auront de communiquer entre eux.
Ceci pourrait être commun aux deux applications.
Ceci pourrait être commun aux deux projets.
Nous pouvons clairement visualiser le principe de \textbf{contexte} pour une application: celle-ci viendra avec son modèle, ses tests, ses vues et son paramétrage et pourrait ainsi être réutilisée dans un autre projet.
C'est en ça que consistent les \href{https://www.djangopackages.com/}{paquets Django} déjà disponibles:
ce sont "\emph{simplement}" de petites applications empaquetées et pouvant être réutilisées dans différents contextes (eg. \href{https://github.com/tomchristie/django-rest-framework}{Django-Rest-Framework}, \href{https://github.com/django-debug-toolbar/django-debug-toolbar}{Django-Debug-Toolbar}, ...
Le projet s'occupe principalement d'appliquer une couche de glue entre différentes applications.
\subsection{manage.py}
Le fichier \texttt{manage.py} que vous trouvez à la racine de votre projet est un \textbf{wrapper} sur les commandes \texttt{django-admin}.
@ -204,13 +204,17 @@ catégories:
\item
\textbf{auth}: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.
\item
\textbf{django}: vérifier la \textbf{compliance} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, ...
\textbf{django}: vérifier la \textbf{conformité} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, ...
Ce sont des commandes d'administration générale.
\item
\textbf{sessions}: suppressions des sessions en cours
\item
\textbf{staticfiles}: gestion des fichiers statiques et lancement du serveur de développement.
\end{itemize}
Chaque section correspond à une application.
En analysant le code, ces applications peuvent être trouvées sous \texttt{django.contrib}.
Nous verrons plus tard comment ajouter de nouvelles commandes.
Si nous démarrons la commande \texttt{python\ manage.py\ runserver},
@ -240,15 +244,13 @@ Nous avons mis un morceau de la sortie console entre crochet \texttt{{[}\ldots{}
Si vous avez suivi les étapes jusqu'ici, vous avez également dû voir un message type \texttt{You\ have\ 18\ unapplied\ migration(s).\ {[}\ldots{}\hspace{0pt}{]}\ Run\ \textquotesingle{}python\ manage.py\ migrate\textquotesingle{}\ to\ apply\ them.}
Cela concerne les migrations, et c'est un point que nous verrons un peu plus tard.
\section{Nouvelle application}
\section{Minimal Viable Application}
Maintenant que nous avons a vu à quoi servait \texttt{manage.py}, nous pouvons créer notre nouvelle application grâce à la commande \texttt{manage.py\ startapp\ \textless{}label\textgreater{}}.
Notre première application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir.
Notre première application servira à structurer des listes de souhaits, les éléments qui les composent et les pourcentages de participation que chaque utilisateur aura souhaité offrir.
De manière générale, essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application. Pour nous, ce sera donc \texttt{wish}.
C'est parti pour \texttt{manage.py\ startapp\ wish}!
\begin{verbatim}
$ python manage.py startapp wish
\end{verbatim}
@ -257,73 +259,74 @@ Résultat? Django nous a créé un répertoire \texttt{wish}, dans lequel nous t
\begin{itemize}
\item
\texttt{wish/init.py} pour que notre répertoire \texttt{wish} soit converti en package Python.
\texttt{wish/\_\_init\_\_.py} pour que notre répertoire \texttt{wish} soit converti en package Python.
\item
\texttt{wish/admin.py} servira à structurer l'administration de notre application.
Chaque information peut être administrée facilement au travers d'une interface générée à la volée par le framework.
Chaque information peut être rée facilement au travers d'une interface générée à la volée par le framework.
Nous y reviendrons par la suite.
\item
\texttt{wish/apps.py} qui contient la configuration de l'application et qui permet notamment de fixer un nom ou un libellé \url{https://docs.djangoproject.com/en/stable/ref/applications/}
\item
\texttt{wish/migrations/} est le dossier dans lequel seront stockées toutes les différentes migrations de notre application (= toutes les modifications que nous apporterons aux données que nous souhaiterons manipuler)
\item
\texttt{wish/models.py} représentera et structurera nos données, et est intimement lié aux migrations.
\texttt{wish/models.py} représentera et structurera nos données. Ce modèle est intimement lié aux migrations.
\item
\texttt{wish/tests.py} pour les tests unitaires.
\end{itemize}
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire \texttt{wish} dans votre répertoire \texttt{gwift} existant.
C'est une forme de convention.
C'est \emph{une} forme de convention.
La structure de vos répertoires devient celle-ci:
TODO
Notre application a bien été créée, et nous l'avons déplacée dans le répertoire \texttt{gwift} !
Notre application a bien été créée, et nous l'avons déplacée dans le répertoire \texttt{gwift} ! \footnote{Il manque quelques fichiers utiles, qui seront décrits par la suite, pour qu'une application soit réellement autonome: templates, \texttt{urls.py}, managers, services, ...}
\section{Fonctionnement général}
Le métier de programmeur est devenu de plus en plus complexe.
Il y a 20 ans, nous pouvions nous contenter d'une simple page PHP dans laquelle nous mixions l'ensemble des actios à réaliser: requêtes en bases de données, construction de la page, ...
La recherche d'une solution a un problème n'était pas spécialement plus complexe - dans la mesure où le rendu des enregistrements en direct n'était finalement qu'une forme un chouia plus évoluée du \texttt{print()} ou des \texttt{System.out.println()} - mais c'était l'évolutivité des applications qui en prenait un coup: une grosse partie des tâches étaient dupliquées entre les différentes pages, et l'ajout d'une nouvelle fonctionnalité était relativement ardue.
Il y a 20 ans, nous pouvions nous contenter d'une simple page PHP dans laquelle nous mixions l'ensemble des actions à réaliser: requêtes en bases de données, construction de la page, ...
La recherche d'une solution à un problème n'était pas spécialement plus complexe - dans la mesure où le rendu des enregistrements en direct n'était finalement qu'une forme un chouia plus évoluée du \texttt{print()} ou des \texttt{System.out.println()} - mais c'était l'évolutivité des applications qui en prenait un coup: une grosse partie des tâches étaient dupliquées entre les différentes pages, et l'ajout d'une nouvelle fonctionnalité était relativement ardue.
Django (et d'autres cadriciels) résolvent ce problème en se basant ouvertement sur le principe de \texttt{Dont\ repeat\ yourself} \footnote{DRY}.
Django (et d'autres frameworks) résolvent ce problème en se basant ouvertement sur le principe de \texttt{Dont\ repeat\ yourself} \footnote{DRY}.
Chaque morceau de code ne doit apparaitre qu'une seule fois, afin de limiter au maximum la redite (et donc, l'application d'un même correctif à différents endroits).
Le chemin parcouru par une requête est expliqué en (petits) détails ci-dessous.
\textbf{Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application}.
Ici, nous prenons l'exemple de l'URL fictive \texttt{https://gwift/wishes/91827}.
Lorsque cette URL "arrive" dans notre application, son point d'entrée se trouvera au niveau des fichiers \texttt{asgi.py} ou \texttt{wsgi.py}.
Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu'elle va parcourir.
\begin{figure}
\centering
\includegraphics{images/diagrams/django-how-it-works.png}
\caption{How it works}
\end{figure}
\textbf{1. Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application}.
Ici, nous prenons l'exemple de l'URL fictive \texttt{https://gwift/wishes/91827}.
Lorsque cette URL "arrive" dans notre application, son point d'entrée se trouvera au niveau des fichiers \texttt{asgi.py} ou \texttt{wsgi.py}.
Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu'elle va parcourir.
\textbf{Etape 0} - La première étape consiste à vérifier que cette URL répond à un schéma que nous avons défini dans le fichier \texttt{gwift/urls.py}.
\begin{minted}{Python}
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls),
path("wishes/<int:wish_id>", wish_details),
]
\end{minted}
\textbf{Etape 1} - Si ce n'est pas le cas, l'application n'ira pas plus loin et retournera une erreur à l'utilisateur.
\textbf{Etape 2} - Django va parcourir l'ensemble des \emph{patterns} présents dans le fichier \texttt{urls.py} et s'arrêtera sur le premier qui correspondra à la requête qu'il a reçue.
Ce cas est relativement trivial: la requête \texttt{/wishes/91827} a une correspondance au
niveau de la ligne \texttt{path("wishes/\textless{}int:wish\_id\textgreater{}} dans l'exemple ci-dessous. Django va alors appeler la fonction \footnote{Qui ne sera pas toujours une fonction. Django s'attend à trouver un \emph{callable}, c'est-à-dire n'importe quel élément qu'il peut appeler comme une fonction.} associée à ce \emph{pattern}, c'est-à-dire \texttt{wish\_details} du module \texttt{gwift.views}.
\begin{minted}{Python}
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls),
path("wishes/<int:wish_id>", wish_details),
]
\end{minted}
\begin{itemize}
\item
Nous importons la fonction \texttt{wish\_details} du module
@ -332,44 +335,41 @@ niveau de la ligne \texttt{path("wishes/\textless{}int:wish\_id\textgreater{}} d
Champomy et cotillons! Nous avons une correspondance avec
\texttt{wishes/details/91827}
\end{itemize}
TODO: En fait, il faudrait quand même s'occuper du modèle ici.
TODO: et de la mise en place de l'administration, parce que nous en aurons besoin pour les étapes de déploiement.
Nous n'allons pas nous occuper de l'accès à la base de données pour le moment (nous nous en occuperons dans un prochain chapitre) et nous nous contenterons de remplir un canevas avec un ensemble de données.
Le module \texttt{gwift.views} qui se trouve dans le fichier \texttt{gwift/views.py} peut ressembler à ceci:
\begin{minted}{Python}
[...]
from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
context = {
"user_name": "Bond,"
"user_first_name": "James",
"now": datetime.now()
}
return render(
request,
"wish_details.html",
context
)
# gwift/views.py
[...]
from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
context = {
"user_name": "Bond,"
"user_first_name": "James",
"generated_at": datetime.now()
}
return render(
request,
"wish_details.html",
context
)
\end{minted}
Pour résumer, cette fonction permet:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
De construire un \emph{contexte}, qui est représenté sous la forme d'un dictionnaire associant des clés à des valeurs. Les clés sont respectivement \texttt{user\_name}, \texttt{user\_first\_name} et \texttt{now}, tandis que leurs valeurs respectives sont \texttt{Bond}, \texttt{James} et le \texttt{moment\ présent} \footnote{Non, pas celui d'Eckhart Tolle}.
De construire un \emph{contexte}, qui est représenté sous la forme d'un dictionnaire associant des clés à des valeurs.
Les clés sont respectivement \texttt{user\_name}, \texttt{user\_first\_name} et \texttt{now}, tandis que leurs valeurs respectives sont \texttt{Bond}, \texttt{James} et le \texttt{moment\ présent} \footnote{Non, pas celui d'Eckhart Tolle}.
\item
Nous passons ensuite ce dictionnaire à un canevas, \texttt{wish\_details.html}
Nous passons ensuite ce dictionnaire à un canevas, \texttt{wish\_details.html}, que l'on trouve normalement dans le répertoire \texttt{templates} de notre projet, ou dans le répertoire \texttt{templates} propre à notre application.
\item
L'application du contexte sur le canevas nous donne un résultat.
L'application du contexte sur le canevas au travers de la fonction \texttt{render} nous donne un résultat formaté.
\end{enumerate}
\begin{minted}{html}
@ -382,7 +382,7 @@ Pour résumer, cette fonction permet:
<body>
<h1>Hi!</h1>
<p>My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.</p>
<p>This page was generated at {{ now }}</p>
<p>This page was generated at {{ generated_at }}</p>
</body>
</html>
\end{minted}
@ -411,46 +411,11 @@ Après application de notre contexte sur ce template, nous obtiendrons ce docume
\section{Configuration globale}
\subsection{setup.cfg}
→ Faire le lien avec les settings → Faire le lien avec les douze
facteurs → Construction du fichier setup.cfg
\begin{verbatim}
[flake8]
max-line-length = 100
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[pycodestyle]
max-line-length = 100
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[mypy]
python_version = 3.8
check_untyped_defs = True
ignore_missing_imports = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
plugins = mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = config.settings.test
[mypy-*.migrations.*]
# Django migrations should not produce any errors:
ignore_errors = True
[coverage:run]
include = khana/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin
\end{verbatim}
\subsection{Structure finale}
Nous avons donc la structure finale pour notre environnement de travail:
En repartant de la structure initiale décrite au chapitre précédent, nous arrivons à ceci.
TODO : passer à poetry
\subsection{Cookie-cutter}

View File

@ -749,4 +749,42 @@ reste extrêmement efficace.
\section{Conclusions (et intégration continue)}
\subsection{setup.cfg}
→ Faire le lien avec les settings → Faire le lien avec les douze
facteurs → Construction du fichier setup.cfg
\begin{verbatim}
[flake8]
max-line-length = 100
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[pycodestyle]
max-line-length = 100
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[mypy]
python_version = 3.8
check_untyped_defs = True
ignore_missing_imports = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
plugins = mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = config.settings.test
[mypy-*.migrations.*]
# Django migrations should not produce any errors:
ignore_errors = True
[coverage:run]
include = khana/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin
\end{verbatim}
Mypy + black + pylint + flake8 + pyflakes + ...

View File

@ -13,22 +13,17 @@ et d'installation globale de dépendances, pouvant potentiellement
occasioner des conflits entre les applications déployées:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Il est tout à fait envisagable que deux applications différentes
soient déployées sur un même hôte, et nécessitent chacune deux
versions différentes d'une même dépendance.
Il est tout à fait envisagable que deux applications différentes soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d'une même dépendance.
\item
Pour la reproductibilité d'un environnement spécifique, cela évite
notamment les réponses type "Ca juste marche chez moi", puisque la
construction d'un nouvel environnement fait partie intégrante du
processus de construction et de la documentation du projet; grâce à
elle, nous avons la possibilité de construire un environnement sain et
d'appliquer des dépendances identiques, quelle que soit la machine
hôte.
Pour la reproductibilité d'un environnement spécifique, cela évite notamment les réponses type "Ca marche chez moi", puisque la construction du nouvel environnement fait partie intégrante du processus de développement et de la documentation du projet; grâce à elle, nous avons la possibilité de construire un environnement sain et d'appliquer des dépendances identiques, quelle que soit l'hôte destiné à accueillir le déploiment.
\end{enumerate}
\includegraphics{images/it-works-on-my-machine.jpg}
\begin{figure}
\centering
\includegraphics{images/it-works-on-my-machine.jpg}
\caption{It works on my machine}
\end{figure}
\section{Environnements virtuels}
@ -36,7 +31,7 @@ occasioner des conflits entre les applications déployées:
\centering
\includegraphics{images/xkcd-1987.png}
\caption{\url{https://xkcd.com/1987}}
\end{figure}
\end{figure}
Un des reproches que l'on peut faire au langage concerne sa versatilité: il est possible de réaliser beaucoup de choses, mais celles-ci ne sont
pas toujours simples ou directes.
@ -117,15 +112,9 @@ J'ai pour habitude de conserver mes projets dans un répertoire
\texttt{\textasciitilde{}/Sources/} et mes environnements virtuels dans
un répertoire \texttt{\textasciitilde{}/.venvs/}.
Cette séparation évite que l'environnement virtuel ne se trouve dans le
même répertoire que les sources, ou ne soit accidentellement envoyé vers
le système de gestion de versions. Elle évite également de rendre ce
répertoire "visible" - il ne s'agit au fond que d'un paramètre de
configuration lié uniquement à votre environnement de développement; les
environnements virtuels étant disposables, il n'est pas conseillé de
trop les lier au projet qui l'utilise comme base. Dans la suite de ce
chapitre, je considérerai ces mêmes répertoires, mais n'hésitez pas à
les modifier.
Cette séparation évite que l'environnement virtuel ne se trouve dans le même répertoire que les sources, ou ne soit accidentellement envoyé vers le système de gestion de versions. Elle évite également de rendre ce
répertoire "visible" - il ne s'agit au fond que d'un paramètre de configuration lié uniquement à votre environnement de développement; les environnements virtuels étant disposables, il n'est pas conseillé de trop les lier au projet qui l'utilise comme base.
Dans la suite de ce chapitre, je considérerai ces mêmes répertoires, mais n'hésitez pas à les modifier.
DANGER: Indépendamment de l'endroit où vous stockerez le répertoire
contenant cet environnement, il est primordial de \textbf{ne pas le

View File

@ -71,6 +71,7 @@
\include{chapters/infrastructure.tex}
\include{chapters/deployments.tex}
\include{chapters/heroku.tex}
\include{chapters/kubernetes.tex}
\include{chapters/deployment-tools.tex}
\include{chapters/deployment-processes.tex}
\chapter{Conclusions}

View File

@ -1,4 +1,4 @@
\part{Principes fondamenteux de Django}
\part{Principes fondamentaux de Django}
Dans cette partie, 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.
@ -13,6 +13,17 @@ Dans un \textbf{pattern} MVC classique, la traduction immédiate du \textbf{cont
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 \texttt{urls.py}.
\begin{center}
\begin{tabular}{ |c|c|c| }
\hline
tableau comparatif MVC vs MTV & cell2 & cell3 \\
cell4 & cell5 & cell6 \\
cell7 & cell8 & cell9 \\
\hline
\end{tabular}
\end{center}
\begin{itemize}
\item
Le \textbf{modèle} (\texttt{models.py}) fait le lien avec la base de données et permet de définir les champs et leur type à associer à une table.

File diff suppressed because it is too large Load Diff

View File

@ -1,156 +0,0 @@
=== 12 facteurs
Pour la méthode de travail et de développement, nous allons nous baser sur les https://12factor.net/fr/[The Twelve-factor App] - ou plus simplement les *12 facteurs*.
L'idée derrière cette méthode, et indépendamment des langages de développement utilisés, consiste à suivre un ensemble de douze concepts, afin de:
. *Faciliter la mise en place de phases d'automatisation*; plus concrètement, de faciliter les mises à jour applicatives, simplifier la gestion de l'hôte, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'https://en.wikipedia.org/wiki/Continuous_integration[intégration continue] ou link:https://en.wikipedia.org/wiki/Continuous_deployment[déploiement continu]
. *Faciliter la mise à pied de nouveaux développeurs ou de personnes souhaitant rejoindre le projet*, dans la mesure où la mise à disposition d'un environnement sera grandement facilitée.
. *Minimiser les divergences entre les différents environnemens sur lesquels un projet pourrait être déployé*
. *Augmenter l'agilité générale du projet*, en permettant une meilleure évolutivité architecturale et une meilleure mise à l'échelle - _Vous avez 5000 utilisateurs en plus? Ajoutez un serveur et on n'en parle plus ;-)_.
En pratique, les points ci-dessus permettront de monter facilement un nouvel environnement - qu'il soit sur la machine du petit nouveau dans l'équipe, sur un serveur Azure/Heroku/Digital Ocean ou votre nouveau Raspberry Pi Zéro caché à la cave - et vous feront gagner un temps précieux.
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*.
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, 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, ...)
* Outils de déploiement
* Standards de tests, y compris tout ce qui touche à la sécurité
* Outils de déploiement de pipeline
* Outils d'analyse et de monitoring
* Tutoriaux
*#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.
Cela permet d'éviter que l'application n'utilise une dépendance qui soit déjà installée sur un des sytèmes de développement, et qu'elle soit difficile, voire impossible, à répercuter sur un autre environnement.
Dans notre cas, cela pourra être fait au travers de https://pypi.org/project/pip/[PIP - Package Installer for Python] ou https://python-poetry.org/[Poetry].
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.
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*
Nous voulons éviter d'avoir à recompiler/redéployer l'application parce que:
. l'adresse du serveur de messagerie a été modifiée,
. un protocole a changé en cours de route
. la base de données a été déplacée
. ...
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.
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*
. La *construction* (_build_) convertit un code source en un ensemble de fichiers exécutables, associé à une version et à une transaction dans le système de gestion de sources.
. 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, 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, 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.
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 (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 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.
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].
Si vous faites vos développements sous SQLite et que vous rencontrez une URL de plus de 200 caractères, votre développement sera passera parfaitement bien, mais plantera en production (ou en _staging_, si vous faites les choses un peu mieux) parce que les données seront tronquées...
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`.
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.
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.
=== Concevoir pour l'opérationnel
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.
* 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, ...)
* Gestion de l'espace de stockage associé à un environnement (pour éviter d'avoir un environnement de production qui fait 157 Tera-octets)
* 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

@ -1,67 +0,0 @@
= Environnement et méthodes de travail
"Make it work, make it right, make it fast"
-- Kent Beck
En fonction de vos connaissances et compétences, la création dune nouvelle application est uneé tape relativement
facile à mettre en place.
Le code qui permet de faire tourner cette application peut ne pas être élégant, voire buggé jusqu'à la moëlle,
il pourra fonctionner et faire "preuve de concept".
Les problèmes arriveront lorsqu'une nouvelle demande sera introduite, lorsqu'un bug sera découvert et devra être corrigé
ou lorsqu'une dépendance cessera de fonctionner ou d'être disponible.
Or, une application qui névolue pas, meurt.
Tout application est donc destinée, soit à être modifiée, corrigée et suivie, soit à déperrir et à être délaissée
par ses utilisateurs.
Et cest juste cette maintenance qui est difficile.
Lapplication des principes présentés et agrégés ci-dessous permet surtout de préparer correctement tout ce qui pourra arriver,
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,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.
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.
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-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):
. Démarrer un script,
. 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...).
include::clean_code.adoc[]
include::mccabe.adoc[]
== Fiabilité, évolutivité et maintenabilité
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

@ -1,95 +0,0 @@
[quote, Brian Foote and Joseph Yoder]
If you think good architecture is expensive, try bad architecture
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.
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, ...),
le plus tard possible, tout en conservant la politique principale en ligne de mire.
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
TODO: Un p'tit bout à ajouter sur les méthodes de conception ;)
=== 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.
Your architecture should tell readers about the system, not about the frameworks you used in your system.
If you are building a health care system, then when new programmers look at the source repository,
their first impression should be, « oh, this is a health care system ».
Those new programmers should be able to learn all the use cases of the system,
yet still not know how the system is delivered.
Le point soulevé ci-dessous est qu'un framework n'est qu'un outil, et pas une obligation de structuration.
L'idée est que le framework doit se conformer à la définition de l'application, et non l'inverse.
Dans le cadre de l'utilisation de Django, c'est un point critique à prendre en considération: une fois que vous aurez fait
ce choix, vous aurez extrêmement difficile à faire machine arrière:
- Votre modèle métier sera largement couplé avec le type de base de données (relationnelle, indépendamment
- Votre couche de présentation sera surtout disponible au travers d'un navigateur
- Les droits d'accès et permissions seront en grosse partie gérés par le frameworks
- La sécurité dépendra de votre habilité à suivre les versions
- Et les fonctionnalités complémentaires (que vous n'aurez pas voulu/eu le temps de développer) dépendront
de la bonne volonté de la communauté
Le point à comprendre ici n'est pas que "Django, c'est mal", mais qu'une fois que vous aurez défini la politique,
les règles métiers, les données critiques et entités, et que vous aurez fait le choix de développer en âme et conscience
votre nouvelle création en utilisant Django, vous serez bon gré mal gré, contraint de continuer avec.
Cette décision ne sera pas irrévocable, mais difficile à contourner.
> At some point in their history most DevOps organizations were hobbled by tightly-coupled, monolithic architectures that while extremely successfull at helping them achieve product/market fit - put them at risk of organizational failure once they had to operate at scale (e.g. eBay's monolithic C++ application in 2001, Amazon's monolithic OBIDOS application in 2001, Twitter's monolithic Rails front-end in 2009, and LinkedIn's monolithic Leo application in 2011).
> In each of these cases, they were able to re-architect their systems and set the stage not only to survice, but also to thrise and win in the marketplace
cite:[devops_handbook(182)]
Ceci dit, Django compense ses contraintes en proposant énormément de flexibilité et de fonctionnalités
*out-of-the-box*, c'est-à-dire que vous pourrez sans doute avancer vite et bien jusqu'à un point de rupture,
puis revoir la conception et réinvestir à ce moment-là, mais en toute connaissance de cause.
[quote, Robert C. Martin, Clean Architecture, page 209]
When any of the external parts of the system become obsolete, such as the database, or the web framework,
you can replace those obsolete elements with a minimum of fuss.
Avec Django, la difficulté à se passer du framework va consister à basculer vers « autre chose » et a remplacer
chacune des tentacules qui aura pousser partout dans lapplication.
NOTE: A noter que les services et les « architectures orientées services » ne sont jamais quune définition
dimplémentation des frontières, dans la mesure où un service nest jamais quune fonction appelée au travers d'un protocole
(rest, soap, ...).
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
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.
Les link:release notes[https://docs.djangoproject.com/en/2.0/releases/2.0/] de Django 2.0 date de décembre 2017; parmi ces notes,
l'une d'elles cite l'abandon du support d'link:Oracle 11.2[https://docs.djangoproject.com/en/2.0/releases/2.0/#dropped-support-for-oracle-11-2].
En substance, cela signifie que le framework se chargeait lui-même de construire certaines parties de requêtes,
qui deviennent non fonctionnelles dès lors que l'on met le framework ou le moteur de base de données à jour.
Réécrit, cela signifie que:
1. Si vos données sont stockées dans un moteur géré par Oracle 11.2, vous serez limité à une version 1.11 de Django
2. Tandis que si votre moteur est géré par une version ultérieure, le framework pourra être mis à jour.
Nous sommes dans un cas concret d'inversion de dépendances ratée: le framework (et encore moins vos politiques et règles métiers)
ne devraient pas avoir connaissance du moteur de base de données.
Pire, vos politiques et données métiers ne devraient pas avoir connaissance **de la version** du moteur de base de données.
En conclusion, le choix d'une version d'un moteur technique (*la base de données*) a une incidence directe sur les fonctionnalités
mises à disposition par votre application, ce qui va à l'encontre des 12 facteurs (et des principes de développement).
Ce point sera rediscuté par la suite, notamment au niveau de l'épinglage des versions, de la reproduction des environnements
et de l'interdépendance entre des choix techniques et fonctionnels.

View File

@ -1,113 +0,0 @@
== 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.
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.
Une forme de comportement de Descartes, qui ne croit pas en Dieu, mais qui envisage quand même cette possibilité,
ce qui lui ouvre le maximum de portes 🙃
Avec cette approche, les composants sont déjà découplés au niveau du code source, ce qui pourrait savérer suffisant
jusquau stade où une modification ne pourra plus faire reculer léchéance.
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
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
* Les composants
* Et des conseils plus généraux.
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 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 ou une structure, qui sera plus facile à tester.
==== Au niveau des classes
* *Privilégiez un couplage faible entre vos classes*. Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences respectif. L'implémentation du service `UserNotificationsService` ne doit pas forcément se trouver embarqué dans une classe `UserService`. De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction. La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée.
==== Au niveau des composants
* *Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants* également. Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'il ne soit possible de contacter trop facilement des couches séparées de l'architecture. Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "_juste parce qu'elle en a la possibilité_". Vous pourriez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que `IFileTransfer` avec ses méthodes `put` et `get`, et non pas les détails d'implémentation complet d'une classe `FtpFileTransfer` ou `SshFileTransfer`).
* *Conserver un bon balancement au niveau des composants*: évitez qu'un composant **A** ne soit un énorme mastodonte, alors que le composant juste à côté ne soit capable que d'une action. De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités seront plus faciles à gérer. Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que chacun de ces composants soit approximativement de même taille.
==== De manière plus générale
* *Conserver une densité de code faible*: il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois. Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisant du refactoring (intensif?), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux. Mais surtout, en évitant de copier/coller bêtement du code existant.
* *Automatiser les tests*, *ajouter un environnement d'intégration continue dès le début du projet* et *vérifier par des outils les points ci-dessus*.

File diff suppressed because it is too large Load Diff

View File

@ -1,642 +0,0 @@
=== Robustesse et flexibilité
[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]
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
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.
==== 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].
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:
[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 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]
----
class Document:
def __init__(self, title, content, published_at):
self.title = title
self.content = content
self.published_at = published_at
def render(self, format_type):
if format_type == "XML":
return """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".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))
----
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]
----
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 """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".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 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 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
> 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.
Lobjectif est de rendre le système facile à étendre, en évitant que limpact dune modification ne soit trop grand.
Les exemples parlent deux-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 dune couche de présentation), mais la mise en forme diffère à chaque fois.
Lapplication na pas à connaître les détails dimplémentation: elle doit juste permettre une forme dextension,
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.
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:
[source,python]
----
class Customer():
def __init__(self, customer_type: str):
self.customer_type = customer_type
def get_discount(customer: Customer) -> int:
if customer.customer_type == "Silver":
return 10
elif customer.customer_type == "Gold":
return 20
return 0
>>> jack = Customer("Silver")
>>> jack.get_discount()
10
----
A ceci:
[source,python]
----
class Customer():
def get_discount(self) -> int:
return 0
class SilverCustomer(Customer):
def get_discount(self) -> int:
return 10
class GoldCustomer(Customer):
def get_discount(self) -> int:
return 20
>>> jack = SilverCustomer()
>>> jack.get_discount()
10
----
En anglais, dans le texte : "_Putting in simple words, the “Customer” class is now closed for any new modification but its 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`].
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 """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".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 """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".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/stable/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 daccord avec les exemples donnés, dans la mesure où la définition concrète dune classe doit dépendre dune interface correctement définie (et que donc, faire hériter un carré dun rectangle, nest pas adéquat dans le mesure où cela induit lutilisateur 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.
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.
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`"_, nous allons nous retrouver avec ceci:
[source,python]
----
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):
def walk(self):
print("Roaaar!")
----
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`:
[source,python]
----
class XmlRenderer:
def render(self, document)
return """<?xml version = "1.0"?>
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".format(
document.title,
document.content,
document.published_at.isoformat()
)
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 """<?xml version = "1.0"?>"""
def render(self, document)
return """{}
<document>
<title>{}</title>
<content>{}</content>
<publication_date>{}</publication_date>
</document>""".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 (`<?xml version = "1.0"?>`) 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 nexposant que les opérations nécessaires à lexécution dune classe.
Ceci évite davoir à redéployer lensemble dune application.
> The lesson here is that depending on something that carries baggage that you dont need can cause you troubles that you didnt except.
Ce principe stipule qu'un client ne doit pas 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.
GNU/Linux Magazine cite:[gnu_linux_mag_hs_104(37-42)] propose un exemple d'interface permettant d'implémenter une imprimante:
[source,java]
----
interface IPrinter
{
public abstract void printPage();
public abstract void scanPage();
public abstract void faxPage();
}
public class Printer
{
protected string name;
public Printer(string name)
{
this.name = name;
}
}
----
L'implémentation d'une imprimante multifonction aura tout son sens:
[source,java]
----
public class AllInOnePrinter implements Printer extends IPrinter
{
public AllInOnePrinter(string name)
{
super(name);
}
public void printPage()
{
System.out.println(this.name + ": Impression");
}
public void scanPage()
{
System.out.println(this.name + ": Scan");
}
public void faxPage()
{
System.out.println(this.name + ": Fax");
}
}
----
Tandis que l'implémentation d'une imprimante premier-prix ne servira pas à grand chose:
[source,java]
----
public class FirstPricePrinter implements Printer extends IPrinter
{
public FirstPricePrinter(string name)
{
super(name);
}
public void printPage()
{
System.out.println(this.name + ": Impression");
}
public void scanPage()
{
System.out.println(this.name + ": Fonctionnalité absente");
}
public void faxPage()
{
System.out.println(this.name + ": Fonctionnalité absente");
}
}
----
L'objectif est donc de découpler ces différentes fonctionnalités en plusieurs interfaces bien spécifiques, implémentant chacune une opération isolée:
[source,java]
----
interface IPrinterPrinter
{
public abstract void printPage();
}
interface IPrinterScanner
{
public abstract void scanPage();
}
interface IPrinterFax
{
public abstract void faxPage();
}
----
Cette réflexion s'applique finalement à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, les variables déclarées, ...
Quel que soit le composant que l'on utilise ou analyse, il est plus qu'intéressant de se limiter uniquement à ce dont nous avons besoin plutôt que
En Python, ce comportement est inféré lors de lexécution, et donc pas vraiment dapplication 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 nait dimpact sur le reste de lapplication.
Il est ainsi possible de trouver quelques horreurs, dans tous les langages:
[source,javascript]
----
/*!
* is-odd <https://github.com/jonschlinkert/is-odd>
*
* Copyright (c) 2015-2017, Jon Schlinkert.
* Released under the MIT License.
*/
'use strict';
const isNumber = require('is-number');
module.exports = function isOdd(value) {
const n = Math.abs(value);
if (!isNumber(n)) {
throw new TypeError('expected a number');
}
if (!Number.isInteger(n)) {
throw new Error('expected an integer');
}
if (!Number.isSafeInteger(n)) {
throw new Error('value exceeds maximum safe integer');
}
return (n % 2) === 1;
};
----
Voire, son opposé, qui dépend évidemment du premier:
[source,javascript]
----
/*!
* is-even <https://github.com/jonschlinkert/is-even>
*
* Copyright (c) 2015, 2017, Jon Schlinkert.
* Released under the MIT License.
*/
'use strict';
var isOdd = require('is-odd');
module.exports = function isEven(i) {
return !isOdd(i);
};
----
Il ne s'agit que d'un simple exemple, mais qui tend à une seule chose: gardez les choses simples (et, éventuellement, stupides) (((kiss))).
Dans l'exemple ci-dessus, l'utilisation du module `is-odd` requière déjà deux dépendances: `is-even` et `is-number`.
Imaginez la suite.
==== Dependency inversion Principle
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.
Lobjectif est que les interfaces soient les plus stables possibles, afin de réduire au maximum les modifications qui pourraient y être appliquées.
De cette manière, toute modification fonctionnelle pourra être directement appliquée sur le composant de bas-niveau, sans que l'interface ne soit impactée.
> 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.
cite:[clean_architecture]
L'injection de dépendances est un patron de programmation qui suit le principe d'inversion de dépendances.
Django est bourré de ce principe, que ce soit pour les _middlewares_ ou pour les connexions aux bases de données.
Lorsque nous écrivons ceci dans notre fichier de configuration,
[source,python]
----
# [snip]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# [snip]
----
Django ira simplement récupérer chacun de ces middlewares, qui répondent chacun à une https://docs.djangoproject.com/en/4.0/topics/http/middleware/#writing-your-own-middleware[interface clairement définie], dans l'ordre.
Il n'y a donc pas de magie; c'est le développeur qui va simplement brancher ou câbler des fonctionnalités au niveau du framework, en les déclarant au bon endroit.
Pour créer un nouveau _middleware_, il suffira d'implémenter le code suivant et de l'ajouter dans la configuration de l'application:
[source,python]
----
def simple_middleware(get_response):
# One-time configuration and initialization.
def middleware(request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
return middleware
----
Dans d'autres projets écrits en Python, ce type de mécanisme peut être implémenté relativement facilement en utilisant les modules https://docs.python.org/3/library/importlib.html[importlib] et la fonction `getattr`.
Un autre exemple concerne les bases de données: pour garder un maximum de flexibilité, Django ajoute une couche d'abstraction en permettant de spécifier le moteur de base de données que vous souhaiteriez utiliser, qu'il s'agisse d'SQLite, MSSQL, Oracle, PostgreSQL ou MySQL/MariaDB footnote:[http://howfuckedismydatabase.com/].
> The database is really nothing more than a big bucket of bits where we store our data on a long term basis.
cite:[clean_architecture(281)]
Dun point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, sil sagit dun disque magnétique, de ram, ... en fait, on ne devrait même pas savoir sil y a un disque du tout.
Et Django le fait très bien pour nous.
En termes architecturaux, ce principe autorise une définition des frontières, et en permettant une séparation claire en inversant le flux de dépendances et en faisant en sorte que les règles métiers n'aient aucune connaissance des interfaces graphiques qui les exploitent ou des moteurs de bases de données qui les stockent.
Ceci autorise une forme d'immunité entre les composants.
==== Sources
* http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp[Understanding SOLID principles on CodeProject]
* http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/[Dependency Injection is NOT the same as dependency inversion]
* http://en.wikipedia.org/wiki/Dependency_injection[Injection de dépendances]
=== au niveau des composants
De la même manière que pour les principes définis ci-dessus,
Mais toujours en faisant attention quune fois que les frontières sont implémentés, elles sont coûteuses à maintenir.
Cependant, il ne sagit 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,
> but this is not a one time decision. You dont 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 dont 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é à sadapter en minimisant la maintenance.
Le problème est quelle ne permettait aucune adaptation, et quà la première demande, larchitecture se plante complètement sans aucune malléabilité.
==== Reuse/release equivalence principle
[quote]
----
Classes and modules that are grouped together into a component should be releasable together
-- (Chapitre 13, Component Cohesion, page 105)
----
==== CCP
(= 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
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
… que lon résumera ainsi: « dont depend on things you dont need » 😘
Au niveau des composants, au niveau architectural, mais également à dautres niveaux.
==== 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).
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 lorganigramme (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

View File

@ -1,736 +0,0 @@
== Démarrer un nouveau projet
=== Travailler en isolation
Nous allons aborder la gestion et l'isolation des dépendances.
Cette section est aussi utile pour une personne travaillant seule, que pour transmettre les connaissances à un nouveau membre de l'équipe ou pour déployer l'application elle-même.
Il en était déjà question au deuxième point des 12 facteurs: même dans le cas de petits projets, il est déconseillé de s'en passer.
Cela évite les déploiements effectués à l'arrache à grand renfort de `sudo` et d'installation globale de dépendances, pouvant potentiellement occasioner des conflits entre les applications déployées:
. Il est tout à fait envisagable que deux applications différentes soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d'une même dépendance.
. Pour la reproductibilité d'un environnement spécifique, cela évite notamment les réponses type "Ca juste marche chez moi", puisque la construction d'un nouvel environnement fait partie intégrante du processus de construction et de la documentation du projet; grâce à elle, nous avons la possibilité de construire un environnement sain et d'appliquer des dépendances identiques, quelle que soit la machine hôte.
image::images/it-works-on-my-machine.jpg[]
Dans la suite de ce chapitre, nous allons considérer deux projets différents:
. Gwift, une application permettant de gérer des listes de souhaits
. Khana, une application de suivi d'apprentissage pour des élèves ou étudiants.
==== Roulements de versions
Django fonctionne sur un https://docs.djangoproject.com/en/dev/internals/release-process/[roulement de trois versions mineures pour une version majeure],
clôturé par une version LTS (_Long Term Support_).
image::images/django-support-lts.png[]
La version utilisée sera une bonne indication à prendre en considération pour nos dépendances,
puisqu'en visant une version particulière, nous ne devrons pratiquement pas nous soucier
(bon, un peu quand même, mais nous le verrons plus tard...) des dépendances à installer,
pour peu que l'on reste sous un certain seuil.
Dans les étapes ci-dessous, nous épinglerons une version LTS afin de nous assurer une certaine sérénité d'esprit (= dont nous
ne occuperons pas pendant les 3 prochaines années).
==== Environnements virtuels
.https://xkcd.com/1987
image::images/xkcd-1987.png[]
Un des reproches que l'on peut faire au langage concerne sa versatilité: il est possible de réaliser beaucoup de choses, mais celles-ci ne sont pas toujours simples ou directes.
Pour quelqu'un qui débarquererait, la quantité d'options différentes peut paraître rebutante.
Nous pensons notamment aux environnements virtuels: ils sont géniaux à utiliser, mais on est passé par virtualenv (l'ancêtre), virtualenvwrapper (sa version améliorée et plus ergonomique), `venv` (la version intégrée depuis la version 3.3 de l'interpréteur, et https://docs.python.org/3/library/venv.html[la manière recommandée] de créer un environnement depuis la 3.5).
Pour créer un nouvel environnement, vous aurez donc besoin:
. D'une installation de Python - https://www.python.org/
. D'un terminal - voir le point <<../environment/_index.adoc#un-terminal,Un terminal>>
NOTE: Il existe plusieurs autres modules permettant d'arriver au même résultat, avec quelques avantages et inconvénients pour chacun d'entre eux. Le plus prometteur d'entre eux est https://python-poetry.org/[Poetry], qui dispose d'une interface en ligne de commande plus propre et plus moderne que ce que PIP propose.
Poetry se propose de gérer le projet au travers d'un fichier pyproject.toml. TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à ses heures), se place comme alternative aux formats comme JSON, YAML ou INI.
[source,bash]
----
La commande poetry new <project> créera une structure par défaut relativement compréhensible:
$ poetry new django-gecko
$ tree django-gecko/
django-gecko/
├── django_gecko
│ └── __init__.py
├── pyproject.toml
├── README.rst
└── tests
├── __init__.py
└── test_django_gecko.py
2 directories, 5 files
----
Ceci signifie que nous avons directement (et de manière standard):
* Un répertoire django-gecko, qui porte le nom de l'application que vous venez de créer
* Un répertoires tests, libellé selon les standards de pytest
* Un fichier README.rst (qui ne contient encore rien)
* Un fichier pyproject.toml, qui contient ceci:
[source,toml]
----
[tool.poetry]
name = "django-gecko"
version = "0.1.0"
description = ""
authors = ["... <...@grimbox.be>"]
[tool.poetry.dependencies]
python = "^3.9"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
----
La commande `poetry init` permet de générer interactivement les fichiers nécessaires à son intégration dans un projet existant.
NOTE: J'ai pour habitude de conserver mes projets dans un répertoire `~/Sources/` et mes environnements virtuels dans un répertoire `~/.venvs/`.
Cette séparation évite que l'environnement virtuel ne se trouve dans le même répertoire que les sources, ou ne soit accidentellement envoyé vers le système de gestion de versions.
Elle évite également de rendre ce répertoire "visible" - il ne s'agit au fond que d'un paramètre de configuration lié uniquement à votre environnement de développement; les environnements virtuels étant disposables, il n'est pas conseillé de trop les lier au projet qui l'utilise comme base.
Dans la suite de ce chapitre, je considérerai ces mêmes répertoires, mais n'hésitez pas à les modifier.
DANGER: Indépendamment de l'endroit où vous stockerez le répertoire contenant cet environnement, il est primordial de **ne pas le conserver dans votre dépôt de stockager**.
Cela irait à l'encontre des douze facteurs, cela polluera inutilement vos sources et créera des conflits avec l'environnement des personnes qui souhaiteraient intervenir sur le projet.
Pur créer notre répertoire de travail et notre environnement virtuel, exécutez les commandes suivantes:
[source,bash]
----
mkdir ~/.venvs/
python -m venv ~/.venvs/gwift-venv
----
Ceci aura pour effet de créer un nouveau répertoire (`~/.venvs/gwift-env/`), dans lequel vous trouverez une installation complète de l'interpréteur Python.
Votre environnement virtuel est prêt, il n'y a plus qu'à indiquer que nous souhaitons l'utiliser, grâce à l'une des commandes suivantes:
[source,bash]
----
# GNU/Linux, macOS
source ~/.venvs/gwift-venv/bin/activate
# MS Windows, avec Cmder
~/.venvs/gwift-venv/Scripts/activate.bat
# Pour les deux
(gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$ <1>
----
<1> Le terminal signale que nous sommes bien dans l'environnement `gwift-env`.
A présent que l'environnement est activé, tous les binaires de cet environnement prendront le pas sur les binaires du système.
De la même manière, une variable `PATH` propre est définie et utilisée, afin que les librairies Python y soient stockées.
C'est donc dans cet environnement virtuel que nous retrouverons le code source de Django, ainsi que des librairies externes pour Python une fois que nous les aurons installées.
NOTE: Pour les curieux, un environnement virtuel n'est jamais qu'un répertoire dans lequel se trouve une installation fraîche de l'interpréteur, vers laquelle pointe les liens symboliques des binaires. Si vous recherchez l'emplacement de l'interpréteur avec la commande `which python`, vous recevrez comme réponse `/home/fred/.venvs/gwift-env/bin/python`.
Pour sortir de l'environnement virtuel, exécutez la commande `deactivate`.
Si vous pensez ne plus en avoir besoin, supprimer le dossier.
Si nécessaire, il suffira d'en créer un nouveau.
Pour gérer des versions différentes d'une même librairie, il nous suffit de jongler avec autant d'environnements que nécessaires. Une application nécessite une version de Django inférieure à la 2.0 ? On crée un environnement, on l'active et on installe ce qu'il faut.
Cette technique fonctionnera autant pour un poste de développement que sur les serveurs destinés à recevoir notre application.
NOTE: Par la suite, nous considérerons que l'environnement virtuel est toujours activé, même si `gwift-env` n'est pas indiqué.
a manière recommandée pour la gestion des dépendances consiste à les épingler dans un fichier requirements.txt, placé à la racine du projet. Ce fichier reprend, ligne par ligne, chaque dépendance et la version nécessaire. Cet épinglage est cependant relativement basique, dans la mesure où les opérateurs disponibles sont ==, <= et >=.
Poetry propose un épinglage basé sur SemVer. Les contraintes qui peuvent être appliquées aux dépendances sont plus touffues que ce que proposent pip -r, avec la présence du curseur ^, qui ne modifiera pas le nombre différent de zéro le plus à gauche:
^1.2.3 (où le nombre en question est 1) pourra proposer une mise à jour jusqu'à la version juste avant la version 2.0.0
^0.2.3 pourra être mise à jour jusqu'à la version juste avant 0.3.0.
...
L'avantage est donc que l'on spécifie une version majeure - mineure - patchée, et que l'on pourra spécifier accepter toute mise à jour jusqu'à la prochaine version majeure - mineure patchée (non incluse 😉).
Une bonne pratique consiste également, tout comme pour npm, à intégrer le fichier de lock (poetry.lock) dans le dépôt de sources: de cette manière, seules les dépendances testées (et intégrées) seront considérées sur tous les environnements de déploiement.
Il est alors nécessaire de passer par une action manuelle (poetry update) pour mettre à jour le fichier de verrou, et assurer une mise à jour en sécurité (seules les dépendances testées sont prises en compte) et de qualité (tous les environnements utilisent la même version d'une dépendance).
L'ajout d'une nouvelle dépendance à un projet se réalise grâce à la commande `poetry add <dep>`:
[source,shell]
----
$ poetry add django
Using version ^3.2.3 for Django
Updating dependencies
Resolving dependencies... (5.1s)
Writing lock file
Package operations: 8 installs, 1 update, 0 removals
• Installing pyparsing (2.4.7)
• Installing attrs (21.2.0)
• Installing more-itertools (8.8.0)
• Installing packaging (20.9)
• Installing pluggy (0.13.1)
• Installing py (1.10.0)
• Installing wcwidth (0.2.5)
• Updating django (3.2 -> 3.2.3)
• Installing pytest (5.4.3)
----
Elle est ensuite ajoutée à notre fichier `pyproject.toml`:
[source,toml]
----
[...]
[tool.poetry.dependencies]
python = "^3.9"
Django = "^3.2.3"
[...]
----
Et contrairement à `pip`, pas besoin de savoir s'il faut pointer vers un fichier (`-r`) ou un dépôt VCS (`-e`), puisque Poetry va tout essayer, [dans un certain ordre](https://python-poetry.org/docs/cli/#add).
L'avantage également (et cela m'arrive encore souvent, ce qui fait hurler le runner de Gitlab), c'est qu'il n'est plus nécessaire de penser à épingler la dépendance que l'on vient d'installer parmi les fichiers de requirements, puisqu'elles s'y ajoutent automatiquement grâce à la commande `add`.
==== Python packaging made easy
Cette partie dépasse mes compétences et connaissances, dans la mesure où je n'ai jamais rien packagé ni publié sur [pypi.org](pypi.org).
Ce n'est pas l'envie qui manque, mais les idées et la nécessité 😉.
Ceci dit, Poetry propose un ensemble de règles et une préconfiguration qui (doivent) énormément facilite(r) la mise à disposition de librairies sur Pypi - et rien que ça, devrait ouvrir une partie de l'écosystème.
Les chapitres 7 et 8 de [Expert Python Programming - Third Edtion](#), écrit par Michal Jaworski et Tarek Ziadé en parlent très bien:
> Python packaging can be a bit overwhelming at first.
> The main reason for that is the confusion about proper tools for creating Python packages.
> Anyway, once you create your first package, you will se that this is as hard as it looks.
> Also, knowing propre, state-of-the-art packaging helps a lot.
En gros, c'est ardu-au-début-mais-plus-trop-après.
Et c'est heureusement suivi et documenté par la PyPA (*https://github.com/pypa[Python Packaging Authority]*).
Les étapes sont les suivantes:
1. Utiliser setuptools pour définir les projets et créer les distributions sources,
2. Utiliser **wheels** pour créer les paquets,
3. Passer par **twine** pour envoyer ces paquets vers PyPI
4. Définir un ensemble d'actions (voire, de plugins nécessaires - lien avec le VCS, etc.) dans le fichier `setup.py`, et définir les propriétés du projet ou de la librairie dans le fichier `setup.cfg`.
Avec Poetry, deux commandes suffisent (théoriquement - puisque je n'ai pas essayé 🤪): `poetry build` et `poetry publish`:
[source,shell]
----
$ poetry build
Building geco (0.1.0)
- Building sdist
- Built geco-0.1.0.tar.gz
- Building wheel
- Built geco-0.1.0-py3-none-any.whl
$ tree dist/
dist/
├── geco-0.1.0-py3-none-any.whl
└── geco-0.1.0.tar.gz
0 directories, 2 files
----
Ce qui est quand même 'achement plus simple que d'appréhender tout un écosystème.
==== Gestion des dépendances, installation de Django et création d'un nouveau projet
Comme nous en avons déjà discuté, PIP est la solution que nous avons choisie pour la gestion de nos dépendances.
Pour installer une nouvelle librairie, vous pouvez simplement passer par la commande `pip install <my_awesome_library>`.
Dans le cas de Django, et après avoir activé l'environnement, nous pouvons à présent y installer Django.
Comme expliqué ci-dessus, la librairie restera indépendante du reste du système, et ne polluera aucun autre projet. nous exécuterons donc la commande suivante:
[source,bash]
----
$ source ~/.venvs/gwift-env/bin/activate # ou ~/.venvs/gwift-env/Scrips/activate.bat pour Windows.
$ pip install django
Collecting django
Downloading Django-3.1.4
100% |################################|
Installing collected packages: django
Successfully installed django-3.1.4
----
IMPORTANT: Ici, la commande `pip install django` récupère la *dernière version connue disponible dans les dépôts https://pypi.org/* (sauf si vous en avez définis d'autres. Mais c'est hors sujet).
Nous en avons déjà discuté: il est important de bien spécifier la version que vous souhaitez utiliser, sans quoi vous risquez de rencontrer des effets de bord.
L'installation de Django a ajouté un nouvel exécutable: `django-admin`, que l'on peut utiliser pour créer notre nouvel espace de travail.
Par la suite, nous utiliserons `manage.py`, qui constitue un *wrapper* autour de `django-admin`.
Pour démarrer notre projet, nous lançons `django-admin startproject gwift`:
[source,bash]
----
$ django-admin startproject gwift
----
Cette action a pour effet de créer un nouveau dossier `gwift`, dans lequel nous trouvons la structure suivante:
[source,bash]
----
$ tree gwift
gwift
├── gwift
| |── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
----
C'est dans ce répertoire que vont vivre tous les fichiers liés au projet. Le but est de faire en sorte que toutes les opérations (maintenance, déploiement, écriture, tests, ...) puissent se faire à partir d'un seul point d'entrée.
L'utilité de ces fichiers est définie ci-dessous:
* `settings.py` contient tous les paramètres globaux à notre projet.
* `urls.py` contient les variables de routes, les adresses utilisées et les fonctions vers lesquelles elles pointent.
* `manage.py`, pour toutes les commandes de gestion.
* `asgi.py` contient la définition de l'interface https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface[ASGI], le protocole pour la passerelle asynchrone entre votre application et le serveur Web.
* `wsgi.py` contient la définition de l'interface https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface[WSGI], qui permettra à votre serveur Web (Nginx, Apache, ...) de faire un pont vers votre projet.
NOTE: Indiquer qu'il est possible d'avoir plusieurs structures de dossiers et qu'il n'y a pas de "magie" derrière toutes ces commandes.
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel nous stockerons les dépendances et un fichier README:
[source,bash]
----
(gwift) $ mkdir requirements
(gwift) $ touch README.md
(gwift) $ tree gwift
gwift
├── gwift
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── requirements <1>
├── README.md <2>
└── manage.py
----
<1> Ici
<2> Et là
Comme nous venons d'ajouter une dépendance à notre projet, profitons-en pour créer un fichier reprenant tous les dépendances de notre projet.
Celles-ci sont normalement placées dans un fichier `requirements.txt`.
Dans un premier temps, ce fichier peut être placé directement à la racine du projet, mais on préférera rapidement le déplacer dans un sous-répertoire spécifique (`requirements`), afin de grouper les dépendances en fonction de leur environnement de destination:
* `base.txt`
* `dev.txt`
* `production.txt`
Au début de chaque fichier, il suffit d'ajouter la ligne `-r base.txt`, puis de lancer l'installation grâce à un `pip install -r <nom du fichier>`.
De cette manière, il est tout à fait acceptable de n'installer `flake8` et `django-debug-toolbar` qu'en développement par exemple.
Dans l'immédiat, nous allons ajouter `django` dans une version strictement inférieure à la version 3.2 dans le fichier `requirements/base.txt`.
[source,bash]
----
$ echo 'django==3.2' > requirements/base.txt
$ echo '-r base.txt' > requirements/prod.txt
$ echo '-r base.txt' > requirements/dev.txt
----
IMPORTANT: Prenez directement l'habitude de spécifier la version ou les versions compatibles: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets.
Pour être sûr et certain le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances.
Entre deux versions d'une même librairie, des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc. Il suffit de parcourir les pages de _Changements incompatibles avec les anciennes versions dans Django_ https://docs.djangoproject.com/fr/3.1/releases/3.0/[(par exemple ici pour le passage de la 3.0 à la 3.1)] pour réaliser que certaines opérations ne sont pas anodines, et que sans filet de sécurité, c'est le mur assuré.
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.
Nous faisons ici une distinction entre un **projet** et une **application**:
* *Un projet* représente l'ensemble des applications, paramètres, pages HTML, middlewares, dépendances, etc., qui font que votre code fait ce qu'il est sensé faire.
* *Une application* est un contexte d'exécution, idéalement autonome, d'une partie du projet.
Pour `gwift`, nous aurons:
.Django Projet vs Applications
image::images/django/django-project-vs-apps-gwift.png[]
. une première application pour la gestion des listes de souhaits et des éléments,
. une deuxième application pour la gestion des utilisateurs,
. voire une troisième application qui gérera les partages entre utilisateurs et listes.
Nous voyons également que la gestion des listes de souhaits et éléments aura besoin de la gestion des utilisateurs - elle n'est pas autonome -, tandis que la gestion des utilisateurs n'a aucune autre dépendance qu'elle-même.
Pour `khana`, nous pourrions avoir quelque chose comme ceci:
.Django Project vs Applications
image::images/django/django-project-vs-apps-khana.png[]
En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la gestion des utilisateurs et la possibilité qu'ils auront de communiquer entre eux.
Ceci pourrait être commun aux deux applications.
Nous pouvons clairement visualiser le principe de **contexte** pour une application: celle-ci viendra avec son modèle, ses tests, ses vues et son paramétrage et pourrait ainsi être réutilisée dans un autre projet.
C'est en ça que consistent les https://www.djangopackages.com/[paquets Django] déjà disponibles: ce sont "_simplement_" de petites applications empaquetées et pouvant être réutilisées dans différents contextes (eg. https://github.com/tomchristie/django-rest-framework[Django-Rest-Framework], https://github.com/django-debug-toolbar/django-debug-toolbar[Django-Debug-Toolbar], ...).
==== manage.py
Le fichier `manage.py` que vous trouvez à la racine de votre projet est un *wrapper* sur les commandes `django-admin`.
A partir de maintenant, nous n'utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet:
* `manage.py check` pour vérifier (en surface...) que votre projet ne rencontre aucune erreur évidente
* `manage.py check --deploy`, pour vérifier (en surface aussi) que l'application est prête pour un déploiement
* `manage.py runserver` pour lancer un serveur de développement
* `manage.py test` pour découvrir les tests unitaires disponibles et les lancer.
La liste complète peut être affichée avec `manage.py help`.
Vous remarquerez que ces commandes sont groupées selon différentes catégories:
* **auth**: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.
* **django**: vérifier la *compliance* du projet, lancer un *shell*, *dumper* les données de la base, effectuer une migration du schéma, ...
* **sessions**: suppressions des sessions en cours
* **staticfiles**: gestion des fichiers statiques et lancement du serveur de développement.
Nous verrons plus tard comment ajouter de nouvelles commandes.
Si nous démarrons la commande `python manage.py runserver`, nous verrons la sortie console suivante:
[source,bash]
----
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
[...]
December 15, 2020 - 20:45:07
Django version 3.1.4, using settings 'gwift.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
----
Si nous nous rendons sur la page http://127.0.0.1:8000 (ou http://localhost:8000) comme le propose si gentiment notre (nouveau) meilleur ami, nous verrons ceci:
.python manage.py runserver (Non, ce n'est pas Challenger)
image::images/django/manage-runserver.png[]
IMPORTANT: Nous avons mis un morceau de la sortie console entre crochet `[...]` ci-dessus, car elle concerne les migrations.
Si vous avez suivi les étapes jusqu'ici, vous avez également dû voir un message type `You have 18 unapplied migration(s). [...] Run 'python manage.py migrate' to apply them.`
Cela concerne les migrations, et c'est un point que nous verrons un peu plus tard.
==== Création d'une nouvelle application
Maintenant que nous avons a vu à quoi servait `manage.py`, nous pouvons créer notre nouvelle application grâce à la commande `manage.py startapp <label>`.
Notre première application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir.
De manière générale, essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application.
Pour nous, ce sera donc `wish`.
C'est parti pour `manage.py startapp wish`!
[source,bash]
----
$ python manage.py startapp wish
----
Résultat? Django nous a créé un répertoire `wish`, dans lequel nous trouvons les fichiers et dossiers suivants:
* `wish/__init__.py` pour que notre répertoire `wish` soit converti en package Python.
* `wish/admin.py` servira à structurer l'administration de notre application. Chaque information peut être administrée facilement au travers d'une interface générée à la volée par le framework. Nous y reviendrons par la suite.
* `wish/apps.py` qui contient la configuration de l'application et qui permet notamment de fixer un nom ou un libellé https://docs.djangoproject.com/en/stable/ref/applications/
* `wish/migrations/` est le dossier dans lequel seront stockées toutes les différentes migrations de notre application (= toutes les modifications que nous apporterons aux données que nous souhaiterons manipuler)
* `wish/models.py` représentera et structurera nos données, et est intimement lié aux migrations.
* `wish/tests.py` pour les tests unitaires.
NOTE: Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire `wish` dans votre répertoire `gwift` existant.
C'est une forme de convention.
La structure de vos répertoires devient celle-ci:
[source,bash]
----
(gwift-env) fred@aerys:~/Sources/gwift$ tree .
.
├── gwift
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   ├── wish <1>
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   └── wsgi.py
├── Makefile
├── manage.py
├── README.md
├── requirements
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt
├── setup.cfg
└── tox.ini
5 directories, 22 files
----
<1> Notre application a bien été créée, et nous l'avons déplacée dans le répertoire `gwift` !
==== Fonctionement général
Le métier de programmeur est devenu de plus en plus complexe. Il y a 20 ans, nous pouvions nous contenter d'une simple page PHP dans laquelle nous mixions l'ensemble des actios à réaliser: requêtes en bases de données, construction de la page, ...
La recherche d'une solution a un problème n'était pas spécialement plus complexe - dans la mesure où le rendu des enregistrements en direct n'était finalement qu'une forme un chouia plus évoluée du `print()` ou des `System.out.println()` - mais c'était l'évolutivité des applications qui en prenait un coup: une grosse partie des tâches étaient dupliquées entre les différentes pages, et l'ajout d'une nouvelle fonctionnalité était relativement ardue.
Django (et d'autres cadriciels) résolvent ce problème en se basant ouvertement sur le principe de `Don't repeat yourself` footnote:[DRY].
Chaque morceau de code ne doit apparaitre qu'une seule fois, afin de limiter au maximum la redite (et donc, l'application d'un même correctif à différents endroits).
Le chemin parcouru par une requête est expliqué en (petits) détails ci-dessous.
.How it works
image::images/diagrams/django-how-it-works.png[]
*1. Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application*.
Ici, nous prenons l'exemple de l'URL fictive `https://gwift/wishes/91827`.
Lorsque cette URL "arrive" dans notre application, son point d'entrée se trouvera au niveau des fichiers `asgi.py` ou `wsgi.py`. Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu'elle va parcourir.
*Etape 0* - La première étape consiste à vérifier que cette URL répond à un schéma que nous avons défini dans le fichier `gwift/urls.py`.
*Etape 1* - Si ce n'est pas le cas, l'application n'ira pas plus loin et retournera une erreur à l'utilisateur.
*Etape 2* - Django va parcourir l'ensemble des _patterns_ présents dans le fichier `urls.py` et s'arrêtera sur le premier qui correspondra à la requête qu'il a reçue.
Ce cas est relativement trivial: la requête `/wishes/91827` a une correspondance au niveau de la ligne `path("wishes/<int:wish_id>` dans l'exemple ci-dessous.
Django va alors appeler la fonction footnote:[Qui ne sera pas toujours une fonction. Django s'attend à trouver un _callable_, c'est-à-dire n'importe quel élément qu'il peut appeler comme une fonction.] associée à ce _pattern_, c'est-à-dire `wish_details` du module `gwift.views`.
[source,python]
----
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details <1>
urlpatterns = [
path('admin/', admin.site.urls),
path("wishes/<int:wish_id>", wish_details), <2>
]
----
<1> Nous importons la fonction `wish_details` du module `gwift.views`
<2> Champomy et cotillons! Nous avons une correspondance avec `wishes/details/91827`
TODO: En fait, il faudrait quand même s'occuper du modèle ici.
TODO: et de la mise en place de l'administration, parce que nous en aurons besoin pour les étapes de déploiement.
[line-through]#Nous n'allons pas nous occuper de l'accès à la base de données pour le moment (nous nous en occuperons dans un prochain chapitre) et nous nous contenterons de remplir un canevas avec un ensemble de données.#
Le module `gwift.views` qui se trouve dans le fichier `gwift/views.py` peut ressembler à ceci:
[source,python]
----
[...]
from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
context = {
"user_name": "Bond,"
"user_first_name": "James",
"now": datetime.now()
}
return render(
request,
"wish_details.html",
context
)
----
Pour résumer, cette fonction permet:
. De construire un _contexte_, qui est représenté sous la forme d'un dictionnaire associant des clés à des valeurs. Les clés sont respectivement `user_name`, `user_first_name` et `now`, tandis que leurs valeurs respectives sont `Bond`, `James` et le `moment présent` footnote:[Non, pas celui d'Eckhart Tolle].
. Nous passons ensuite ce dictionnaire à un canevas, `wish_details.html`
. L'application du contexte sur le canevas nous donne un résultat.
[source,html]
----
<!-- fichier wish_details.html -->
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>👤 Hi!</h1>
<p>My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.</p>
<p>This page was generated at {{ now }}</p>
</body>
</html>
----
Après application de notre contexte sur ce template, nous obtiendrons ce document, qui sera renvoyé au navigateur de l'utilisateur qui aura fait la requête initiale:
[source,html]
----
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>👤 Hi!</h1>
<p>My name is Bond. James Bond.</p>
<p>This page was generated at 2027-03-19 19:47:38</p>
</body>
</html>
----
.Résultat
image::images/django/django-first-template.png[]
==== 12 facteurs et configuration globale
-> Faire le lien avec les settings
-> Faire le lien avec les douze facteurs
-> Construction du fichier setup.cfg
==== setup.cfg
(Repris de cookie-cutter-django)
[source,ini]
----
[flake8]
max-line-length = 120
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[pycodestyle]
max-line-length = 120
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[mypy]
python_version = 3.8
check_untyped_defs = True
ignore_missing_imports = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
plugins = mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = config.settings.test
[mypy-*.migrations.*]
# Django migrations should not produce any errors:
ignore_errors = True
[coverage:run]
include = khana/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin
----
=== Structure finale de notre environnement
Nous avons donc la structure finale pour notre environnement de travail:
[source,bash]
----
(gwift-env) fred@aerys:~/Sources/gwift$ tree .
.
├── gwift
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   ├── wish <1>
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   └── wsgi.py
├── Makefile
├── manage.py
├── README.md
├── requirements
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt
├── setup.cfg
└── tox.ini
----
=== Cookie cutter
Pfiou! Ca en fait des commandes et du boulot pour "juste" démarrer un nouveau projet, non? Sachant qu'en plus, nous avons dû modifier des fichiers, déplacer des dossiers, ajouter des dépendances, configurer une base de données, ...
Bonne nouvelle! Il existe des générateurs, permettant de démarrer rapidement un nouveau projet sans (trop) se prendre la tête. Le plus connu (et le plus personnalisable) est https://cookiecutter.readthedocs.io/[Cookie-Cutter], qui se base sur des canevas _type https://pypi.org/project/Jinja2/[Jinja2]_, pour créer une arborescence de dossiers et fichiers conformes à votre manière de travailler. Et si vous avez la flemme de créer votre propre canevas, vous pouvez utiliser https://cookiecutter-django.readthedocs.io[ceux qui existent déjà].
Pour démarrer, créez un environnement virtuel (comme d'habitude):
[source,bash]
----
λ python -m venv .venvs\cookie-cutter-khana
λ .venvs\cookie-cutter-khana\Scripts\activate.bat
(cookie-cutter-khana) λ pip install cookiecutter
Collecting cookiecutter
[...]
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 arrow-0.17.0 binaryornot-0.4.4 certifi-2020.12.5 chardet-4.0.0 click-7.1.2 cookiecutter-1.7.2 idna-2.10 jinja2-time-0.2.0 poyo-0.5.0 python-dateutil-2.8.1 python-slugify-4.0.1 requests-2.25.1 six-1.15.0 text-unidecode-1.3 urllib3-1.26.2
(cookie-cutter-khana) λ cookiecutter https://github.com/pydanny/cookiecutter-django
[...]
[SUCCESS]: Project initialized, keep up the good work!
----
Si vous explorez les différents fichiers, vous trouverez beaucoup de similitudes avec la configuration que nous vous proposions ci-dessus.
En fonction de votre expérience, vous serez tenté de modifier certains paramètres, pour faire correspondre ces sources avec votre utilisation ou vos habitudes.
NOTE: Il est aussi possible d'utiliser l'argument `--template`, suivie d'un argument reprenant le nom de votre projet (`<my_project>`), lors de l'initialisation d'un projet avec la commande `startproject` de `django-admin`, afin de calquer votre arborescence sur un projet existant.
La https://docs.djangoproject.com/en/stable/ref/django-admin/#startproject[documentation] à ce sujet est assez complète.
[source,bash]
----
django-admin.py startproject --template=https://[...].zip <my_project>
----

View File

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