From c4a7d94926d3603cbaa4cc04e3f43655f1627c5b Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Sat, 16 Apr 2022 20:53:08 +0200 Subject: [PATCH] Rework structure --- chapters/architecture.tex | 1182 ++++++++++++++++++++++++++++------- chapters/maintenability.tex | 911 +-------------------------- chapters/robustesse.tex | 0 chapters/tests.tex | 117 ++++ main.tex | 2 + parts/environment.tex | 7 + 6 files changed, 1096 insertions(+), 1123 deletions(-) create mode 100644 chapters/robustesse.tex create mode 100644 chapters/tests.tex diff --git a/chapters/architecture.tex b/chapters/architecture.tex index c65c368..4a38d04 100644 --- a/chapters/architecture.tex +++ b/chapters/architecture.tex @@ -1,267 +1,973 @@ -\chapter{Elements d'architecture} + + +\chapter{Eléments d'architecture} \begin{quote} -If you think good architecture is expensive, try bad architecture - ---- Brian Foote and Joseph Yoder + 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} \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: -Au delà des principes dont il est question plus haut, c'est dans les -ressources proposées et les cas démontrés que l'on comprend leur -intérêt: plus que de la définition d'une architecture adéquate, c'est -surtout dans la facilité de maintenance d'une application que ces -principes s'identifient. - -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 d'options 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 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. - -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. - -\section{Politiques et règles métiers} - -\section{Considérations sur les frameworks} - -\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. - ---- 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: - -\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 - 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} - -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. - -\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. - - \cite[182]{devops_handbook} -\end{quote} - -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. - ---- Robert C. Martin Clean Architecture -\end{quote} - -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 l'application. - -A noter que les services et les «~architectures orientées services~» ne -sont jamais qu'une définition d'implémentation des frontières, dans la -mesure où un service n'est jamais qu'une fonction appelée au travers -d'un protocole (rest, soap, \ldots\hspace{0pt}). Une application -monolotihique sera tout aussi fonctionnelle qu'une application découpée -en microservices. \cite[p. 243]{clean_architecture} - -\section{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{[}\url{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{[}\url{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: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\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. +\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 \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. Pire, vos -politiques et données métiers ne devraient pas avoir connaissance -\textbf{de la version} du moteur de base de données. +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. +\end{enumerate} -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). +\includegraphics{images/arch-comp-modules.png} -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. +\section{Modules} -\section{Conclusions} +\subsection{Single Responsility 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 \href{https://en.wikipedia.org/wiki/Unix_philosophy}{Philosophie Unix}, documentée par Doug McIlroy et qui demande de "\emph{faire une seule chose, mais de le faire bien}" \cite{unix_philosophy}. + +Selon ce principe, une classe ou un élément de programmation ne doit donc pas avoir plus d'une seule raison de changer. + +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: + +\begin{enumerate} + \item Le CFO (Chief Financial Officer) + \item Le CTO (Chief Technical Officer) + \item Le COO (Chief Operating Officer) +\end{enumerate} + +Chacun d'entre eux aura besoin de données et d'informations relatives à ces membres du personnel, et provenant donc d'une même source de données centralisée. +Mais chacun d'entre eux également besoin d'une représentation différente ou de traitements distincts. \cite{clean_architecture} + +Nous sommes d'accord qu'il s'agit à chaque fois de données liées aux employés; celles-ci vont cependant un cran plus loin et pourraient nécessiter des ajustements spécifiques en fonction de l'acteur 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 l'un d'entre eux. + +Dans le cas d'un élément de code centralisé, une modification induite par un des acteurs pourrait ainsi avoir un impact sur les données utilisées par les autres. + +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} + 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 """ + + {} + {} + {} + """.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) + ) + \end{minted} + \caption{Un convertisseur de document un peu bateau} +\end{listing} + + +Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, ...), il sera nécessaire de modifier la classe \texttt{Document}. +Ceci n'est: + +\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} +\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: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{Python} + class Document: + def __init__(self, title, content, published_at): + self.title = title + self.content = content + self.published_at = published_at + + class DocumentRenderer: + def render(self, document): + if format_type == "XML": + return """ + + {} + {} + {} + """.format( + self.title, + self.content, + self.published_at.isoformat() + ) + + if format_type == "Markdown": + import markdown + return markdown.markdown(self.content) + + raise ValueError("Format type '{}' is not known".format(format_type)) + \end{minted} + \caption{Isolation du rendu d'un document par rapport à sa modélisation} +\end{listing} + + +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. +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}. +Au niveau architectural, cet équivalent correspondra aux frontières. + +\subsection{Open-Closed} \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. + 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. +\end{quote} + +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: --- Antoine de Saint-Exupéry +\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 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. + +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 + Une classe \texttt{Customer}, pour laquelle la méthode \texttt{GetDiscount} ne renvoit rien; +\item + Une classe \texttt{SilverCustomer}, pour laquelle la méthode revoit une réduction de 10\%; +\item + Une classe \texttt{GoldCustomer}, pour laquelle la même méthode renvoit une réduction de 20\%. +\end{itemize} + +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: + +\begin{listing}[H] + \begin{minted}{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 + \end{minted} +\end{listing} + +A ceci: + +\begin{listing}[H] + \begin{minted}{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 + \end{minted} +\end{listing} + +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}. + +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: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{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 """ + + {} + {} + {} + """.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) + ) + \end{minted} +\end{listing} + +devient le suivant: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{Python} + class Renderer: + def render(self, document): + raise NotImplementedError + + class XmlRenderer(Renderer): + def render(self, document) + return """ + + {} + {} + {} + """.format( + document.title, + document.content, + document.published_at.isoformat() + ) + + class MarkdownRenderer(Renderer): + def render(self, document): + import markdown + return markdown.markdown(document.content) + \end{minted} + \caption{Notre convertisseur suit le principe Open-Closed} +\end{listing} + +Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons simplement une nouvelle classe de rendu qui héritera de \texttt{Renderer}. + +\subsection{Liskov Substitution} + +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: + +\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.). + +--- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia}. \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. -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} +\begin{quote} +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. -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, l’idée est de pousser le développement jusqu’au point où une décision pourrait devoir être faite. -A ce stade, l’architecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe. -Nous n’allons donc pas jusqu’au point où le service doit être créé (même s’il peut ne pas être nécessaire), ni à l’extrême au fait d’ignorer qu’un service pourrait être nécessaire, mais nous aboutissons à une forme de compromis. -Une forme d'application de la philosophie de René Descartes, où le fait de seulement envisager une possibilité ouvre un maximum de portes. +--- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia aussi} +\end{quote} -Avec cette approche, les composants seront déjà découplés au mieux. +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. -En terme de découpe, les composants peuvent l'être aux niveaux suivants: +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{itemize} -\item - \textbf{Code source} -\item - \textbf{Déploiement}, au travers de dll, jar, linked libraries, \ldots{} voire - au travers de threads ou de processus locaux. -\item - \textbf{Mise à disposition de services} -\end{itemize} +\begin{listing}[H] + \begin{minted}[tabsize=4]{Python} + class Duck: + def make_some_noise(self): + print("Kwak") + + def eat(self, thing): + if thing in ("plant", "insect", "seed", "seaweed", "fish"): + return "Yummy!" + raise IndigestionError("Arrrh") + + class Lion(Duck): + def make_some_noise(self): + print("Roaaar!") + \end{minted} + \caption{Un lion et un canard sont sur un bateau...} +\end{listing} -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: +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}. +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). -\begin{itemize} -\item - Les méthodes et fonctions -\item - Les classes -\item - Les composants -\item - Et des conseils plus généraux. -\end{itemize} +Pour revenir à nos exemples de rendus de documents, nous aurions pu faire hériter notre \texttt{MarkdownRenderer} de la classe \texttt{XmlRenderer}: -\subsection{Au niveau des méthodes et fonctions} +\begin{listing}[H] + \begin{minted}[tabsize=4]{Python} + class XmlRenderer: + def render(self, document) + return """ + + {} + {} + {} + """.format( + document.title, + document.content, + document.published_at.isoformat() + ) + + class MarkdownRenderer(XmlRenderer): + def render(self, document): + import markdown + return markdown.markdown(document.content) + \end{minted} + \caption{Le convertisseur Markdown hérite d'un convertisseur XML...} +\end{listing} -\begin{itemize} - \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). - 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. - \item - \textbf{Conservez de petites interfaces et signatures de fonctions/méthodes}. Quatre paramètres, pas plus. - Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester. - \end{itemize} - -\subsection{Au niveau des classes} +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} + class XmlRenderer: + def header(self): + return """""" + + def render(self, document) + return """{} + + {} + {} + {} + """.format( + self.header(), + document.title, + document.content, + document.published_at.isoformat() + ) + + class MarkdownRenderer(XmlRenderer): + def render(self, document): + import markdown + return markdown.markdown(document.content) + \end{minted} + \caption{... 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. + +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} + class Renderer: + def render(self, document): + raise NotImplementedError + + class XmlRenderer(Renderer): + def header(self): + return """""" + + def render(self, document) + return """{} + + {} + {} + {} + """.format( + self.header(), + document.title, + document.content, + document.published_at.isoformat() + ) + + class MarkdownRenderer(Renderer): + def render(self, document): + import markdown + return markdown.markdown(document.content) + \end{minted} + \caption{Définition d'héritage suivant le principe de substitution de Liskov} +\end{listing} + +\subsection{Interface Segregation} + +Le principe de ségrégation d'interface suggère de n'exposer que les opérations nécessaires à l'exécution d'un contexte. +Ceci limite la nécessité de recompiler un module, et évite ainsi d'avoir à redéployer l'ensemble d'une application alors qu'il suffirait de déployer un nouveau fichier JAR ou une DLL au bon endroit. + +\begin{quote} +The lesson here is that depending on something that carries baggage that you don't need can cause you troubles that you didn't except. +\end{quote} + +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: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{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; + } + } + \end{minted} + \caption{Une interface représenant une imprimante} +\end{listing} + +L’implémentation d’une imprimante multifonction aura tout son sens: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{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"); + } + } + \end{minted} + \caption{Une imprimante multi-fonction implémente les fonctionnalités d'une imprimante classique} +\end{listing} + +Tandis que l’implémentation d’une imprimante premier-prix ne servira pas à grand chose: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{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"); + } + } + \end{minted} + \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} + +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: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{java} + interface IPrinterPrinter + { + public abstract void printPage(); + } + + interface IPrinterScanner + { + public abstract void scanPage(); + } + + interface IPrinterFax + { + public abstract void faxPage(); + } + \end{minted} + \caption{Explosion des interfaces d'impression} +\end{listing} -\begin{itemize} - \item - \textbf{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 \texttt{UserNotificationsService} ne doit pas forcément se trouver embarqué dans une classe \texttt{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. - \end{itemize} +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. -\subsection{Au niveau des composants} +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: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{javascript} + /*! + * 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; + } + \end{minted} + \caption{Le module 'isOdd', en JavaScript} +\end{listing} + +Voire, son opposé, qui dépend évidemment du premier: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{javascript} + /*! + * 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); + }; + \end{minted} + \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: + +\begin{enumerate} + \item \texttt{is-even} + \item \texttt{is-number} +\end{enumerate} + +Imaginez la suite. + +\subsection{Dependency Inversion} + +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. +L'objectif 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. + +\begin{quote} +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} +\end{quote} + +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 \emph{middlewares} ou pour les connexions aux bases de données. +Lorsque nous écrivons ceci dans notre fichier de configuration, + +\begin{listing}[H] + \begin{minted}[tabsize=4]{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] + \end{minted} + \caption{La configuration des middlewares pour une application Django} +\end{listing} + +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. +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: + +\begin{listing}[H] + \begin{minted}[tabsize=4]{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 + \end{minted} + \caption{Création d'un nouveau middleware pour Django} +\end{listing} -\begin{itemize} - \item - \textbf{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, "\emph{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 \texttt{IFileTransfer} avec ses méthodes \texttt{put} et \texttt{get}, et non pas les détailsd'implémentation complet d'une classe \texttt{FtpFileTransfer} ou \texttt{SshFileTransfer}). - \item - \textbf{Conserver un bon balancement au niveau des composants}: évitez qu'un composant \textbf{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. - \end{itemize} +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}. -\subsection{De manière générale} +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/}}. -\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. - 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. - \end{itemize} - \ No newline at end of file +\begin{quote} +The database is really nothing more than a big bucket of bits where we store our data on a long term basis \cite[p. 281]{clean_architecture} +\end{quote} + +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, ... 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. + +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 desmoteurs de bases de données qui les stockent. +Ceci autorise une forme d'immunité entre les composants. + +\section{Composants} + +\begin{quote} + If you think good architecture is expensive, try bad architecture + + --- Brian Foote and Joseph Yoder + \end{quote} + + Au delà des principes dont il est question plus haut, c'est dans les + ressources proposées et les cas démontrés que l'on comprend leur + intérêt: plus que de la définition d'une architecture adéquate, c'est + surtout dans la facilité de maintenance d'une application que ces + principes s'identifient. + + 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 d'options 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 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. + + 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. + + \section{Politiques et règles métiers} + + \section{Considérations sur les frameworks} + + \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. + + --- 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: + + \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 + 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} + + 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. + + \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. + + \cite[182]{devops_handbook} + \end{quote} + + 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. + + --- Robert C. Martin Clean Architecture + \end{quote} + + 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 l'application. + + A noter que les services et les «~architectures orientées services~» ne + sont jamais qu'une définition d'implémentation des frontières, dans la + mesure où un service n'est jamais qu'une fonction appelée au travers + d'un protocole (rest, soap, \ldots\hspace{0pt}). Une application + monolotihique sera tout aussi fonctionnelle qu'une application découpée + en microservices. \cite[p. 243]{clean_architecture} + + \section{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{[}\url{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{[}\url{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: + + \begin{enumerate} + \def\labelenumi{\arabic{enumi}.} + \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. 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). + + 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. + + \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. + + -- 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. + 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} + + 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, l’idée est de pousser le développement jusqu’au point où une décision pourrait devoir être faite. + A ce stade, l’architecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe. + Nous n’allons donc pas jusqu’au point où le service doit être créé (même s’il peut ne pas être nécessaire), ni à l’extrême au fait d’ignorer qu’un service pourrait être nécessaire, mais nous aboutissons à une forme de compromis. + Une forme d'application de la philosophie de René Descartes, où le fait de seulement envisager une possibilité ouvre un maximum de portes. + + Avec cette approche, les composants seront déjà découplés au mieux. + + En terme de découpe, les composants peuvent l'être aux niveaux suivants: + + \begin{itemize} + \item + \textbf{Code source} + \item + \textbf{Déploiement}, au travers de dll, jar, linked libraries, \ldots{} voire + au travers de threads ou de processus locaux. + \item + \textbf{Mise à disposition de services} + \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: + + \begin{itemize} + \item + Les méthodes et fonctions + \item + Les classes + \item + Les composants + \item + Et des conseils plus généraux. + \end{itemize} + + \subsection{Au niveau des méthodes et fonctions} + + \begin{itemize} + \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). + 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. + \item + \textbf{Conservez de petites interfaces et signatures de fonctions/méthodes}. Quatre paramètres, pas plus. + Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester. + \end{itemize} + + \subsection{Au niveau des classes} + + + \begin{itemize} + \item + \textbf{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 \texttt{UserNotificationsService} ne doit pas forcément se trouver embarqué dans une classe \texttt{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. + \end{itemize} + + \subsection{Au niveau des composants} + + + \begin{itemize} + \item + \textbf{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, "\emph{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 \texttt{IFileTransfer} avec ses méthodes \texttt{put} et \texttt{get}, et non pas les détailsd'implémentation complet d'une classe \texttt{FtpFileTransfer} ou \texttt{SshFileTransfer}). + \item + \textbf{Conserver un bon balancement au niveau des composants}: évitez qu'un composant \textbf{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. + \end{itemize} + + \subsection{De manière générale} + + \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. + 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. + \end{itemize} + \ No newline at end of file diff --git a/chapters/maintenability.tex b/chapters/maintenability.tex index 02bc504..47e2089 100644 --- a/chapters/maintenability.tex +++ b/chapters/maintenability.tex @@ -7,18 +7,6 @@ The primary cost of maintenance is in spelunking and risk --- Robert C. Martin \end{quote} -\section{Poésie de la programmation} - -\begin{quote} -La poésie est "l'art d'évoquer et de suggérer les sensations, les impressions, les émotions les plus vives par l'union intense des sons, des rythmes, des harmonies, en particulier par les vers." - --- https://www.larousse.fr/dictionnaires/francais/po%C3%A9sie/61960 -\end{quote} - -Sans aller jusqu'à demander de développer vos algorithmes sur douze pieds, la programmation reste un art régit par un ensemble de bonnes pratiques, par des règles à respecter et par la nécessité de travailler avec d'autres personnes qui ont \sout{parfois} souvent une expérience, des compétences ou une approche différente. - -\section{12 facteurs} - Pour la méthode de travail et de développement, nous allons nous baser sur les \href{https://12factor.net/fr/}{The Twelve-factor App} - ou plus simplement les \textbf{12 facteurs}. @@ -53,7 +41,7 @@ précieux. Pour reprendre plus spécifiquement les différentes idées derrière cette méthode, nous avons: -\subsection{Une base de code unique, suivie par un contrôle de versions} +\section{Une base de code unique, suivie par un contrôle de versions} Chaque déploiement de l'application, et quel que soit l'environnement ciblé, se basera sur une source unique, afin de minimiser les différences que l'on pourrait trouver entre deux environnements d'un même projet. Git est reconnu dans l'industrie comme standard des systèmes de contrôles de versions, malgré une courbe d'apprentissage assez ardue. @@ -83,7 +71,7 @@ Ce cépôt n'est pas uniquement destiné à hébergé le code source, mais Tutoriaux \end{itemize} -\subsection{Déclarez explicitement et isolez les dépendances du projet} +\section{Déclaration explicite et isolation des dépendances} 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 @@ -110,7 +98,7 @@ 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 \footnote{Le paquet PyLint dépend par exemple d'Astroid; \href{https://github.com/PyCQA/pylint-django/issues/343}{en janvier 2022}, ce dernier a été mis à jour sans respecter le principe de versions sémantiques et introduisant une régression. PyLint spécifiait que sa dépendance avec Astroid devait respecter une version ~2.9. Lors de sa mise à jour en 2.9.1, Astroid a introduit un changement majeur, qui faisait planter Pylint. L'épinglage explicite aurait pu éviter ceci.} -\subsection{Sauver la configuration directement au niveau de l'environnement} +\section{Configuration applicative} Il faut éviter d'avoir à recompiler/redéployer l'application simplement parce que: @@ -138,8 +126,7 @@ l'un des paramètres utilisés risquerait de subir une modification ou s'il conc Le risque est de se retrouver avec une liste colossale de paramètres; pensez à leur assigner une variable par défaut. Par exemple, Gitea expose \href{https://docs.gitea.io/en-us/config-cheat-sheet/}{la liste suivante de paramètres}; il serait impossible de tous les configurer un à un avant de pouvoir démarrer une instance. -\subsection{Traiter les ressources externes comme des ressources -attachées} +\section{Ressources externes} Nous parlons de bases de données, de services de mise en cache, d'API externes, ... L'application doit être capable d'effectuer @@ -169,8 +156,7 @@ Nous serons ravis de pouvoir simplement modifier une chaîne sans avoir à recompiler ou redéployer les modifications. Ces ressources sont donc spécifiés grâce à des variables d'environnement, et chacune d'entre elles dispose également d'un \textbf{type}, afin de profiter d'une correspondance dynamique entre un moteur d'exécution et une information de configuration. -\subsection{Séparer proprement les phases de construction, de mise à -disposition et d'exécution} +\section{Séparation des phases de construction} \begin{enumerate} \item @@ -190,12 +176,9 @@ disposition et d'exécution} Parmi les solutions possibles, nous pourrions nous pourrions nous baser sur les \emph{releases} de Gitea, sur un serveur d'artefacts (\href{https://fr.wikipedia.org/wiki/Capistrano_(logiciel)}{Capistrano}), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, ...). -\subsection{Les processus d'exécution ne doivent rien connaître ou -conserver de l'état de l'application} +\section{Mémoire des processus d'exécution} -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é. +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é. Dit autrement, l'exécution de l'application ne doit pas dépendre de la présence d'une information stockée en mémoire ou sur disque. Pratiquement, si l'application devait rencontrer un problème, il est nécessaire qu'elle puisse redémarrer rapidement, éventuellement en étant déployée sur un autre serveur - par exemple suite à un problème matériel. @@ -204,8 +187,7 @@ Lors d'une initialisation ou réinitialisation, la solution consiste donc à jou 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. -\subsection{Autoriser la liaison d'un port de l'application à un port -du système hôte} +\section{Liaison des ports} Les applications 12-factors sont auto-contenues et peuvent fonctionner en autonomie totale. Elles doivent être joignables grâce à un mécanisme @@ -218,20 +200,14 @@ en HTTP ou via un autre protocole. L'applicatoin fonctionne de manière autonome et expose un port (ici, le 8000). Le serveur (= l'hôte) choisit d'appliquer une correspondance entre "son" port 443 et le port offert par l'application (8000). -\subsection{Faites confiance aux processus systèmes pour l'exécution -de l'application} +\section{Connaissance et confiance des processys systèmes} -Comme décrit plus haut (cf. \#6), l'application doit utiliser des -processus \emph{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 \emph{via} des processus Web; \emph{long-running} jobs -pour des processus asynchrones, \ldots\hspace{0pt} Si cela existe sur l'hôte hébergeant l'application, ne vous fatiguez pas: utilisez le. +Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus \emph{stateless} (sans état). Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des particuliers pour certaines tâches: requêtes HTTP \emph{via} des processus Web; \emph{long-running} jobs pour des processus asynchrones, ... +Si cela existe sur l'hôte hébergeant l'application, ne vous fatiguez pas: utilisez le. \includegraphics{images/12factors/process-types.png} -\subsection{Améliorer la robustesse de l'application grâce à des -arrêts élégants et à des démarrages rapides} +\section{Arrêts élégants et démarrages rapides} Par "arrêt élégant", nous voulons surtout éviter le fameux \texttt{kill\ -9\ \textless{}pid\textgreater{}} (ou équivalent), ou tout autre arrêt brutal d'un processus qui nécessiterait une intervention urgente du @@ -250,30 +226,21 @@ hôte, etc.). \includegraphics{images/12factors/process-type-chronology.png} -\subsection{Conserver les différents environnements aussi similaires -que possible, et limiter les divergences entre un environnement de -développement et de production} +\section{Similarité des environnements} -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. +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, au plus proche de la phase de développement, selon le principe de la corde d'Andon \cite[p. 140]{devops_handbook} \index{Andon} \footnote{Pour donner un exemple tout bête, SQLite utilise un -\href{https://www.sqlite.org/datatype3.html}{mécanisme de stockage -dynamique}, associée à la valeur plutôt qu'au schéma, \emph{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 \texttt{URLField} proposé par Django a une longeur -maximale par défaut de -\href{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 -\emph{staging}, si vous faites les choses un peu mieux) parce que les -données seront tronquées, et que cela ne plaira pas à la base de données. +\href{https://www.sqlite.org/datatype3.html}{mécanisme de stockage dynamique}, associée à la valeur plutôt qu'au schéma, \emph{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 \texttt{URLField} proposé par Django a une longeur maximale par défaut de \href{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 \emph{staging}, si vous faites les choses peu mieux) parce que les données seront tronquées, et que cela ne plaira pas à la base de données. Conserver des environements similaires limite ce genre de désagréments.} -\subsection{Gérer les journeaux d'évènements comme des flux} +\section{Journaux de flux évènementiels} Une application ne doit jamais se soucier de l'endroit où les évènements qui la concerne seront écrits, mais se doit simplement de les envoyer sur la sortie \texttt{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 \href{https://www.graylog.org/}{Greylog} ou \href{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. @@ -281,19 +248,12 @@ De cette manière, que nous soyons en développement sur le poste d'un développ 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. La différence entre ces deux points vous fera, au mieux, gagner plusieurs heures sur l'identification ou la résolution d'un problème. -\subsection{Isoler les tâches administratives} +\section{Isolation des tâches administratives} -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 -\emph{Read}, \emph{Eval}, \emph{Print} et \emph{Loop}) \index{REPL} à disposition (au -hasard: \href{https://pythonprogramminglanguage.com/repl/}{Python} ou -\href{https://kotlinlang.org/}{Kotlin}), ce qui facilite les étapes de -maintenance. +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 \emph{Read}, \emph{Eval}, \emph{Print} et \emph{Loop}) \index{REPL} à disposition (au hasard: \href{https://pythonprogramminglanguage.com/repl/}{Python} ou \href{https://kotlinlang.org/}{Kotlin}), ce qui facilite les étapes de maintenance. -\subsection{Conclusions} +\section{Conclusions} 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, @@ -325,822 +285,3 @@ Au fur et à mesure que le code est délibérément construit pour être mainten Centralisation de la configuration (\textbf{via} ZooKeeper, par exemple) \end{enumerate} - -\section{Robustesse et flexibilité du code source} - -\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} -\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: - -\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 -\end{enumerate} - -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. -\end{enumerate} - -\includegraphics{images/arch-comp-modules.png} - -\subsection{Single Responsility 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 \href{https://en.wikipedia.org/wiki/Unix_philosophy}{Philosophie Unix}, documentée par Doug McIlroy et qui demande de "\emph{faire une seule chose, mais de le faire bien}" \cite{unix_philosophy}. - -Selon ce principe, une classe ou un élément de programmation ne doit donc pas avoir plus d'une seule raison de changer. - -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: - -\begin{enumerate} - \item Le CFO (Chief Financial Officer) - \item Le CTO (Chief Technical Officer) - \item Le COO (Chief Operating Officer) -\end{enumerate} - -Chacun d'entre eux aura besoin de données et d'informations relatives à ces membres du personnel, et provenant donc d'une même source de données centralisée. -Mais chacun d'entre eux également besoin d'une représentation différente ou de traitements distincts. \cite{clean_architecture} - -Nous sommes d'accord qu'il s'agit à chaque fois de données liées aux employés; celles-ci vont cependant un cran plus loin et pourraient nécessiter des ajustements spécifiques en fonction de l'acteur 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 l'un d'entre eux. - -Dans le cas d'un élément de code centralisé, une modification induite par un des acteurs pourrait ainsi avoir un impact sur les données utilisées par les autres. - -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} - 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 """ - - {} - {} - {} - """.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) - ) - \end{minted} - \caption{Un convertisseur de document un peu bateau} -\end{listing} - - -Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, ...), il sera nécessaire de modifier la classe \texttt{Document}. -Ceci n'est: - -\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} -\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: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{Python} - class Document: - def __init__(self, title, content, published_at): - self.title = title - self.content = content - self.published_at = published_at - - class DocumentRenderer: - def render(self, document): - if format_type == "XML": - return """ - - {} - {} - {} - """.format( - self.title, - self.content, - self.published_at.isoformat() - ) - - if format_type == "Markdown": - import markdown - return markdown.markdown(self.content) - - raise ValueError("Format type '{}' is not known".format(format_type)) - \end{minted} - \caption{Isolation du rendu d'un document par rapport à sa modélisation} -\end{listing} - - -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. -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}. -Au niveau architectural, cet équivalent correspondra aux frontières. - -\subsection{Open-Closed} - -\begin{quote} - 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. -\end{quote} - -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: - -\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 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. - -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 - Une classe \texttt{Customer}, pour laquelle la méthode \texttt{GetDiscount} ne renvoit rien; -\item - Une classe \texttt{SilverCustomer}, pour laquelle la méthode revoit une réduction de 10\%; -\item - Une classe \texttt{GoldCustomer}, pour laquelle la même méthode renvoit une réduction de 20\%. -\end{itemize} - -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: - -\begin{listing}[H] - \begin{minted}{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 - \end{minted} -\end{listing} - -A ceci: - -\begin{listing}[H] - \begin{minted}{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 - \end{minted} -\end{listing} - -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}. - -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: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{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 """ - - {} - {} - {} - """.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) - ) - \end{minted} -\end{listing} - -devient le suivant: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{Python} - class Renderer: - def render(self, document): - raise NotImplementedError - - class XmlRenderer(Renderer): - def render(self, document) - return """ - - {} - {} - {} - """.format( - document.title, - document.content, - document.published_at.isoformat() - ) - - class MarkdownRenderer(Renderer): - def render(self, document): - import markdown - return markdown.markdown(document.content) - \end{minted} - \caption{Notre convertisseur suit le principe Open-Closed} -\end{listing} - -Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons simplement une nouvelle classe de rendu qui héritera de \texttt{Renderer}. - -\subsection{Liskov Substitution} - -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: - -\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.). - ---- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia}. -\end{quote} - -\begin{quote} -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. - ---- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia aussi} -\end{quote} - -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: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{Python} - class Duck: - def make_some_noise(self): - print("Kwak") - - def eat(self, thing): - if thing in ("plant", "insect", "seed", "seaweed", "fish"): - return "Yummy!" - raise IndigestionError("Arrrh") - - class Lion(Duck): - def make_some_noise(self): - print("Roaaar!") - \end{minted} - \caption{Un lion et un canard sont sur un bateau...} -\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}. -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 \texttt{MarkdownRenderer} de la classe \texttt{XmlRenderer}: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{Python} - class XmlRenderer: - def render(self, document) - return """ - - {} - {} - {} - """.format( - document.title, - document.content, - document.published_at.isoformat() - ) - - class MarkdownRenderer(XmlRenderer): - def render(self, document): - import markdown - return markdown.markdown(document.content) - \end{minted} - \caption{Le convertisseur Markdown hérite d'un convertisseur XML...} -\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: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{Python} - class XmlRenderer: - def header(self): - return """""" - - def render(self, document) - return """{} - - {} - {} - {} - """.format( - self.header(), - document.title, - document.content, - document.published_at.isoformat() - ) - - class MarkdownRenderer(XmlRenderer): - def render(self, document): - import markdown - return markdown.markdown(document.content) - \end{minted} - \caption{... 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. - -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} - class Renderer: - def render(self, document): - raise NotImplementedError - - class XmlRenderer(Renderer): - def header(self): - return """""" - - def render(self, document) - return """{} - - {} - {} - {} - """.format( - self.header(), - document.title, - document.content, - document.published_at.isoformat() - ) - - class MarkdownRenderer(Renderer): - def render(self, document): - import markdown - return markdown.markdown(document.content) - \end{minted} - \caption{Définition d'héritage suivant le principe de substitution de Liskov} -\end{listing} - -\subsection{Interface Segregation} - -Le principe de ségrégation d'interface suggère de n'exposer que les opérations nécessaires à l'exécution d'un contexte. -Ceci limite la nécessité de recompiler un module, et évite ainsi d'avoir à redéployer l'ensemble d'une application alors qu'il suffirait de déployer un nouveau fichier JAR ou une DLL au bon endroit. - -\begin{quote} -The lesson here is that depending on something that carries baggage that you don't need can cause you troubles that you didn't except. -\end{quote} - -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: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{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; - } - } - \end{minted} - \caption{Une interface représenant une imprimante} -\end{listing} - -L’implémentation d’une imprimante multifonction aura tout son sens: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{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"); - } - } - \end{minted} - \caption{Une imprimante multi-fonction implémente les fonctionnalités d'une imprimante classique} -\end{listing} - -Tandis que l’implémentation d’une imprimante premier-prix ne servira pas à grand chose: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{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"); - } - } - \end{minted} - \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} - -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: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{java} - interface IPrinterPrinter - { - public abstract void printPage(); - } - - interface IPrinterScanner - { - public abstract void scanPage(); - } - - interface IPrinterFax - { - public abstract void faxPage(); - } - \end{minted} - \caption{Explosion des interfaces d'impression} -\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. - -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: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{javascript} - /*! - * 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; - } - \end{minted} - \caption{Le module 'isOdd', en JavaScript} -\end{listing} - -Voire, son opposé, qui dépend évidemment du premier: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{javascript} - /*! - * 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); - }; - \end{minted} - \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: - -\begin{enumerate} - \item \texttt{is-even} - \item \texttt{is-number} -\end{enumerate} - -Imaginez la suite. - -\subsection{Dependency Inversion} - -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. -L'objectif 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. - -\begin{quote} -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} -\end{quote} - -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 \emph{middlewares} ou pour les connexions aux bases de données. -Lorsque nous écrivons ceci dans notre fichier de configuration, - -\begin{listing}[H] - \begin{minted}[tabsize=4]{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] - \end{minted} - \caption{La configuration des middlewares pour une application Django} -\end{listing} - - -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. -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: - -\begin{listing}[H] - \begin{minted}[tabsize=4]{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 - \end{minted} - \caption{Création d'un nouveau middleware pour Django} -\end{listing} - - -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/}}. - -\begin{quote} -The database is really nothing more than a big bucket of bits where we store our data on a long term basis \cite[p. 281]{clean_architecture} -\end{quote} - -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, ... 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. - -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 desmoteurs de bases de données qui les stockent. -Ceci autorise une forme d'immunité entre les composants. - -\section{Tests unitaires et d'intégration} - -\begin{quote} -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. - --- Robert C. Martin, Clean Architecture -\end{quote} - -\section{Complexité cyclomatique\index{McCabe}} - -La \href{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, cette condition peut être évalue à VRAI ou à FAUX. -L'exécution du code dispose donc de deux embranchements, correspondant chacun à un résultat de cette condition. - -Le code suivant \autoref{cyclomatic-simple-code} a une complexité cyclomatique 1; il s'agit du cas le plus simple que nous pouvons implémenter: l'exécution du code rentre dans la fonction (il y a un seul embranchement), et aucun bloc conditionnel n'est présent sur son chemin. -La complexité reste de 1. - -\begin{listing}[!hbpt] - \begin{minted}{Python} - from datetime import date - - def print_current_date(): - print(date.today()) - \end{minted} - \caption{Une version ultra-simple de l'affichage du jour de la semaine} - \label{cyclomatic-simple-code} -\end{listing} - -Si nous complexifions cette fonction en vérifiant (par exemple) le jour de la semaine, nous aurons notre embranchement initial (l'impression à l'écran de la date du jour), mais également un second embranchement qui vérifiera si cette date correspond à un lundi: - -\begin{listing}[!h] - \begin{minted}{Python} - from datetime import date - - def print_current_date_if_monday(): - if date.today().weekday() == 0: - print("Aujourd'hui, c'est lundi!") - print(date.today()) - \end{minted} - \caption{Ajout d'une fonctionnalité essentielle et totalement indispensable} -\end{listing} - -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. -Cette complexité est liée à deux concepts: - -\begin{itemize} - \item \textbf{La lisibilité du code}: au plus la complexité cyclomatique sera élevée, au plus le code sera compliqué à comprendre en première instance. Il sera composé de plusieurs conditions, éventuellement imbriquées, il débordera probablement de la hauteur que votre écran sera capable d'afficher - \item \textbf{Les tests unitaires}: pour nous assurer d'une couverture de code correcte, il sera nécessaire de couvrir tous les embranchements présentés. Connaître la complexité permet de savoir combien de tests devront être écrits pour assurer une couverture complète de tous les cas pouvant se présenter. -\end{itemize} - -\subsection{Lisibilité du code} - -Il est important de noter que refactoriser un bloc, par exemple en extrayant une méthode, n'améliore pas la complexité cyclomatique globale de l'application. -L'amélioration que nous visons ici est une amélioration \textbf{locale}, qui facilite la lecture d'un bloc spécifique, et pas d'un programme complet. - -"Améliorons" notre code ci-dessous, pour lui ajouter la possibilité de gérer les autres jours de la semaine: - -\begin{listing}[!ht] - \begin{minted}{Python} - from datetime import date - - def print_current_date(): - if date.today().weekday() == 0: - print("Lundi") - elif date.today().weekday() == 1: - print("Mardi") - elif date.today().weekday() == 2: - print("Mercredi") - elif date.today().weekday() == 3: - print("Jeudi") - elif date.today().weekday() == 4: - print("Vendredi") - elif date.today().weekday() == 5: - print("Samedi") - elif date.today().weekday() == 6: - print("Dimanche") - print(date.today()) - \end{minted} - \caption{Un code un peu nul avec une complexité cyclomatique qui l'est tout autant} - \label{Impression du jour de la semaine, version naïve} -\end{listing} - -La complexité de ce code est évaluée à 8, même si la complexité effective ne sera que de 7. -Extraire une méthode à partir de ce bloc pourra réduire la complexité de la fonction \mintinline{python}{print_current_date} n'améliorera rien et ne fera que déplacer le problème. -Une solution serait de passer par un dictionnaire, de façon à ramener la complexité à 1: - -\begin{listing}[!ht] - \begin{minted}{python} - from datetime import date - - def print_current_date(): - DAYS_OF_WEEK = { - 0: "Lundi", - 1: "Mardi", - 2: "Mercredi", - 3: "Jeudi", - 4: "Vendredi", - 5: "Samedi", - 6: "Dimanche" - } - - print(DAYS_OF_WEEK.get(date.today().weekday())) - print(date.today()) - \end{minted} - \caption{La même version, avec une complexité réduite à 1} -\end{listing} - - -\subsection{Tests unitaires} - -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. - diff --git a/chapters/robustesse.tex b/chapters/robustesse.tex new file mode 100644 index 0000000..e69de29 diff --git a/chapters/tests.tex b/chapters/tests.tex new file mode 100644 index 0000000..036bbf6 --- /dev/null +++ b/chapters/tests.tex @@ -0,0 +1,117 @@ + +\chapter{Tests unitaires et d'intégration} + +\begin{quote} +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. + +-- Robert C. Martin, Clean Architecture +\end{quote} + +\section{Complexité cyclomatique\index{McCabe}} + +La \href{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, cette condition peut être évalue à VRAI ou à FAUX. +L'exécution du code dispose donc de deux embranchements, correspondant chacun à un résultat de cette condition. + +Le code suivant \autoref{cyclomatic-simple-code} a une complexité cyclomatique 1; il s'agit du cas le plus simple que nous pouvons implémenter: l'exécution du code rentre dans la fonction (il y a un seul embranchement), et aucun bloc conditionnel n'est présent sur son chemin. +La complexité reste de 1. + +\begin{listing}[!hbpt] + \begin{minted}{Python} + from datetime import date + + def print_current_date(): + print(date.today()) + \end{minted} + \caption{Une version ultra-simple de l'affichage du jour de la semaine} + \label{cyclomatic-simple-code} +\end{listing} + +Si nous complexifions cette fonction en vérifiant (par exemple) le jour de la semaine, nous aurons notre embranchement initial (l'impression à l'écran de la date du jour), mais également un second embranchement qui vérifiera si cette date correspond à un lundi: + +\begin{listing}[!h] + \begin{minted}{Python} + from datetime import date + + def print_current_date_if_monday(): + if date.today().weekday() == 0: + print("Aujourd'hui, c'est lundi!") + print(date.today()) + \end{minted} + \caption{Ajout d'une fonctionnalité essentielle et totalement indispensable} +\end{listing} + +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. +Cette complexité est liée à deux concepts: + +\begin{itemize} + \item \textbf{La lisibilité du code}: au plus la complexité cyclomatique sera élevée, au plus le code sera compliqué à comprendre en première instance. Il sera composé de plusieurs conditions, éventuellement imbriquées, il débordera probablement de la hauteur que votre écran sera capable d'afficher + \item \textbf{Les tests unitaires}: pour nous assurer d'une couverture de code correcte, il sera nécessaire de couvrir tous les embranchements présentés. Connaître la complexité permet de savoir combien de tests devront être écrits pour assurer une couverture complète de tous les cas pouvant se présenter. +\end{itemize} + +\section{Lisibilité du code} + +Il est important de noter que refactoriser un bloc, par exemple en extrayant une méthode, n'améliore pas la complexité cyclomatique globale de l'application. +L'amélioration que nous visons ici est une amélioration \textbf{locale}, qui facilite la lecture d'un bloc spécifique, et pas d'un programme complet. + +"Améliorons" notre code ci-dessous, pour lui ajouter la possibilité de gérer les autres jours de la semaine: + +\begin{listing}[H] + \begin{minted}{Python} + from datetime import date + + def print_current_date(): + if date.today().weekday() == 0: + print("Lundi") + elif date.today().weekday() == 1: + print("Mardi") + elif date.today().weekday() == 2: + print("Mercredi") + elif date.today().weekday() == 3: + print("Jeudi") + elif date.today().weekday() == 4: + print("Vendredi") + elif date.today().weekday() == 5: + print("Samedi") + elif date.today().weekday() == 6: + print("Dimanche") + print(date.today()) + \end{minted} + \caption{Un code un peu nul avec une complexité cyclomatique qui l'est tout autant} + \label{Impression du jour de la semaine, version naïve} +\end{listing} + +La complexité de ce code est évaluée à 8, même si la complexité effective ne sera que de 7. +Extraire une méthode à partir de ce bloc pourra réduire la complexité de la fonction \mintinline{python}{print_current_date} n'améliorera rien et ne fera que déplacer le problème. +Une solution serait de passer par un dictionnaire, de façon à ramener la complexité à 1: + +\begin{listing}[H] + \begin{minted}{python} + from datetime import date + + def print_current_date(): + DAYS_OF_WEEK = { + 0: "Lundi", + 1: "Mardi", + 2: "Mercredi", + 3: "Jeudi", + 4: "Vendredi", + 5: "Samedi", + 6: "Dimanche" + } + + print(DAYS_OF_WEEK.get(date.today().weekday())) + print(date.today()) + \end{minted} + \caption{La même version, avec une complexité réduite à 1} +\end{listing} + + +\section{Tests unitaires} + +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. \ No newline at end of file diff --git a/main.tex b/main.tex index 4809b5b..6f74df5 100644 --- a/main.tex +++ b/main.tex @@ -48,6 +48,8 @@ \include{chapters/architecture.tex} +\include{chapters/tests.tex} + \include{chapters/python.tex} \chapter{Démarrer un nouveau projet} diff --git a/parts/environment.tex b/parts/environment.tex index e4ed1bf..5355aed 100644 --- a/parts/environment.tex +++ b/parts/environment.tex @@ -83,3 +83,10 @@ Dans une version plus manuelle, cela pourrait se résumer à ces trois technologie existe encore\ldots\hspace{0pt}). \end{enumerate} +\begin{quote} + La poésie est "l'art d'évoquer et de suggérer les sensations, les impressions, les émotions les plus vives par l'union intense des sons, des rythmes, des harmonies, en particulier par les vers." + + -- https://www.larousse.fr/dictionnaires/francais/po%C3%A9sie/61960 + \end{quote} + + Sans aller jusqu'à demander de développer vos algorithmes sur douze pieds, la programmation reste un art régit par un ensemble de bonnes pratiques, par des règles à respecter et par la nécessité de travailler avec d'autres personnes qui ont \sout{parfois} souvent une expérience, des compétences ou une approche différente. \ No newline at end of file