Réécriture de l'Architecture

This commit is contained in:
Gregory Trullemans 2023-04-21 13:26:38 +02:00
parent 1461ee2fa1
commit 6fa6fa3f7d
1 changed files with 86 additions and 113 deletions

View File

@ -1,40 +1,31 @@
\chapter{Eléments d'architecture}
\begin{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" \emph{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}
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}
\end{quote}
Les principes SOLID, introduit par Robert C. Martin dans les années 2000 pour orienter le développement de modules, sont les suivants:
Les principes SOLID, introduit par Robert C. Martin dans les années 2000 pour orienter le développement de modules, sont les suivants :
\begin{enumerate}
\item
\textbf{SRP} - Single responsibility principle - Principe de Responsabilité Unique
\item
\textbf{OCP} - Open-closed principle
\item
\textbf{LSP} - Liskov Substitution
\item
\textbf{ISP} - Interface ségrégation principle
\item
\textbf{DIP} - Dependency Inversion Principle
\item \textbf{SRP} - Single responsibility principle - Principe de Responsabilité Unique
\item \textbf{OCP} - Open-closed principle
\item \textbf{LSP} - Liskov Substitution
\item \textbf{ISP} - Interface ségrégation principle
\item \textbf{DIP} - Dependency Inversion Principle
\end{enumerate}
Des équivalents à ces directives existent au niveau des composants, puis au niveau architectural:
Des équivalents à ces directives existent au niveau des composants, puis au niveau architectural :
\begin{enumerate}
\item
Reuse/release équivalence principle,
\item
\textbf{CCP} - Common Closure Principle,
\item
\textbf{CRP} - Common Reuse Principle.
\item Reuse/release équivalence principle,
\item \textbf{CCP} - Common Closure Principle,
\item \textbf{CRP} - Common Reuse Principle.
\end{enumerate}
\includegraphics{images/arch-comp-modules.png}
\section{Modules}
\subsection{Single Responsility Principle}
@ -46,7 +37,7 @@ Selon ce principe, une classe ou un élément de programmation ne doit donc pas
Plutôt que de centraliser le maximum de code à un seul endroit ou dans une seule classe par convenance ou commodité \footnote{Aussi appelé \emph{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:
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 :
\begin{enumerate}
\item Le CFO (Chief Financial Officer)
@ -62,8 +53,8 @@ Dès que possible, identifiez les différents acteurs et demandeurs, en vue de p
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 \texttt{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 \texttt{render} permet également de proposer (très grossièrement) un type de sortie et un format de contenu: \texttt{XML} ou \texttt{Markdown}.
Vous trouverez ci-dessous une classe \texttt{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 \texttt{render} permet également de proposer (très grossièrement) un type de sortie et un format de contenu : \texttt{XML} ou \texttt{Markdown}.
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
@ -98,15 +89,15 @@ Une méthode \texttt{render} permet également de proposer (très grossièrement
\end{listing}
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, ...), il sera nécessaire de modifier la classe \texttt{Document}.
Ceci n'est:
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, \ldots), il sera nécessaire de modifier la classe \texttt{Document}.
Ceci n' :
\begin{enumerate}
\item Ni intuitif: \emph{ce n'est pas le document qui doit savoir dans quels formats il peut être converti}
\item Ni conseillé: \emph{lorsque nous aurons quinze formats différents à gérer, il sera nécessaire d'avoir autant de conditions dans cette méthode}
\item Ni intuitif : \emph{ce n'est pas le document qui doit savoir dans quels formats il peut être converti}
\item Ni conseillé : \emph{lorsque nous aurons quinze formats différents à gérer, il sera nécessaire d'avoir autant de conditions dans cette méthode}
\end{enumerate}
En suivant le principe de responsabilité unique, une bonne pratique consiste à créer une nouvelle classe de rendu pour chaque type de format à gérer:
En suivant le principe de responsabilité unique, une bonne pratique consiste à créer une nouvelle classe de rendu pour chaque type de format à gérer :
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
@ -143,7 +134,7 @@ En suivant le principe de responsabilité unique, une bonne pratique consiste à
A présent, lorsque nous devrons ajouter un nouveau format de prise en charge, il nous suffira de modifier la classe \texttt{DocumentRenderer}, sans que la classe \texttt{Document} ne soit impactée.
En même temps, le jour où une instance de type \texttt{Document} sera liée à un champ \texttt{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.
Un autre exemple consiterait à faire communiquer une méthode avec une base de données: ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement spécifique (emplacement sur un disque, ...): cette action doit être prise en compte par une autre classe (ou un autre concept ou composant), 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.
Un autre exemple consiterait à faire communiquer une méthode avec une base de données : ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement spécifique (emplacement sur un disque, \ldots) : cette action doit être prise en compte par une autre classe (ou un autre concept ou composant), 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.
L'équivalent du principe de responsabilité unique au niveau des composants sera le \texttt{Common Closure Principle} \index{CCP}.
@ -157,19 +148,20 @@ Au niveau architectural, cet équivalent correspondra aux frontières.
L'objectif est de rendre le système facile à étendre, en limitant l'impact qu'une modification puisse avoir.
Reprendre notre exemple de modélisation de documents parle de lui-même:
Reprendre notre exemple de modélisation de documents parle de lui-même :
\begin{enumerate}
\item Des données que nous avons converties dans un format spécifique pourraient à présent devoir être présentées dans une page web.
\item
Des données que nous avons converties dans un format spécifique pourraient à présent devoir être présentées dans une page web.
\item Et demain, ce sera dans un document PDF.
\item Et après demain, dans un tableur Excel.
\end{enumerate}
La source de ces données reste la même (au travers d'une couche de présentation): c'est leur mise en forme qui diffère à chaque fois.
L'application n'a pas à connaître les détails d'implémentation: elle doit juste permettre une forme d'extension, sans avoir à appliquer quelconque modification en son cœur.
La source de ces données reste la même (au travers d'une couche de présentation) : c'est leur mise en forme qui diffère à chaque fois.
L'application n'a pas à connaître les détails d'implémentation : elle doit juste permettre une forme d'extension, sans avoir à appliquer quelconque modification en 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 comme nous l'avons initisée plus tôt 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 prendre un nouvel exemple, nous pourrions ainsi définir trois classes:
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 comme nous l'avons initisée plus tôt 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 prendre un nouvel exemple, nous pourrions ainsi définir trois classes :
\begin{itemize}
\item
@ -183,7 +175,7 @@ Pour prendre un nouvel exemple, nous pourrions ainsi définir trois classes:
Si nous devions rencontrer un nouveau type de client, il nous suffira de créer une nouvelle sous-classe, implémentant la réduction que nous souhaitons lui offrir.
Ceci évite d'avoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction d'une variable ou d'un paramètre - ici, le type de client.
Nous passerions ainsi de ceci:
Nous passerions ainsi de ceci :
\begin{listing}[H]
\begin{minted}{Python}
@ -204,7 +196,7 @@ Nous passerions ainsi de ceci:
\end{minted}
\end{listing}
A ceci:
A ceci :
\begin{listing}[H]
\begin{minted}{Python}
@ -228,11 +220,11 @@ A ceci:
En anglais, dans le texte : "\emph{Putting in simple words, the ``Customer'' class is now closed for any new modification but it's open for extensions when new customer types are added to the project.}".
\textbf{En résumé}: nous fermons la classe \texttt{Customer} à toute modification, mais nous ouvrons la possibilité de créer de nouvelles extensions en ajoutant de nouveaux types héritant de \texttt{Customer}.
\textbf{En résumé} : nous fermons la classe \texttt{Customer} à toute modification, mais nous ouvrons la possibilité de créer de nouvelles extensions en ajoutant de nouveaux types héritant de \texttt{Customer}.
De cette manière, nous simplifions également la maintenance de la méthode \texttt{get\_discount}, dans la mesure où elle dépend directement du type dans lequel elle est implémentée.
Nous pouvons également appliquer ceci à notre exemple sur les rendus de document, où le code suivant:
Nous pouvons également appliquer ceci à notre exemple sur les rendus de document, où le code suivant :
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
@ -265,7 +257,7 @@ Nous pouvons également appliquer ceci à notre exemple sur les rendus de docume
\end{minted}
\end{listing}
devient le suivant:
devient le suivant :
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
@ -300,7 +292,7 @@ Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons simplement
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 ou n'ait pas besoin de certaines méthodes, alors que celles-ci sont disponibles sa classe parente.
Mathématiquement, ce principe peut être défini de la manière suivante:
Mathématiquement, ce principe peut être défini de la manière suivante :
\begin{quote}
{[}\ldots\hspace{0pt}{]} 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.).
@ -318,7 +310,7 @@ Then q(y) should be provable for objects y of type S, where S is a subtype of T.
Ce n'est donc pas parce qu'une classe \textbf{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 \texttt{make\_some\_noise} et une méthode \texttt{eat} sur une classe \texttt{Duck}, et qu'une réflexion avancée (et sans doute un peu alcoolisée) nous dit que "\emph{Puisqu'un \texttt{Lion} fait aussi du bruit, faisons le hériter de notre classe `Canard`"}, nous allons nous retrouver avec ceci:
Petit exemple pratique : si nous définissons une méthode \texttt{make\_some\_noise} et une méthode \texttt{eat} sur une classe \texttt{Duck}, et qu'une réflexion avancée (et sans doute un peu alcoolisée) nous dit que "\emph{Puisqu'un \texttt{Lion} fait aussi du bruit, faisons le hériter de notre classe `Canard`"}, nous allons nous retrouver avec ceci :
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
@ -335,7 +327,7 @@ Petit exemple pratique: si nous définissons une méthode \texttt{make\_some\_no
def make_some_noise(self):
print("Roaaar!")
\end{minted}
\caption{Un lion et un canard sont sur un bateau...}
\caption{Un lion et un canard sont sur un bateau\ldots}
\end{listing}
Le principe de substitution de Liskov suggère qu'une classe doit toujours pouvoir être considérée comme une instance de sa classe parente, et \textbf{doit pouvoir s'y substituer}.
@ -343,7 +335,7 @@ Dans notre exemple, cela signifie que nous pourrons tout à fait accepter qu'un
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 \texttt{MarkdownRenderer} de la classe \texttt{XmlRenderer}:
Pour revenir à nos exemples de rendus de documents, nous aurions pu faire hériter notre \texttt{MarkdownRenderer} de la classe \texttt{XmlRenderer} :
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
@ -365,10 +357,10 @@ Pour revenir à nos exemples de rendus de documents, nous aurions pu faire héri
import markdown
return markdown.markdown(document.content)
\end{minted}
\caption{Le convertisseur Markdown hérite d'un convertisseur XML...}
\caption{Le convertisseur Markdown hérite d'un convertisseur XML\ldots}
\end{listing}
Si nous décidons à un moment d'ajouter une méthode d'entête au niveau de notre classe de rendu XML, notre rendu en Markdown héritera irrémédiablement de cette même méthode:
Si nous décidons à un moment d'ajouter une méthode d'entête au niveau de notre classe de rendu XML, notre rendu en Markdown héritera irrémédiablement de cette même méthode :
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
@ -397,9 +389,9 @@ Si nous décidons à un moment d'ajouter une méthode d'entête au niveau de not
\caption{\ldots~ et il a mal à l'entête}
\end{listing}
Le code ci-dessus ne porte pas à conséquence \footnote{Pas immédiatement, en tout cas...}, mais dès que nous invoquerons la méthode \texttt{header()} sur une instance de type \texttt{MarkdownRenderer}, nous obtiendrons un bloc de déclaration XML (\texttt{\textless{}?xml\ version\ =\ "1.0"?\textgreater{}}) pour un fichier Markdown, ce qui n'aura aucun sens.
Le code ci-dessus ne porte pas à conséquence \footnote{Pas immédiatement, en tout cas\ldots}, mais dès que nous invoquerons la méthode \texttt{header()} sur une instance de type \texttt{MarkdownRenderer}, nous obtiendrons un bloc de déclaration XML (\texttt{\textless{}?xml\ version\ =\ "1.0"?\textgreater{}}) pour un fichier Markdown, ce qui n'aura aucun sens.
En revenant à notre proposition d'implémentation, suite au respect d'Open-Closed, une solution serait de n'implémenter la méthode \texttt{header()} qu'au niveau de la classe \texttt{XmlRenderer}:
En revenant à notre proposition d'implémentation, suite au respect d'Open-Closed, une solution serait de n'implémenter la méthode \texttt{header()} qu'au niveau de la classe \texttt{XmlRenderer} :
\begin{listing}[H]
\begin{minted}[tabsize=4]{Python}
@ -444,7 +436,7 @@ The lesson here is that depending on something that carries baggage that you don
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, et permet donc à nos clients de ne pas dépendre de méthodes dont ils n'ont pas besoin.
GNU/Linux Magazine \cite[pp. 37-42]{gnu_linux_mag_hs_104} propose un exemple d'interface permettant d'implémenter une imprimante:
GNU/Linux Magazine \cite[pp. 37-42]{gnu_linux_mag_hs_104} propose un exemple d'interface permettant d'implémenter une imprimante :
\begin{listing}[H]
\begin{minted}[tabsize=4]{java}
@ -468,7 +460,7 @@ GNU/Linux Magazine \cite[pp. 37-42]{gnu_linux_mag_hs_104} propose un exemple d'i
\caption{Une interface représenant une imprimante}
\end{listing}
Limplémentation dune imprimante multifonction aura tout son sens:
Limplémentation dune imprimante multifonction aura tout son sens :
\begin{listing}[H]
\begin{minted}[tabsize=4]{java}
@ -498,7 +490,7 @@ Limplémentation dune imprimante multifonction aura tout son sens:
\caption{Une imprimante multi-fonction implémente les fonctionnalités d'une imprimante classique}
\end{listing}
Tandis que limplémentation dune imprimante premier-prix ne servira pas à grand chose:
Tandis que limplémentation dune imprimante premier-prix ne servira pas à grand chose :
\begin{listing}[H]
\begin{minted}[tabsize=4]{java}
@ -528,7 +520,7 @@ Tandis que limplémentation dune imprimante premier-prix ne servira pas à
\caption{Une imprimante premier prix ne peut qu'imprimer, mais expose malgré tout des fonctions (absentes) de scanner et d'envoi par fax}
\end{listing}
Lobjectif est donc de découpler ces différentes fonctionnalités en plusieurs interfaces bien spécifiques, implémentant chacune une opération isolée:
Lobjectif est donc de découpler ces différentes fonctionnalités en plusieurs interfaces bien spécifiques, implémentant chacune une opération isolée :
\begin{listing}[H]
\begin{minted}[tabsize=4]{java}
@ -551,12 +543,12 @@ Lobjectif est donc de découpler ces différentes fonctionnalités en plusieu
\end{listing}
Cette réflexion s'applique à 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 d'embarquer le must absolu qui peut faire 1000x fonctions de plus que n'importe quel autre produit, alors que seules deux d'entre elles seront nécessaires.
Cette réflexion s'applique à n'importe quel composant : votre système d'exploitation, les librairies et dépendances tierces, les variables déclarées, \ldots
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 d'embarquer le must absolu qui peut faire 1000 fonctions de plus que n'importe quel autre produit, alors que seules deux d'entre elles seront nécessaires.
En Python, ce comportement est inféré lors de l'exécution, et donc pas vraiment d'application pour ce 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 juste mettre à jour une DLL ou un JAR sans que cela n'ait d'impact sur le reste de l'application.
En Python, ce comportement est inféré lors de l'exécution, et donc pas vraiment d'application pour ce 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 juste mettre à jour une DLL ou un JAR sans que cela n'ait d'impact sur le reste de l'application.
Il est ainsi possible de trouver quelques horreurs, et ce dans tous les langages:
Il est ainsi possible de trouver quelques horreurs, et ce dans tous les langages :
\begin{listing}[H]
\begin{minted}[tabsize=4]{javascript}
@ -592,7 +584,7 @@ Il est ainsi possible de trouver quelques horreurs, et ce dans tous les langages
\caption{Le module 'isOdd', en JavaScript}
\end{listing}
Voire, son opposé, qui dépend évidemment du premier:
Voire, son opposé, qui dépend évidemment du premier :
\begin{listing}[H]
\begin{minted}[tabsize=4]{javascript}
@ -613,8 +605,8 @@ Voire, son opposé, qui dépend évidemment du premier:
\caption{Le module 'isEven', en JavaScript, qui dépend du premier}
\end{listing}
Il ne s'agit que d'un simple exemple, mais qui tend à une seule chose: gardez les choses simples (et, éventuellement, stupides). \index{KISS}
Dans l'exemple ci-dessus, l'utilisation du module \texttt{is-odd} requière déjà deux dépendances:
Il ne s'agit que d'un simple exemple, mais qui tend à une seule chose : gardez les choses simples (et, éventuellement, stupides). \index{KISS}
Dans l'exemple ci-dessus, l'utilisation du module \texttt{is-odd} requière déjà deux dépendances :
\begin{enumerate}
\item \texttt{is-even}
@ -660,9 +652,9 @@ Lorsque nous écrivons ceci dans notre fichier de configuration,
Django ira simplement récupérer chacun de ces middlewares, qui répondent chacun à une
\href{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: l'interface exige une signature particulière, tandis que l'implémentation effective n'est réalisée qu'au niveau le plus bas.
Il n'y a donc pas de magie : l'interface exige une signature particulière, tandis que l'implémentation effective n'est réalisée qu'au niveau le plus bas.
C'est ensuite 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 \emph{middleware}, il nous suffirait d'implémenter de nouvelles fonctionnalités au niveau du code code suivant et de l'ajouter dans la configuration de l'application, au niveau de la liste des middlewares actifs:
Pour créer un nouveau \emph{middleware}, il nous suffirait d'implémenter de nouvelles fonctionnalités au niveau du code code suivant et de l'ajouter dans la configuration de l'application, au niveau de la liste des middlewares actifs :
\begin{listing}[H]
\begin{minted}[tabsize=4]{python}
@ -688,7 +680,7 @@ Pour créer un nouveau \emph{middleware}, il nous suffirait d'implémenter de no
Dans d'autres projets écrits en Python, ce type de mécanisme peut être implémenté relativement facilement en utilisant les modules \href{https://docs.python.org/3/library/importlib.html}{importlib} et la fonction \texttt{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{\url{http://howfuckedismydatabase.com/}}.
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{\url{http://howfuckedismydatabase.com/}}.
D'un point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, s'il s'agit d'un disque magnétique, de mémoire vive, \ldots~ en fait, on ne devrait même pas savoir s'il y a un disque du tout.
Et Django le fait très bien pour nous.
@ -710,14 +702,14 @@ Ceci autorise une forme d'immunité entre 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.
\end{quote}
Plus spécifiquement, la définition exacte devient celle-ci:
Plus spécifiquement, la définition exacte devient celle-ci :
\begin{quote}
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.
\end{quote}
Que l'on résumera ainsi: "dont depend on things you dont need", comme nous l'avons déjà expliqué plus haut.
Que l'on résumera ainsi : "dont depend on things you dont need", comme nous l'avons déjà expliqué plus haut.
\subsection{Stable dependency principle}
@ -749,11 +741,10 @@ Une bonne architecture va rendre le système facile à lire, facile à développ
Un des autres objectifs d'une bonne architecture consiste à se garder le plus d'options possibles, et à se concentrer sur les détails (le type de base de données, la conception concrète, \ldots) le plus tard possible, tout en conservant la politique principale en ligne de mire.
Ceci permet de délayer les choix techniques à «~plus tard~», ce qui permet également de concrétiser ces choix en ayant le plus d'informations possibles \cite[pp.137-141]{clean_architecture}
Derrière une bonne architecture, il y a aussi un investissement quant aux ressources qui seront nécessaires à faire évoluer l'application: ne
pas investir dès qu'on le peut va juste lentement remplir la case de la dette technique.
Derrière une bonne architecture, il y a aussi un investissement quant aux ressources qui seront nécessaires à faire évoluer l'application : ne pas investir dès qu'on le peut va juste lentement remplir la case de la dette technique.
Une architecture ouverte et pouvant être étendue n'a d'intérêt que si le développement est suivi et que les gestionnaires (et architectes) s'engagent à économiser du temps et de la qualité lorsque des changements seront demandés pour l'évolution du projet : ne pas investir à améliorer l'architecture dès que ce sera possible fera lentement (mais sûrement!) dériver la base de code vers une augmentation de la dette technique.
Faire évoluer correctement l'architecture d'un projet demande une bonne expérience, mais également un bon sens de l'observation, un investissement non négligeable en attention portée aux détails et de la patience:
Faire évoluer correctement l'architecture d'un projet demande une bonne expérience, mais également un bon sens de l'observation, un investissement non négligeable en attention portée aux détails et de la patience :
\begin{quote}
This is not a one time decision.
@ -763,7 +754,8 @@ Faire évoluer correctement l'architecture d'un projet demande une bonne expéri
You pay attention as the system evolves.
You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries don't exist.
At that point, you weight the costs of implementing those boundaries versus the cost of ignoring them and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing becomes less than the cost of ignoring.
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.
\end{quote}
\section{Politiques et règles métiers}
@ -772,29 +764,22 @@ Faire évoluer correctement l'architecture d'un projet demande une bonne expéri
\begin{quote}
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.
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.
--- Robert C. Martin Clean Architecture
\end{quote}
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:
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 :
\begin{itemize}
\item
Votre modèle métier sera largement couplé avec le type de base de données (relationnelle, indépendamment
\item
Votre couche de présentation sera surtout disponible au travers d'un navigateur
\item
Les droits d'accès et permissions seront en grosse partie gérés par le frameworks
\item
La sécurité dépendra de votre habilité à suivre les versions
\item Votre modèle métier sera largement couplé avec le type de base de données (relationnelle, indépendamment ???? -de quoi-)
\item Votre couche de présentation sera surtout disponible au travers d'un navigateur
\item Les droits d'accès et permissions seront en grosse partie gérés par le frameworks
\item La sécurité dépendra de votre habilité à suivre les versions
\item
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é
\end{itemize}
@ -803,15 +788,8 @@ Le point à comprendre ici n'est pas que "Django, c'est mal", mais qu'une fois q
Cette décision ne sera pas irrévocable, mais difficile à contourner.
\begin{quote}
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.
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[182]{devops_handbook}
\end{quote}
@ -819,9 +797,7 @@ Cette décision ne sera pas irrévocable, mais difficile à contourner.
Ceci dit, Django compense ses contraintes en proposant énormément de flexibilité et de fonctionnalités \textbf{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.
\begin{quote}
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.
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.
--- Robert C. Martin Clean Architecture
\end{quote}
@ -838,16 +814,14 @@ Django est un framework qui évolue, et qui a pu présenter certains problèmes
Les \href{https://docs.djangoproject.com/en/2.0/releases/2.0/}{Releases Notes} de Django 2.0 date de décembre 2017; parmi ces notes, l'une d'elles cite l'abandon du support d'\href{https://docs.djangoproject.com/en/2.0/releases/2.0/\#dropped-support-for-oracle-11-2}{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:
Réécrit, cela signifie que :
\begin{enumerate}
\item
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
\item
Tandis que si votre moteur est géré par une version ultérieure, le framework pourra être mis à jour.
\item 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
\item Tandis que si votre moteur est géré par une version ultérieure, le framework pourra être mis à jour.
\end{enumerate}
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.
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 \textbf{de la version} du moteur de base de données.
En conclusion, le choix d'une version d'un moteur technique (\textbf{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).
@ -857,14 +831,14 @@ Ce point sera rediscuté par la suite, notamment au niveau de l'épinglage des v
\section{Conclusions}
\begin{quote}
La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais lorsqu'il n'y a plus rien à retirer.
La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais lorsqu'il n'y a plus rien à retirer.
-- Antoine de Saint-Exupéry
-- Antoine de Saint-Exupéry
\end{quote}
Il est impossible de se projeter dans le futur d'une application: il est impossible d'imaginer les modifications qui seront demandées par les utilisateurs, de se projeter dans l'évolution d'un langage, dans les nécessités d'intégration de certaines librairies ou dans le support-même de certaines fonctionnalités par les navigateurs Web.
Il est impossible de se projeter dans le futur d'une application : il est impossible d'imaginer les modifications qui seront demandées par les utilisateurs, de se projeter dans l'évolution d'un langage, dans les nécessités d'intégration de certaines librairies ou dans le support-même de certaines fonctionnalités par les navigateurs Web.
Ce sont des choses qui viennent avec l'expérience (ou avec la tordure d'esprit \footnote{Si, ça existe}).
Cela rejoint le fameux "YAGNI\index{YAGNI}" dont nous parlions plus tôt: il est inutile de vouloir développer absolument toutes les fonctionnalités qui pourraient un jour pouvoir être utilisées ou souhaitées, car cela complexifiera autant le code, que les déploiement, l'utilisabilité ou la compréhension que les utilisateurs pourront avoir de votre application. \cite{rework}
Cela rejoint le fameux "YAGNI\index{YAGNI}" dont nous parlions plus tôt : il est inutile de vouloir développer absolument toutes les fonctionnalités qui pourraient un jour pouvoir être utilisées ou souhaitées, car cela complexifiera autant le code, que les déploiement, l'utilisabilité ou la compréhension que les utilisateurs pourront avoir de votre application. \cite{rework}
Il est impossible d'imaginer ou de se projeter dans tous les éléments qui pourraient devoir être modifiés après que votre développement ait été livrée.
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ù une décision pourrait devoir être faite.
@ -874,18 +848,17 @@ Une forme d'application de la philosophie de René Descartes, où le fait de seu
Avec cette approche, les composants seront déjà découplés au mieux.
Les composants peuvent être découpés au niveau:
Les composants peuvent être découpés au niveau :
\begin{itemize}
\item
\textbf{Du code source}, via des modules, paquets, dépendances, \ldots
\item \textbf{Du code source}, via des modules, paquets, dépendances, \ldots
\item
\textbf{Du déploiement ou de l'exécution}, au travers de dll, jar, linked libraries, \ldots, voire au travers de threads ou de processus locaux.
\item
\textbf{Via la mise à disposition de nouveaux services}, lorsqu'il est plus intuitif de contacter un nouveau point de terminaison que d'intégrer de force de nouveaux concepts dans une base de code existante.
\end{itemize}
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:
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 :
\begin{itemize}
\item Les méthodes et fonctions
@ -900,13 +873,13 @@ Cette section se base sur deux ressources principales \cite{maintainable_softwar
\item
\textbf{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).
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.
\item
\textbf{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.
\item
\textbf{N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc.}: 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.
\textbf{N'écrivez votre code qu'une seule fois : évitez les duplications, copie, etc.} : 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.
\item
\textbf{Conservez de petites interfaces et signatures de fonctions/méthodes}.
Quatre paramètres, pas plus.
@ -945,7 +918,7 @@ Dans la même veine, faites en sorte que les dépendances aillent toutes "dans l
\begin{itemize}
\item
\textbf{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 faisantdu refactoring (intensif), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux.
Pour cela, il faut (activement) passer du temps à réduire la taille du code existant : soit en faisantdu 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.
\item
\textbf{Automatiser les tests}, en ajoutant un environnement d'intégration continue dès le début du projet et en faisant vérifier par des outils automatiques tous les points ci-dessus.