Jekyll2024-01-22T14:01:38+00:00https://techblog.deepki.com//feed.xmlDeepki TechblogBienvenue sur le blog de l'équipe technique de Deepki.
Deepki : l'équipe techniquePrincipes SOLID et comment les appliquer en Python2024-01-22T00:00:00+00:002024-01-22T00:00:00+00:00https://techblog.deepki.com//SOLID-in-python<p>Les principes SOLID, ont été introduits pour la première fois dans les années 2000 par <a href="https://en.wikipedia.org/wiki/Robert_C._Martin">Robert C. Martin</a> dans son article “Design Principles and Design Patterns (<a href="https://staff.cs.utu.fi/~jounsmed/doos_06/material/DesignPrinciplesAndPatterns.pdf">PDF</a>)” et il en parle ensuite dans son livre “<a href="/being-a-professional-dev/">Clean Code: A Handbook of Agile Software Craftsmanship</a>”.
Ces principes ont été développés pour aider à concevoir du code plus maintenable, robuste et flexible.</p>
<p>Souvent associés à des langages orientés objet, on va voir aujourd’hui comment ils fonctionnent et comment on pourrait les appliquer au langage python.</p>
<p>Les 5 principes qui créent l’acronyme SOLID sont:</p>
<ol>
<li>Single Responsibility Principle (SRP)</li>
<li>Open/Closed Principle (OCP)</li>
<li>Liskov Substitution Principle (LSP)</li>
<li>Interface Segregation Principle (ISP)</li>
<li>Dependency Inversion Principle (DIP)</li>
</ol>
<h2 id="single-responsibility-principle-srp-">Single Responsibility Principle (SRP) :</h2>
<p>Le Principe de Responsabilité Unique énonce qu’une classe ne devrait avoir qu’une seule responsabilité. Par exemple, si nous avons une classe qui gère la lecture et l’écriture dans un fichier, il serait préférable de la diviser en deux classes distinctes : une pour la lecture et une pour l’écriture.</p>
<p>Ce principe peut également s’étendre aux fonctions afin qu’elles soient moins complexes et plus lisibles.</p>
<p>Dans le cas suivant, nous avons une fonction qui a 2 responsabilités : la création de l’utilisateur et l’envoi de l’e-mail.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create_user_and_send_email</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-></span> <span class="n">Mapping</span><span class="p">:</span>
<span class="p">...</span>
</code></pre></div></div>
<p>Si on veut respecter le principe SRP, il nous faudrait 2 fonctions avec chacune sa responsabilité</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">def</span> <span class="nf">create_user</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">):</span>
<span class="p">...</span>
<span class="k">def</span> <span class="nf">send_email</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">):</span>
<span class="p">...</span>
</code></pre></div></div>
<h2 id="openclosed-principle-ocp-">Open/Closed Principle (OCP) :</h2>
<p>Le Principe d’Ouverture/Fermeture préconise que les classes ou les fonctions doivent être ouvertes à l’extension, mais fermées à la modification. Ça limite le risque de générer des bugs en ajoutant des nouvelles fonctionnalités</p>
<p>Dans l’exemple suivant, nous avons une classe <code class="language-plaintext highlighter-rouge">Widget</code> qui ne respecte pas le principe OCP:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Widget</span><span class="p">():</span>
<span class="k">def</span> <span class="nf">get_dashboard_widget_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="n">Mapping</span><span class="p">:</span>
<span class="p">...</span>
<span class="k">def</span> <span class="nf">get_pdf_widget_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="n">Mapping</span><span class="p">:</span>
<span class="p">...</span>
</code></pre></div></div>
<p>Plutôt que d’utiliser des classes distinctes pour chaque type de widget, toutes les logiques de création des widget sont regroupées dans une seule classe. Pour ajouter un nouveau type de widget, il faut modifier la classe existante, violant ainsi le principe OCP.
En appliquant l’OCP, nous pouvons diviser cette classe en deux qui pourraient hériter de <code class="language-plaintext highlighter-rouge">Widget</code>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Widget</span><span class="p">(</span><span class="n">ABC</span><span class="p">):</span>
<span class="o">@</span><span class="n">abstractmethod</span>
<span class="k">def</span> <span class="nf">get_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="n">Mapping</span><span class="p">:</span>
<span class="p">...</span>
<span class="k">class</span> <span class="nc">WidgetDashBoard</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="n">Mapping</span><span class="p">:</span>
<span class="p">...</span>
<span class="k">class</span> <span class="nc">PDFWidget</span><span class="p">(</span><span class="n">Widget</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="n">Mapping</span><span class="p">:</span>
<span class="p">...</span>
</code></pre></div></div>
<p>Ainsi si le besoin d’un nouveau type de widget arrive, les autres widgets ne seront pas impactés.</p>
<h2 id="liskov-substitution-principle-lsp-">Liskov Substitution Principle (LSP) :</h2>
<p>Le Principe de Substitution de Liskov stipule que les objets d’une classe dérivée doivent pouvoir remplacer les objets de la classe de base sans affecter la cohérence du programme. En Python, cela signifie que les sous-classes doivent être compatibles avec les classes de base</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Fluid</span><span class="p">():</span>
<span class="k">def</span> <span class="nf">get_meter</span><span class="p">():</span>
<span class="p">...</span>
<span class="k">def</span> <span class="nf">get_consumption_m3</span><span class="p">()</span>
<span class="p">...</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Water</span><span class="p">(</span><span class="n">Fluid</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_meter</span><span class="p">():</span>
<span class="p">...</span>
<span class="k">def</span> <span class="nf">get_consumption_m3</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="s">"water consumption: 20m3"</span><span class="p">)</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Electricity</span><span class="p">(</span><span class="n">Fluid</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_meter</span><span class="p">():</span>
<span class="p">...</span>
<span class="k">def</span> <span class="nf">get_consumption_m3</span><span class="p">()</span>
<span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="s">"no m3 consumption for electricity"</span><span class="p">)</span>
</code></pre></div></div>
<p>Dans cet exemple la classe <code class="language-plaintext highlighter-rouge">Electricity</code> ne respecte pas le LSP car il ne respecte pas le contrat de la classe de base et a un comportement qui n’est pas cohérent, tandis que la classe <code class="language-plaintext highlighter-rouge">Water</code> le respecte.
Pour des cas comme celui-ci il faudrait repenser la classe <code class="language-plaintext highlighter-rouge">Fluid</code> d’une manière plus générique, qui n’implique pas l’usage des unités par example <code class="language-plaintext highlighter-rouge">get_consumption</code> plutôt que <code class="language-plaintext highlighter-rouge">get_consumption_m3</code>
ou, en utilisant le principe ISP, faire 2 classes <code class="language-plaintext highlighter-rouge">FluidKwh</code> et <code class="language-plaintext highlighter-rouge">FluidM3</code> par exemple.</p>
<h2 id="interface-segregation-principle-isp-">Interface Segregation Principle (ISP) :</h2>
<p>Le Principe de Ségrégation d’Interface déclare qu’une classe qui utilise une interface ne doit pas être forcée de dépendre des méthodes qu’elle n’utilise pas. Il faut éviter les interfaces trop larges et préférer les interfaces concises et plus petites.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">BuildingService</span><span class="p">(</span><span class="n">ABC</span><span class="p">):</span>
<span class="o">@</span><span class="n">abstractmethod</span>
<span class="k">def</span> <span class="nf">get_building_destination</span><span class="p">():</span>
<span class="p">...</span>
<span class="o">@</span><span class="n">abstractmethod</span>
<span class="k">def</span> <span class="nf">get_consumption_electricty</span><span class="p">()</span>
<span class="p">...</span>
<span class="o">@</span><span class="n">abstractmethod</span>
<span class="k">def</span> <span class="nf">get_consumption_water</span><span class="p">()</span>
<span class="p">...</span>
<span class="o">@</span><span class="n">abstractmethod</span>
<span class="k">def</span> <span class="nf">get_consumption_fuel</span><span class="p">()</span>
<span class="p">...</span>
</code></pre></div></div>
<p>Dans cette classe, la méthode <code class="language-plaintext highlighter-rouge">get_consumption_fuel</code> pourra présenter un problème, car tous les bâtiments n’utilisent pas le <code class="language-plaintext highlighter-rouge">fuel</code> comme moyen de chauffage. Ainsi, toutes les classes qui vont l’utiliser vont être forcées d’implémenter cette méthode.
Pour respecter l’ISP on pourrait créer une classe plus simple avec des méthodes plus génériques qui pourraient simplifier l’usage de l’interface et n’importe quelle classe pourrait l’implémenter</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">BuildingService</span><span class="p">(</span><span class="n">ABC</span><span class="p">):</span>
<span class="o">@</span><span class="n">abstractmethod</span>
<span class="k">def</span> <span class="nf">get_building_destination</span><span class="p">():</span>
<span class="p">...</span>
<span class="o">@</span><span class="n">abstractmethod</span>
<span class="k">def</span> <span class="nf">get_consumption</span><span class="p">(</span><span class="n">fluid</span><span class="p">):</span>
<span class="p">...</span>
</code></pre></div></div>
<h2 id="dependency-inversion-principle-dip">Dependency Inversion Principle (DIP):</h2>
<p>Le principe d’Inversion de Dépendance préconise que les modules de haut niveau ne devraient pas dépendre des modules de bas niveau, mais plutôt des abstractions. En Python, cela peut être réalisé en utilisant des interfaces ou des classes abstraites pour définir les contrats entre les différentes parties du code. Cela permet de réduire les dépendances directes et facilite le remplacement des composants sans affecter le reste du système.</p>
<p>Dans l’exemple suivant, <code class="language-plaintext highlighter-rouge">MessageSender.message_service</code> utilise la classe abstraite, <code class="language-plaintext highlighter-rouge">MessageService</code>, et non l’implémentation concrète :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">class</span> <span class="nc">MessageService</span><span class="p">(</span><span class="n">ABC</span><span class="p">):</span>
<span class="o">@</span><span class="n">abstractmethod</span>
<span class="k">def</span> <span class="nf">send</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">):</span>
<span class="p">...</span>
<span class="k">class</span> <span class="nc">MessageSender</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message_service</span><span class="p">:</span> <span class="n">MessageService</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">message_service</span> <span class="o">=</span> <span class="n">message_service</span>
<span class="k">def</span> <span class="nf">send</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">message_service</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</code></pre></div></div>
<p>Quand on utilise une classe abstraite pour définir le contrat, ça nous permet d’utiliser toute implémentation de <code class="language-plaintext highlighter-rouge">MessageService</code>, par exemple pour les tests on pourrait envoyer une implémentation sous la forme d’un mock pour éviter la dépendance avec <code class="language-plaintext highlighter-rouge">MessageService</code> et ainsi pouvoir tester uniquement les méthodes de la classe <code class="language-plaintext highlighter-rouge">MessageSender</code>.</p>
<p>Autre exemple : si on a plusieurs implémentations de <code class="language-plaintext highlighter-rouge">MessageService</code>, on peut utiliser <code class="language-plaintext highlighter-rouge">MessageSender</code> sans que cela représente plusieurs implémentations de <code class="language-plaintext highlighter-rouge">MessageSender</code>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Utiliser les principes SOLID n’est pas quelque chose de très compliqué et son usage n’est pas uniquement limité à des langages qui sont principalement orientés objets, comme c’est le cas des langages comme le Java.
Avec SOLID on peut créer des systèmes plus flexibles, extensibles et faciles à maintenir.
L’application cohérente de ces principes favorise la modularité, la réutilisabilité et la compréhensibilité du code, contribuant ainsi à la création de logiciels de haute qualité.</p>
<p>De mon point de vue, ce qui rend complexe l’usage de Python est qu’on peut se retrouver très facilement avec des fichiers ou des fonctions énormes qui cherchent à faire beaucoup de choses différentes.
En utilisant les principes SOLID (notamment le <em>Single Responsibility Principle</em> et l’<em>Open/Closed Principle</em>), on est capable d’identifier les responsabilités de chacun des composants : réduisant ainsi la complexité et l’ajout potentiel de bugs lors des prochaines évolutions du code.</p>
<p>Je trouve aussi que les principes LSP, ISP, DIP sont plus difficiles à utiliser en python notamment, car la flexibilité du langage permet de créer des fonctions “libres”, qui n’appartient pas à une classe. En conséquence, quand on n’a pas eu d’expérience sur un langage plus strict, par exemple, on pourrait avoir plus de mal à identifier sur quels endroits on a besoin de créer un service et d’utiliser les principes, et sur quels, on peut se permettre d’utiliser juste des fonctions.</p>
<p>Dans des grandes bases des codes, utiliser ces principes même si dans une équipe les classes ne sont pas beaucoup utilisées, peut aider à avoir des bases de code qui sont compréhensibles facilement et des features scalables, en utilisant l’esprit de ces principes pour les utiliser sur des fonctions ou sur l’architecture du code.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design">SOLID: The First 5 Principles of Object Oriented Design</a></li>
<li><a href="https://www.baeldung.com/solid-principles">A Solid Guide to SOLID Principles</a></li>
<li>“Clean Code: A Handbook of Agile Software Craftsmanship” par Robert C. Martin.</li>
<li>“Design Patterns: Elements of Reusable Object-Oriented Software” par Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides.</li>
</ul>Mariana ROLDAN VELEZLes principes SOLID, ont été introduits pour la première fois dans les années 2000 par Robert C. Martin dans son article “Design Principles and Design Patterns (PDF)” et il en parle ensuite dans son livre “Clean Code: A Handbook of Agile Software Craftsmanship”. Ces principes ont été développés pour aider à concevoir du code plus maintenable, robuste et flexible.Pydantic, la révolution de python ?2024-01-16T00:00:00+00:002024-01-16T00:00:00+00:00https://techblog.deepki.com//pydantic-aka-le-gamechanger<p>J’ai eu l’opportunité, en février 2023, d’assister à une <a href="https://archive.fosdem.org/2023/schedule/event/rust_how_Pydantic_v2_leverages_rusts_superpowers/">conférence</a> de <a href="https://github.com/samuelcolvin">Samuel Colvin</a> au <a href="https://fosdem.org/2024/">FOSDEM</a> à Bruxelles dans le cadre de mes fonctions chez Deepki. Il présentait une librairie de sa création : <a href="https://docs.Pydantic.dev/latest/">Pydantic</a>.
Je vais, à travers cet article de blog, vous présenter cette librairie, pourquoi je pense qu’elle révolutionne le langage Python et l’utilisation que l’on en fait chez Deepki.</p>
<h1 id="alors-cest-quoi-pydantic-">Alors, c’est quoi Pydantic ?</h1>
<p>Une image vaut bien mille mots il paraît, de la même façon qu’une slide de l’auteur fait encore mieux l’affaire. Pydantic est tout simplement une librairie de validation de données.
Cette librairie permet donc de créér des modèles de données et de tout simplement s’assurer de leur conformité.</p>
<figure>
<img src="/assets/img/Pydantic/Pydantic-usage.png" alt="La librairie Pydantic et son immense popularité : une croissance exponentielle" class="" />
<figcaption>Usage de Pydantic</figcaption>
</figure>
<meta property="og:image:alt" content="Usage de Pydantic" />
<meta property="og:image" content="https://techblog.deepki.com/assets/img/Pydantic/Pydantic-usage.png" />
<meta property="og:image:alt" content="La librairie Pydantic et son immense popularité : une croissance exponentielle" />
<p>Nous l’utilisons désormais dans mon équipe et je trouve maintenant complètement impensable de s’en séparer.</p>
<h3 id="a-quelle-problématique-cette-librairie-répond-elle-">A quelle problématique cette librairie répond-elle ?</h3>
<p>Voici un exemple très concret et courant d’utilisation d’une dataclasse pour définir une structure de données. L’intention est très bonne, le développeur a accès à un type bien défini et se sent en confiance lorsqu’il va l’utiliser.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">dataclass</span>
<span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
<span class="nb">id</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">name</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="n">email</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
</code></pre></div></div>
<p>Or, voici qu’un développeur décide d’utiliser cette classe pour implémenter une nouvelle fonctionnalité. Il se trouve qu’il fait quelques erreurs lors de son développement, qui aboutissent à un code complètement bogué. Ce cas représente très bien un des inconvénients d’un langage dont le typage est dynamique : une variable ne se voit pas assigner un type, seulement sa valeur en possède. Python ne remonte aucun avertissement sur ce genre d’utilisation nativement.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="mi">42</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"Pablo"</span><span class="p">,</span> <span class="n">email</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">email</span><span class="p">)</span>
<span class="c1">#> 42
</span></code></pre></div></div>
<p>Il est possible de partiellement répondre à cette problématique en utilisant un outil d’analyse statique (permet de détecter les erreurs avant le lancement du code) comme <a href="https://mypy-lang.org/">mypy</a>. L’outil ne répond cependant pas totalement à notre problématique car il est intimement lié à la quantité et précision de typage fait par les développeurs. Par exemple utiliser le type <code class="language-plaintext highlighter-rouge">Iterator</code> est valide pour les types <code class="language-plaintext highlighter-rouge">list</code>, <code class="language-plaintext highlighter-rouge">tuple</code>, <code class="language-plaintext highlighter-rouge">sets</code> ou encore <code class="language-plaintext highlighter-rouge">dict</code>, ceci peut laisser passer des erreurs si ce n’est pas voulu.</p>
<h1 id="comment-on-utilise-t-on-pydantic-">Comment on utilise-t-on Pydantic ?</h1>
<p>Commençons par créer notre premier modèle et voyons comment l’utiliser. Si vous êtes amateur de python, vous remarquerez très vite la ressemblance avec la syntaxe d’une <a href="https://docs.python.org/3/library/dataclasses.html">dataclasse</a> (Je vous conseille par ailleurs l’excellent <a href="/typeddict-vs-dataclass-in-python/">article de Pierre Assemat</a> sur les dataclasses)</p>
<h2 id="définissons-un-modèle-de-données">Définissons un modèle de données</h2>
<p>Notre point de départ, définir un modèle de données représentant une entité métier.<br />
Voici un exemple définissant un utilisateur, observez la facilité de lecture qu’offre Pydantic, même un néophyte est capable de comprendre de quoi il retourne. En effet Pydantic ne s’appuie que sur des concepts déjà connus des développeurs : le typage et les classes. Nous n’avons pas de mise à niveau à faire pour faire nos opérations basiques.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Annotated</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
<span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">PositiveInt</span><span class="p">,</span> <span class="n">EmailStr</span><span class="p">,</span> <span class="n">PastDate</span><span class="p">,</span> <span class="n">HttpUrl</span><span class="p">,</span> <span class="n">UUID4</span><span class="p">,</span> <span class="n">field_validator</span>
<span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">UUID4</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">email</span><span class="p">:</span> <span class="n">EmailStr</span>
<span class="n">birth_date</span><span class="p">:</span> <span class="n">PastDate</span>
<span class="n">websites</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">HttpUrl</span><span class="p">]</span> <span class="o">|</span> <span class="bp">None</span>
<span class="n">signup</span><span class="p">:</span> <span class="n">datetime</span> <span class="o">|</span> <span class="bp">None</span>
</code></pre></div></div>
<p>Notez d’ailleurs l’utilisation de types qui n’existent actuellement pas dans Python. Pydantic nous fournit toute une ribambelle de <a href="https://docs.Pydantic.dev/2.5/api/types/">types communément utilisés</a> qui sont utilisables directement, permettant de garder la base de code toujours aussi lisible (ça ressemble à de l’anglais).</p>
<h2 id="ajoutons-des-règles-de-validation-sur-mesure">Ajoutons des règles de validation sur mesure</h2>
<p>Parfois, les types de Python ou Pydantic ne suffisent pas à faire respecter des contraintes métier sur nos données. On peut alors créér des validateurs sur mesure. Nous définissons donc ici un validateur pour le nom de l’utilisateur : nous souhaitons qu’il appartienne à une liste prédéfinie et le formatter (décorateur <code class="language-plaintext highlighter-rouge">@field_validator</code>).</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">UUID4</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">email</span><span class="p">:</span> <span class="n">EmailStr</span>
<span class="n">birth_date</span><span class="p">:</span> <span class="n">PastDate</span>
<span class="n">websites</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">HttpUrl</span><span class="p">]</span> <span class="o">|</span> <span class="bp">None</span>
<span class="n">signup</span><span class="p">:</span> <span class="n">datetime</span> <span class="o">|</span> <span class="bp">None</span>
<span class="o">@</span><span class="n">field_validator</span><span class="p">(</span><span class="s">"name"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">name_is_allowed</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
<span class="k">if</span> <span class="n">value</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">[</span><span class="s">"Clement"</span><span class="p">,</span> <span class="s">"Paul"</span><span class="p">,</span> <span class="s">"Julie"</span><span class="p">,</span> <span class="s">"Alix"</span><span class="p">,</span> <span class="s">"Jar Jar Binks"</span><span class="p">]:</span>
<span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"This name is not allowed"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">value</span><span class="p">.</span><span class="n">lowercase</span><span class="p">().</span><span class="n">capitalize</span><span class="p">()</span>
</code></pre></div></div>
<h2 id="créons-un-champ-calculé-dynamiquement">Créons un champ calculé dynamiquement</h2>
<p>Il peut arriver que nous souhaitions avoir des données calculées à partir d’autres données dans notre modèle, cela peut être aisément implémenté grâce aux <code class="language-plaintext highlighter-rouge">computed_fields</code>. Nous définissons ici une propriété dynamique qui nous renvoie l’âge actuel de notre utilisateur, calculé grâce à sa date de naissance renseignée dans le modèle de base.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">UUID4</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">email</span><span class="p">:</span> <span class="n">EmailStr</span>
<span class="n">birth_date</span><span class="p">:</span> <span class="n">PastDate</span>
<span class="n">websites</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">HttpUrl</span><span class="p">]</span> <span class="o">|</span> <span class="bp">None</span>
<span class="n">signup</span><span class="p">:</span> <span class="n">datetime</span> <span class="o">|</span> <span class="bp">None</span>
<span class="o">@</span><span class="n">computed_field</span>
<span class="o">@</span><span class="nb">property</span>
<span class="k">def</span> <span class="nf">current_age</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">int</span><span class="p">:</span>
<span class="k">return</span> <span class="n">datetime</span><span class="p">.</span><span class="n">date</span><span class="p">.</span><span class="n">today</span><span class="p">().</span><span class="n">year</span> <span class="o">-</span> <span class="bp">self</span><span class="p">.</span><span class="n">birth_date</span><span class="p">.</span><span class="n">year</span>
</code></pre></div></div>
<h2 id="utilisons-notre-nouveau-modèle">Utilisons notre nouveau modèle</h2>
<p>Maintenant que nous avons un modèle, essayons de l’utiliser en nous fondant sur des données externes au format le plus commun du développement web : le json.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">external_data</span> <span class="o">=</span> <span class="p">{</span>
<span class="s">"id"</span><span class="p">:</span> <span class="s">"20CCF8B8-8EAA-4324-99FC-7512A5FB5D00"</span><span class="p">,</span>
<span class="s">"name"</span><span class="p">:</span> <span class="s">"Jar Jar Binks"</span><span class="p">,</span>
<span class="s">"email"</span><span class="p">:</span> <span class="s">"jar.jar@binks.com"</span><span class="p">,</span>
<span class="s">"birth_date"</span><span class="p">:</span> <span class="s">"1995-06-31"</span><span class="p">,</span>
<span class="s">"websites"</span><span class="p">:</span> <span class="p">[</span><span class="s">"jarjar.binks.com"</span><span class="p">,</span> <span class="s">"jjb.star-wars.com"</span><span class="p">]</span>
<span class="p">}</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span><span class="o">**</span><span class="n">external_data</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nb">id</span><span class="p">)</span>
<span class="c1">#> 20CCF8B8-8EAA-4324-99FC-7512A5FB5D00
</span><span class="k">print</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">current_age</span><span class="p">)</span>
<span class="c1">#> 18
</span>
<span class="k">print</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">model_dump</span><span class="p">())</span>
<span class="c1">#> {
#> "id": "20CCF8B8-8EAA-4324-99FC-7512A5FB5D00",
#> "name": "Jar Jar Binks",
#> "email": "jar.jar@binks.com",
#> "birth_date": "1995-06-31",
#> "websites": ["jarjar.binks.com", "jjb.star-wars.com"]
#> }
</span></code></pre></div></div>
<p>La principale propriété de Pydantic à remarquer est <strong>la coercition de types</strong> : pour un type cible, Pydantic sait comment convertir le type donné en entrée. Si l’on désire à la fin un entier, on peut fournir en entrée un entier, une chaine de caractère, un nombre flottant, etc. Ceci est particulièrement pratique pour parser des dates par exemple (ici on fournit une chaine de caractères qui est convertie en objet python <code class="language-plaintext highlighter-rouge">datetime</code>)</p>
<h2 id="la-validation-des-erreurs-en-elle-même">La validation des erreurs en elle même</h2>
<p>Voici un exemple concret de ce que nous renvoie Pydantic lorsque les données en entrée ne sont pas conformes :</p>
<figure>
<img src="/assets/img/Pydantic/Pydantic-error-handling.png" alt="La gestion d'erreur de Pydantic est exhaustive et compréhensible" class="" />
<figcaption>La gestion d'erreur de Pydantic</figcaption>
</figure>
<meta property="og:image:alt" content="La gestion d'erreur de Pydantic" />
<meta property="og:image" content="https://techblog.deepki.com/assets/img/Pydantic/Pydantic-error-handling.png" />
<meta property="og:image:alt" content="La gestion d'erreur de Pydantic est exhaustive et compréhensible" />
<p>Quelques conclusions par rapport à cet exemple :</p>
<ul>
<li>Pydantic détecte plusieurs erreurs possibles
<ul>
<li>la variable n’a pas le bon type (et ne peut être convertie) : entier au lieu de string/UUID pour le champ <code class="language-plaintext highlighter-rouge">ID</code>.</li>
<li>le contenu de la variable n’est pas conforme : <code class="language-plaintext highlighter-rouge">birth_date</code> est une date dans le futur, la date passée n’a pas le bon format.</li>
<li>la variable est manquante : nous n’avons pas rempli le champ <code class="language-plaintext highlighter-rouge">websites</code>. On peut cependant facilement ajouter des valeurs par défaut pour nos champs (<code class="language-plaintext highlighter-rouge">websites: list[HttpUrl] | None = None</code>) de la même façon qu’on le ferait pour un paramètre d’une fonction.</li>
</ul>
</li>
<li>Les messages d’erreurs sont clairs et nous indiquent :
<ul>
<li>le type et la valeur donnés en entrée</li>
<li>le type et la valeur attendus</li>
<li>un message d’erreur en anglais pour savoir comment corriger le problème</li>
</ul>
</li>
</ul>
<p>En plus de nous apporter de la rigueur, Pydantic nous renvoie donc des messages d’erreur compréhensibles et complets facilitant la correction.</p>
<h1 id="des-usages-concrets-chez-deepki-">Des usages concrets chez Deepki ?</h1>
<h2 id="développement-web">Développement web</h2>
<p>Pour développer des API propres et fiables, une tâche ô combien courante dans notre métier aujourd’hui, Pydantic se démarque.
En convertissant sans effort des dictionnaires désordonnés, des blobs ou chaînes de caractère JSON ou encore des résultats aux formats très différents de diverses bases de données en objets python entièrement validés, avec des rapports d’erreurs précis, Pydantic permet aux développeurs de garder leur API simple et lisible dès le départ.<br />
Les modèles peuvent être imbriqués, réutilisés et librement combinés pour capturer élégamment l’essence de n’importe quelle entité de notre domaine.</p>
<h3 id="interface-entre-notre-api-et-notre-base-de-données">Interface entre notre API et notre base de données</h3>
<p>Aujourd’hui, il n’est pas rare qu’une application web se connecte à de multiples bases de données. Chacune de ces bases de données et leur clients adaptés à notre langage fournissent des interfaces et des formats de réponse différents. Nous enlevons aujourd’hui plus facilement cette charge mentale au développeur.
Nous avons pris le parti dans mon équipe de regrouper tous nos accès à nos bases de données dans un même module (cf <a href="https://nanosoft.co.za/blog/post/clean-architecture-repository">repository pattern</a>). Il ne renvoie que des instances de modèles entièrement validées par Pydantic.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Représentation métier de notre utilisateur
</span><span class="k">class</span> <span class="nc">UserModel</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
<span class="p">...</span>
<span class="n">signup</span><span class="p">:</span> <span class="n">datetime</span>
<span class="n">last_login</span><span class="p">:</span> <span class="n">datetime</span>
<span class="n">password</span><span class="p">:</span> <span class="n">password</span>
<span class="c1"># Représentation des champs possibles pour créér une requête
</span><span class="k">class</span> <span class="nc">UserQuery</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="n">last_login</span><span class="p">:</span> <span class="n">datetime</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>
<span class="k">class</span> <span class="nc">UserMongoRepository</span><span class="p">():</span>
<span class="k">def</span> <span class="nf">create_one</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">new_user</span><span class="p">:</span> <span class="n">UserModel</span><span class="p">)</span> <span class="o">-></span> <span class="bp">None</span><span class="p">:</span>
<span class="bp">self</span><span class="p">.</span><span class="n">collection</span><span class="p">.</span><span class="n">insert_one</span><span class="p">(</span><span class="n">new_user</span><span class="p">.</span><span class="n">model_dump</span><span class="p">())</span>
<span class="k">def</span> <span class="nf">find_one</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filters</span><span class="p">:</span> <span class="n">UserQuery</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-></span> <span class="n">UserModel</span> <span class="o">|</span> <span class="bp">None</span><span class="p">:</span>
<span class="n">doc</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">collection</span><span class="p">.</span><span class="n">find_one</span><span class="p">(</span><span class="n">filters</span><span class="p">)</span>
<span class="k">return</span> <span class="n">UserModel</span><span class="p">(</span><span class="o">**</span><span class="n">doc</span><span class="p">)</span> <span class="k">if</span> <span class="n">doc</span> <span class="k">else</span> <span class="bp">None</span>
<span class="k">def</span> <span class="nf">find_all</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filters</span><span class="p">:</span> <span class="n">UserQuery</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-></span> <span class="n">Iterator</span><span class="p">[</span><span class="n">UserUserModel</span><span class="p">]:</span>
<span class="n">docs</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">collection</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">filters</span><span class="p">)</span>
<span class="k">return</span> <span class="p">(</span><span class="n">UserModel</span><span class="p">(</span><span class="o">**</span><span class="n">doc</span><span class="p">)</span> <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">docs</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">delete_many</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filters</span><span class="p">:</span> <span class="n">UserQuery</span><span class="p">)</span> <span class="o">-></span> <span class="nb">int</span><span class="p">:</span>
<span class="n">result</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">collection</span><span class="p">.</span><span class="n">delete_many</span><span class="p">(</span><span class="n">filters</span><span class="p">)</span>
<span class="k">return</span> <span class="n">result</span><span class="p">.</span><span class="n">deleted_count</span>
<span class="k">def</span> <span class="nf">count</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filters</span><span class="p">:</span> <span class="n">UserQuery</span><span class="p">)</span> <span class="o">-></span> <span class="nb">int</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">collection</span><span class="p">.</span><span class="n">count_documents</span><span class="p">(</span><span class="n">filters</span><span class="p">)</span>
</code></pre></div></div>
<ul>
<li>Toutes les interactions/spécificités de notre base de données, ici MongoDB, ne sont présentes qu’à un seul endroit. Ceci a la vertu de rentre triviale la recherche de code interagissant avec notre collection et faciliterait grandement une migration vers une autre base de données si nécessaire.</li>
<li>Hors de ce module, le code métier n’utilise que du Python standard avec les modèles de données définis via Pydantic. Tout est typé et validé. Il est donc maintenant impossible d’insérer des données en base ne passant pas la validation de Pydantic. Ceci peut nous éviter des corruptions de la base de données et des migrations pénibles. Enfin, nous récupérons uniquement des objets que nous connaissons, pas de spécificité de notre base de données dans le reste du code.</li>
</ul>
<h3 id="inputoutput-dun-endpoint-dapi">Input/Output d’un endpoint d’API</h3>
<p>Voici un exemple concret (avec <a href="https://flask.palletsprojects.com/">Flask</a> ici) de la validation des données en entrée et sortie d’un endpoint HTTP :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Représentation métier de notre utilisateur
</span><span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">UUID4</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">email</span><span class="p">:</span> <span class="n">EmailStr</span>
<span class="n">birth_date</span><span class="p">:</span> <span class="n">PastDate</span>
<span class="n">websites</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">HttpUrl</span><span class="p">]</span> <span class="o">|</span> <span class="bp">None</span>
<span class="n">signup</span><span class="p">:</span> <span class="n">datetime</span> <span class="o">|</span> <span class="bp">None</span>
<span class="c1"># Validation du contenu de la requête entrante
</span><span class="k">class</span> <span class="nc">CreateUserRequest</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">'Jar Jar Binks'</span>
<span class="n">age</span><span class="p">:</span> <span class="n">PositiveInt</span>
<span class="n">email</span><span class="p">:</span> <span class="n">EmailStr</span>
<span class="n">birth_date</span><span class="p">:</span> <span class="n">PastDate</span>
<span class="n">website</span><span class="p">:</span> <span class="n">HttpUrl</span> <span class="o">|</span> <span class="bp">None</span>
<span class="c1"># Validation de la réponse de l'endpoint
</span><span class="k">class</span> <span class="nc">CreateUserResponse</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="n">created_user</span><span class="p">:</span> <span class="n">User</span>
<span class="n">creation_date</span><span class="p">:</span> <span class="n">datetime</span>
<span class="o">@</span><span class="n">admin_blueprint</span><span class="p">.</span><span class="n">route</span><span class="p">(</span><span class="s">"/create-user"</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s">"POST"</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">create_user</span><span class="p">():</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">req</span> <span class="o">=</span> <span class="n">CreateUserRequest</span><span class="p">(</span><span class="o">**</span><span class="n">request</span><span class="p">.</span><span class="n">json</span><span class="p">)</span>
<span class="k">except</span> <span class="n">ValidationError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="c1"># En cas d'erreur de validation, on renvoie une erreur 400 avec un message d'erreur explicite
</span> <span class="k">raise</span> <span class="n">BadRequest</span><span class="p">(</span><span class="n">format_Pydantic_validation_error</span><span class="p">(</span><span class="n">e</span><span class="p">))</span>
<span class="c1"># Logique interne de l'endpoint
</span> <span class="n">user</span><span class="p">:</span> <span class="n">User</span> <span class="o">=</span> <span class="n">create_user</span><span class="p">()</span>
<span class="c1"># Création et validation de la réponse
</span> <span class="n">response</span> <span class="o">=</span> <span class="n">CreateUserResponse</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="c1"># Sérialisation JSON de la réponse
</span> <span class="c1"># Nous pouvons même choisir certains champs imbriqués à exclure de la réponse
</span> <span class="k">return</span> <span class="n">response</span><span class="p">.</span><span class="n">model_dump_json</span><span class="p">(</span><span class="n">exclude</span><span class="o">=</span><span class="p">{</span><span class="s">'user'</span><span class="p">:</span> <span class="p">{</span><span class="s">'websites'</span><span class="p">,</span> <span class="s">'signup'</span><span class="p">}}),</span> <span class="mi">201</span>
</code></pre></div></div>
<p>Nous voyons beaucoup de choses dans cet exemple :</p>
<ul>
<li>création très explicite et compréhensible de modèles représentant notre entrée et sortie de l’endpoint.</li>
<li>Une gestion des erreurs entièrement encapsulée par Pydantic. Nous n’avons qu’à générer notre réponse HTTP en incluant le message d’erreur généré.</li>
<li>Une sérialisation JSON intégrée des données renvoyées par l’endpoint.</li>
</ul>
<p>De mon point de vue, le développeur a ici beaucoup gagné :</p>
<ul>
<li>le temps de développement est drastiquement réduit, plus besoin de valider à la main toutes les entrées grâce aux nombreux types fournis par Pydantic pour valider la donnée (<code class="language-plaintext highlighter-rouge">email</code>, <code class="language-plaintext highlighter-rouge">URL</code>, entier positif, etc..)</li>
<li>On se concentre sur la logique métier, en ayant accès à des données typées et validées en lesquelles nous avons confiance.</li>
</ul>
<h2 id="plugins-et-ecosystèmes">Plugins et ecosystèmes</h2>
<h3 id="des-interactions-avec-des-orms">Des interactions avec des ORMs</h3>
<p>Pour des développeurs ayant de l’attrait pour l’utilisation d’<a href="https://fr.wikipedia.org/wiki/Mapping_objet-relationnel">ORMs</a>, Pydantic s’intègre très bien avec eux pour lier des modèles de base de données avec notre code python. Un <a href="https://docs.Pydantic.dev/latest/concepts/models/#arbitrary-class-instances">exemple ici</a> avec le très populaire <a href="https://www.sqlalchemy.org/">SQLAlchemy</a>.</p>
<h3 id="intégration-par-défaut-sur-les-nouveaux-frameworks">Intégration par défaut sur les nouveaux frameworks</h3>
<p>Parmi les évolutions de framework web dans le monde de Python, <a href="https://fastapi.tiangolo.com/">FastAPI</a> génère une forte traction. Un de ses forts partis pris est d’utiliser par défaut Pydantic pour la création d’APIs web. De ceci découle une des fonctionnalités les plus attirantes du framework : la <a href="https://fastapi.tiangolo.com/features/#automatic-docs">documentation de l’interface</a> est entièrement auto générée et synchronisée avec la base de code.</p>
<h2 id="performance">Performance</h2>
<p>Pydantic a, à mon avis, réussi un pari très inspirant en mêlant un des attraits les plus forts de Python, son expérience utilisateur, à la performance et stabilité, en implémentant le cœur de la librairie en Rust.</p>
<figure>
<img src="/assets/img/Pydantic/Pydantic-rust-in-python.png" alt="Une expérience développeur incroyable sans les soucis de performance de Python" class="" />
<figcaption>Une expérience développeur incroyable sans les soucis de performance de Python</figcaption>
</figure>
<meta property="og:image:alt" content="Une expérience développeur incroyable sans les soucis de performance de Python" />
<meta property="og:image" content="https://techblog.deepki.com/assets/img/Pydantic/Pydantic-rust-in-python.png" />
<meta property="og:image:alt" content="Une expérience développeur incroyable sans les soucis de performance de Python" />
<h1 id="quest-ce-que-pydantic-nest-pas-">Qu’est ce que Pydantic n’est pas ?</h1>
<p>Cet article est fortement influencé par mon opinion, il faut cependant comprendre que Pydantic répond à certains objectifs et ne doit pas être utilisé partout et tout le temps.</p>
<ul>
<li>
<p>Pydantic n’a pas pour ambition de remplacer toutes les fonctionnalités Python pour faire de l’orienté objet. Il apporte seulement de la validation de donnée en reprenant les syntaxes existantes.</p>
</li>
<li>
<p>En ajoutant de la validation via nos modèles, nous dégradons partiellement la performance de notre service. En effet, si je déclare et type une variable comme une liste d’entiers et essaye de valider celle ci sur 100.000 entiers, cela aura forcément un impact sur la durée d’exécution de nos fonctions.</p>
</li>
<li>
<p>Les données ne sont validées qu’à l’instanciation d’un modèle. En cas de changement, Pydantic ne vérifie pas les modifications entrantes. Un contournement est possible en ajoutant une <a href="https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.validate_assignment">option de configuration</a> de notre modèle.</p>
</li>
</ul>
<h1 id="quest-ce-que-ça-change-pour-python-">Qu’est ce que ça change pour Python ?</h1>
<p>La bibliothèque Pydantic est à mon avis un événement qui change la donne pour Python et son écosystème. En effet, elle apporte rigueur et robustesse à la gestion des données d’une manière élégante et intuitive qui manque aujourd’hui. Élegante et intuitive car elle ne s’appuie que sur des pratiques déjà connues et adoptées par les développeurs python (le typage, l’orienté objet).</p>
<p>Les développeurs Python peuvent désormais construire leurs applications sur des bases plus fiables et prédictibles. Rigueur au niveau des types, validation des entrées, gestion d’erreur précise : Pydantic automatise ce qui est souvent fait à la main, qui induit souvent des erreurs humaines. Cette bande passante libérée pour le développeur lui permet de son concentrer uniquement sur son cœur de métier.</p>
<p>Enfin Pydantic répond très bien à l’enjeu de conjuguer performance, stabilité et expérience développeur et ceci montre la direction que pourraient prendre beaucoup de librairies dans le futur à mon avis (bonjour Pandas). Je pense sincérement que l’intégrer au noyau cœur du langage Python serait bénéfique pour toute la communauté.</p>Pablo AbrilJ’ai eu l’opportunité, en février 2023, d’assister à une conférence de Samuel Colvin au FOSDEM à Bruxelles dans le cadre de mes fonctions chez Deepki. Il présentait une librairie de sa création : Pydantic. Je vais, à travers cet article de blog, vous présenter cette librairie, pourquoi je pense qu’elle révolutionne le langage Python et l’utilisation que l’on en fait chez Deepki.Améliorer l’architecture d’une application Vue grâce à la composition API2023-11-28T00:00:00+00:002023-11-28T00:00:00+00:00https://techblog.deepki.com//better-architecture-composition-api<p>En tant que développeur full stack, je cherche souvent à appliquer côté frontend, les meilleures pratiques du développement backend (et vice versa).</p>
<p>C’est ce que nous allons voir dans cet article.</p>
<p><img src="/assets/img/better-architecture-composition-api/minion-letsgo.gif" width="100%" class="center" /></p>
<meta property="og:image" content="https://techblog.deepki.com/assets/img/better-architecture-composition-api/minion-letsgo.gif" />
<h2 id="comment-la-composition-api-peut-améliorer-larchitecture-dune-application-">Comment la composition API peut améliorer l’architecture d’une application ?</h2>
<p>Chez Deepki, nous utilisons <a href="https://vuejs.org/guide/introduction.html">Vue.js</a> comme framework frontend.</p>
<p>Initialement nous utilisions <strong>Vue 2</strong> basé sur <a href="https://vuejs.org/guide/introduction.html#options-api">l’options API</a> qui impose une façon de structurer le code de ses composants à partir d’options déclaratives (<code class="language-plaintext highlighter-rouge">data</code>, <code class="language-plaintext highlighter-rouge">methods</code>, <code class="language-plaintext highlighter-rouge">mounted</code>…).</p>
<p>Cette approche est relativement simple et permet de s’affranchir de la compréhension de la réactivité en profondeur.</p>
<p>Par exemple, voici la partie javascript d’un composant en options API :</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><script></span>
<span class="k">export</span> <span class="k">default</span> <span class="p">{</span>
<span class="nx">data</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Bob</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="na">methods</span><span class="p">:</span> <span class="p">{</span>
<span class="nx">sayHello</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Hello </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="nx">mounted</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">sayHello</span><span class="p">()</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="nt"></script></span>
</code></pre></div></div>
<p>La nouvelle version <strong>Vue 3</strong> embarque un nouveau concept : <a href="https://vuejs.org/guide/extras/composition-api-faq.html">la composition API</a>. Et depuis, nous migrons notre base de code pour respecter cette nouvelle pratique.</p>
<h3 id="mais-cest-quoi-la-composition-api-">Mais, c’est quoi <em>la composition API</em> ?</h3>
<p>C’est un ensemble d’APIs qui permet d’importer des fonctions plutôt que déclarer des options comme c’était le cas avec <em>l’options API</em>.</p>
<ul>
<li><strong>Reactivity</strong> : Créer des états réactifs (<code class="language-plaintext highlighter-rouge">ref</code>, <code class="language-plaintext highlighter-rouge">computed</code>, <code class="language-plaintext highlighter-rouge">watch</code>)</li>
<li><strong>Lifecycle hooks</strong> : Se brancher au cycle de vie du composant (<code class="language-plaintext highlighter-rouge">onMounted</code>, <code class="language-plaintext highlighter-rouge">onUnmounted</code>)</li>
<li><strong>Dependency injection</strong> : Gérer les injections de dépendances de manière réactive (<code class="language-plaintext highlighter-rouge">provide</code>, <code class="language-plaintext highlighter-rouge">inject</code>)</li>
</ul>
<p>Pour reprendre l’exemple précédent, voici sa version en composition API en utilisant la syntaxe <a href="https://vuejs.org/api/sfc-script-setup.html">script setup</a> :</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><script </span><span class="na">setup</span><span class="nt">></span>
<span class="kd">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">ref</span><span class="p">(</span><span class="dl">'</span><span class="s1">Bob</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">sayHello</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Hello </span><span class="p">${</span><span class="nx">name</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="nx">onMounted</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">sayHello</span><span class="p">()</span>
<span class="p">})</span>
<span class="nt"></script></span>
</code></pre></div></div>
<h3 id="et-à-quoi-ça-sert-">Et… À quoi ça sert ?</h3>
<p>Il y a de nombreux avantages à utiliser la composition API plutôt que l’options API.
Le principal est la <strong>réutilisation de code</strong> sous la forme de fonctions composables.</p>
<p>Cela permet également de <strong>séparer le code</strong> de gros composants pour en augmenter la lisibilité.</p>
<p>Mais plutôt que de se limiter à la théorie, prenons un exemple concret.</p>
<p>Imaginons une fonctionnalité du type « todo list », où l’utilisateur peut ajouter de nouvelles tâches ou bien supprimer des tâches existantes. Les tâches seraient enregistrées et listées grâce à une API côté serveur.</p>
<p>Dans le cadre de cet exemple, nous nous limiterons à la partie <code class="language-plaintext highlighter-rouge">script</code> du composant vue, mais pour vous faire une idée, voici à quoi cela pourrait ressembler :</p>
<p><img src="/assets/img/better-architecture-composition-api/todo-list.gif" width="100%" class="center" /></p>
<meta property="og:image" content="https://techblog.deepki.com/assets/img/better-architecture-composition-api/todo-list.gif" />
<p>Et voici le code TypeScript correspondant :</p>
<div class="language-vue highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><</span><span class="k">script</span> <span class="na">lang=</span><span class="s">"ts"</span><span class="nt">></span>
<span class="k">import</span> <span class="nx">axios</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">axios</span><span class="dl">"</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">defineComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">vue</span><span class="dl">"</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">defineComponent</span><span class="p">({</span>
<span class="nx">mounted</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">todoList</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getTodoList</span><span class="p">()</span>
<span class="p">},</span>
<span class="nx">data</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="na">todoList</span><span class="p">:</span> <span class="p">[],</span>
<span class="na">current</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="na">content</span><span class="p">:</span> <span class="dl">""</span> <span class="p">},</span>
<span class="na">displayCurrent</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="na">computed</span><span class="p">:</span> <span class="p">{</span>
<span class="nx">canAddTodo</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">title</span> <span class="o">!==</span> <span class="dl">""</span> <span class="o">&&</span> <span class="k">this</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">content</span> <span class="o">!==</span> <span class="dl">""</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="na">methods</span><span class="p">:</span> <span class="p">{</span>
<span class="k">async</span> <span class="nx">getTodoList</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">/api/todo/list</span><span class="dl">"</span><span class="p">)</span>
<span class="p">},</span>
<span class="k">async</span> <span class="nx">createTodo</span><span class="p">(</span><span class="nx">todo</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">"</span><span class="s2">/api/todo/create</span><span class="dl">"</span><span class="p">,</span> <span class="nx">todo</span><span class="p">)</span>
<span class="p">},</span>
<span class="k">async</span> <span class="nx">deleteTodo</span><span class="p">(</span><span class="nx">todo</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">"</span><span class="s2">/api/todo/delete</span><span class="dl">"</span><span class="p">,</span> <span class="nx">todo</span><span class="p">)</span>
<span class="p">},</span>
<span class="nx">onChange</span><span class="p">(</span><span class="nx">newValue</span><span class="p">,</span> <span class="nx">type</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">title</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="nx">newValue</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">content</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">content</span> <span class="o">=</span> <span class="nx">newValue</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="nx">onCancelTodo</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">toggleDisplayCurrent</span><span class="p">()</span>
<span class="k">this</span><span class="p">.</span><span class="nx">current</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span>
<span class="na">content</span><span class="p">:</span> <span class="dl">""</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="nx">addNewTodo</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">todoList</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">current</span><span class="p">)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">createTodo</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">current</span><span class="p">)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">current</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span>
<span class="na">content</span><span class="p">:</span> <span class="dl">""</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="nx">removeTodo</span><span class="p">(</span><span class="nx">index</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">deletedTodo</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">todoList</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="nx">index</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deleteTodo</span><span class="p">(</span><span class="nx">deletedTodo</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="p">},</span>
<span class="nx">toggleDisplayCurrent</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">displayCurrent</span> <span class="o">=</span> <span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">displayCurrent</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="nt"></</span><span class="k">script</span><span class="nt">></span>
</code></pre></div></div>
<p>À l’initialisation, le composant appelle le serveur pour lister les tâches existantes et les afficher.</p>
<p>L’objectif est d’améliorer la qualité de ce code.</p>
<p>Grâce à la composition API et au remaniement de code, nous allons pouvoir :</p>
<ol>
<li>Remanier pour y extraire des cas d’usages et les découpler au maximum</li>
<li>Améliorer les noms donnés aux fonctions et aux variables pour gagner en lisibilité</li>
</ol>
<p>La première étape nous permet de rassembler les <code class="language-plaintext highlighter-rouge">méthodes</code>, <code class="language-plaintext highlighter-rouge">computed</code> et <code class="language-plaintext highlighter-rouge">références</code> qui sont liées aux mêmes fonctionnalités, comme par exemple, regrouper les fonctions d’appel au serveur et les extraire dans un autre fichier <code class="language-plaintext highlighter-rouge">client.ts</code> :</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">useTodoListClient</span> <span class="o">=</span> <span class="p">(</span><span class="nx">baseUrl</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/api/todo/</span><span class="dl">"</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">ref</span><span class="o"><</span><span class="nx">string</span><span class="o">></span><span class="p">(</span><span class="nx">baseUrl</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">getTodoList</span> <span class="o">=</span> <span class="k">async</span> <span class="p">():</span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">TodoItem</span><span class="p">[]</span><span class="o">></span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">url</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">/list`</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">createTodo</span> <span class="o">=</span> <span class="p">(</span><span class="na">todo</span><span class="p">:</span> <span class="nx">TodoItem</span><span class="p">):</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">url</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">/create`</span><span class="p">,</span> <span class="nx">todo</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">deleteTodo</span> <span class="o">=</span> <span class="p">(</span><span class="na">todo</span><span class="p">:</span> <span class="nx">TodoItem</span><span class="p">):</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">url</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">/delete`</span><span class="p">,</span> <span class="nx">todo</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="p">{</span>
<span class="nx">getTodoList</span><span class="p">,</span>
<span class="nx">createTodo</span><span class="p">,</span>
<span class="nx">deleteTodo</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Cela permet de séparer le composant du code permettant d’appeler le serveur, et éviter de devoir modifier ce composant dans le cas d’une évolution côté serveur. Ainsi seul le fichier <code class="language-plaintext highlighter-rouge">client.ts</code> sera impacté.</p>
<p>Ensuite nous pouvons extraire la gestion de la <code class="language-plaintext highlighter-rouge">todoList</code> dans un fichier externe <code class="language-plaintext highlighter-rouge">todo.ts</code>.
Ce fichier fournit une interface pour mettre à jour la liste et fait appel à <code class="language-plaintext highlighter-rouge">client.ts</code> pour interagir avec le serveur.</p>
<p>L’interface est composée de 3 fonctions :</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">fetchTodoList</code> : Pour récupérer la liste de tâches</li>
<li><code class="language-plaintext highlighter-rouge">addNewTodo</code> : Pour ajouter une nouvelle tâche</li>
<li><code class="language-plaintext highlighter-rouge">removeTodo</code> : Pour supprimer une tâche existante</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">computed</span><span class="p">,</span> <span class="nx">onMounted</span><span class="p">,</span> <span class="nx">ref</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">vue</span><span class="dl">"</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">useTodoListClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./client</span><span class="dl">"</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">TodoItem</span> <span class="p">{</span>
<span class="nl">title</span><span class="p">:</span> <span class="nx">string</span>
<span class="nx">content</span><span class="p">:</span> <span class="nx">string</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">useTodo</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="nx">useTodoListClient</span><span class="p">()</span>
<span class="kd">const</span> <span class="nx">todoList</span> <span class="o">=</span> <span class="nx">ref</span><span class="o"><</span><span class="nx">TodoItem</span><span class="p">[]</span><span class="o">></span><span class="p">([])</span>
<span class="nx">onMounted</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">fetchTodoList</span><span class="p">()</span>
<span class="p">})</span>
<span class="kd">const</span> <span class="nx">fetchTodoList</span> <span class="o">=</span> <span class="k">async</span> <span class="p">():</span> <span class="k">void</span> <span class="o">=></span> <span class="p">(</span><span class="nx">todoList</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">getTodoList</span><span class="p">())</span>
<span class="kd">const</span> <span class="nx">addNewTodo</span> <span class="o">=</span> <span class="p">(</span><span class="na">todo</span><span class="p">:</span> <span class="nx">TodoItem</span><span class="p">):</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">todoList</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">todo</span><span class="p">)</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">createTodo</span><span class="p">(</span><span class="nx">todo</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">removeTodo</span> <span class="o">=</span> <span class="p">(</span><span class="na">index</span><span class="p">:</span> <span class="nx">number</span><span class="p">):</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="na">deletedTodo</span><span class="p">:</span> <span class="nx">TodoItem</span><span class="p">[]</span> <span class="o">=</span> <span class="nx">todoList</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="nx">index</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">deleteTodo</span><span class="p">(</span><span class="nx">deletedTodo</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="p">}</span>
<span class="k">return</span> <span class="p">{</span>
<span class="na">todoList</span><span class="p">:</span> <span class="nx">computed</span><span class="p">(()</span> <span class="o">=></span> <span class="nx">todoList</span><span class="p">.</span><span class="nx">value</span><span class="p">),</span>
<span class="nx">fetchTodoList</span><span class="p">,</span>
<span class="nx">addNewTodo</span><span class="p">,</span>
<span class="nx">removeTodo</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Pour le composant, la <code class="language-plaintext highlighter-rouge">todoList</code> n’est plus directement modifiable (grâce à l’utilisation de la <code class="language-plaintext highlighter-rouge">computed</code>) mais uniquement via cette nouvelle interface, ainsi on évite tous potentiels futurs bugs liés aux changements sans précaution de cette liste.</p>
<p>Enfin, nous pouvons gagner en lisibilité en :</p>
<ul>
<li>créant une fonction <code class="language-plaintext highlighter-rouge">resetNewTodo</code> pour éviter la duplication de code.</li>
<li>renommant la référence <code class="language-plaintext highlighter-rouge">current</code> en <code class="language-plaintext highlighter-rouge">draftTodo</code></li>
<li>renommant la référence <code class="language-plaintext highlighter-rouge">displayCurrent</code> en <code class="language-plaintext highlighter-rouge">displayTodoForm</code></li>
<li>renommant la fonction <code class="language-plaintext highlighter-rouge">toggleDisplayCurrent</code> en <code class="language-plaintext highlighter-rouge">toggleDisplay</code></li>
<li>renommant la computed <code class="language-plaintext highlighter-rouge">canAddTodo</code> en <code class="language-plaintext highlighter-rouge">isDraftTodoComplete</code></li>
</ul>
<p>Ainsi le code du composant est le suivant :</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><script </span><span class="na">setup</span> <span class="na">lang=</span><span class="s">"ts"</span><span class="nt">></span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">computed</span><span class="p">,</span> <span class="nx">ref</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">vue</span><span class="dl">"</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">useTodo</span><span class="p">,</span> <span class="nx">TodoItem</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./todo</span><span class="dl">"</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">todoList</span><span class="p">,</span> <span class="nx">fetchTodoList</span><span class="p">,</span> <span class="nx">addNewTodo</span><span class="p">,</span> <span class="nx">removeTodo</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useTodo</span><span class="p">()</span>
<span class="kd">const</span> <span class="nx">displayTodoForm</span> <span class="o">=</span> <span class="nx">ref</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">toggleDisplay</span> <span class="o">=</span> <span class="p">():</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">displayTodoForm</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="o">!</span><span class="nx">displayTodoForm</span><span class="p">.</span><span class="nx">value</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">draftTodo</span> <span class="o">=</span> <span class="nx">ref</span><span class="o"><</span><span class="nx">TodoItem</span><span class="o">></span><span class="p">({</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span>
<span class="na">content</span><span class="p">:</span> <span class="dl">""</span>
<span class="p">})</span>
<span class="kd">const</span> <span class="nx">onChange</span> <span class="o">=</span> <span class="p">(</span><span class="nx">newValue</span><span class="p">:</span> <span class="nx">string</span><span class="p">,</span> <span class="nx">type</span><span class="p">:</span> <span class="nx">string</span><span class="p">):</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">title</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">draftTodo</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="nx">newValue</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">content</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">draftTodo</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">content</span> <span class="o">=</span> <span class="nx">newValue</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">isDraftTodoComplete</span> <span class="o">=</span> <span class="nx">computed</span><span class="o"><</span><span class="nx">boolean</span><span class="o">></span><span class="p">(</span>
<span class="p">()</span> <span class="o">=></span> <span class="nx">draftTodo</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">title</span> <span class="o">!==</span> <span class="dl">""</span> <span class="o">&&</span> <span class="nx">draftTodo</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">content</span> <span class="o">!==</span> <span class="dl">""</span>
<span class="p">)</span>
<span class="kd">const</span> <span class="nx">onValidateNewTodo</span> <span class="o">=</span> <span class="p">():</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">addNewTodo</span><span class="p">(</span><span class="nx">draftTodo</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span>
<span class="nx">resetDraftTodo</span><span class="p">()</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">onCancelNewTodo</span> <span class="o">=</span> <span class="p">():</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">toggleDisplay</span><span class="p">()</span>
<span class="nx">resetDraftTodo</span><span class="p">()</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">resetDraftTodo</span> <span class="o">=</span> <span class="p">():</span> <span class="k">void</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">draftTodo</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span>
<span class="na">content</span><span class="p">:</span> <span class="dl">""</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nt"></script></span>
</code></pre></div></div>
<p>Bien évidemment ce code n’est pas parfait, et peut-être amélioré. Mais l’idée est de faciliter les évolutions et éviter qu’elles soient coûteuses en termes de temps et d’effort.</p>
<p>⚠️ Ici j’ai uniquement parlé du découpage via la composition, mais il faut garder en tête que le <strong>découpage en composants</strong> reste l’un des principaux atout des frameworks comme Vue.</p>
<p>Ainsi bien découper son code par fonctionnalité grâce à la composition API ne remplace aucunement le découpage par composant. Au contraire, <strong>ces 2 méthodes se complémentent parfaitement</strong> et vous permettront de rendre le code de vos applications plus simple et plus lisible.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Depuis que j’ai découvert la flexibilité et la puissance de la composition API offerte par Vue 3, je ne peux pas m’en passer.</p>
<p>Finalement, voici les avantages principaux identifiés :</p>
<ul>
<li>Meilleure organisation de la logique interne des composants</li>
<li>Partage de fonctionnalités transverses entre composants (cela évite la duplication de code et l’utilisation des <a href="/dessign-patterns-vuejs/">mixins</a>)</li>
<li>Découpage de gros composants avec la possibilité d’ajouter des couches d’abstractions pour en améliorer l’architecture</li>
<li>Meilleur support pour Typescript</li>
</ul>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://vuejs.org/guide/introduction.html">Vue.js Introduction</a></li>
<li><a href="https://vuejs.org/guide/extras/composition-api-faq.html#what-is-composition-api">What is composition API - Vue.js</a></li>
<li><a href="https://markus.oberlehner.net/blog/vue-3-composition-api-vs-options-api/">Composition API vs Options API - Markus Oberlehner</a></li>
</ul>Pierre AssematEn tant que développeur full stack, je cherche souvent à appliquer côté frontend, les meilleures pratiques du développement backend (et vice versa).Wrap like an Egyptian2023-11-20T00:00:00+00:002023-11-20T00:00:00+00:00https://techblog.deepki.com//wrappers<p>Le wrapping consiste à envelopper (to wrap en anglais) un code dans un autre code.</p>
<p>Le <a href="/some-python-design-patterns/">design pattern</a> « Façade », illustré dans l’exemple ci-dessous, est un exemple de Wrapper.</p>
<p>Voici une petite illustration IRL du wrapping (<em>tirée de <a href="https://refactoring.guru/design-patterns/facade">Façade</a></em>)</p>
<p>Quand nous passons une commande chez un distributeur, plusieurs actions s’enchainent :</p>
<ul>
<li>fabrication</li>
<li>emballage</li>
<li>prise en compte des taxes</li>
<li>livraison…</li>
</ul>
<p>La complexité de cet enchaînement nous est invisible puisqu’elle ne se réduit qu’à l’action de “commander” (Wrapper)</p>
<figure>
<img src="/assets/img/wrappers/wrapper-irl-example.jpg" alt="Exemple de Wrapper dans la vie de tous les jours" class="" />
<figcaption>Wrapper IRL</figcaption>
</figure>
<meta property="og:image:alt" content="Wrapper IRL" />
<meta property="og:image" content="https://techblog.deepki.com/assets/img/wrappers/wrapper-irl-example.jpg" />
<meta property="og:image:alt" content="Exemple de Wrapper dans la vie de tous les jours" />
<p>Il y a un an, dans notre application, nous avions voulu remplacer la bibliothèque de gestion de date js, <a href="https://momentjs.com/">moment</a> dépréciée alors. Or, les appels aux fonctions de <strong>moment</strong> étaient éparpillés dans notre application.
Faire la migration au cas par cas, dans les différents fichiers de notre base de code aurait été pénible.</p>
<p>Pour s’assurer d’une harmonie dans le comportement des fonctions de date et pour faciliter le remplacement de la bibliothèque externe, nous avons opté pour l’utilisation d’un fichier dans lequel seront répertoriés des Wrappers qui appellent les fonctionnalités de <strong>moment</strong>, utilisés dans notre code.
Cet exemple illustrera la suite de l’article.</p>
<h2 id="les-avantages-à-wrapper">Les avantages à wrapper</h2>
<h3 id="centraliser-les-appels">Centraliser les appels</h3>
<p>Pour formater une date avec <strong>moment</strong>, on peut utiliser le code suivant :</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">moment</span><span class="p">(</span><span class="nx">date</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="nx">format</span><span class="p">)</span>
</code></pre></div></div>
<p>Maintenant, encapsulons cette fonctionnalité dans une fonction que nous créons :</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// time-wrapper.js</span>
<span class="kd">const</span> <span class="nx">formatDate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">date</span><span class="p">,</span> <span class="nx">format</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">moment</span><span class="p">(</span><span class="nx">date</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="nx">format</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>De cette façon, nous avons créé une fonction qui utilise une fonction de <strong>moment</strong>. On peut donc répéter ce processus et compiler en un seul fichier <code class="language-plaintext highlighter-rouge">time-wrapper.js</code> les fonctions de <strong>moment</strong> que nous souhaitons utiliser dans notre application.</p>
<p>Effectivement, lorsqu’on wrappe une bibliothèque externe, le reste du code ne doit pas directement appeler cette bibliothèque.
Sinon, nous nuirons à la centralisation des appels. Le code doit absolument passer par notre Wrapper.</p>
<p>Ainsi, un premier avantage du Wrapper est l’inventaire des fonctionnalités, d’une source tierce, que nous utilisons.</p>
<h3 id="lisibilité-et-dé-complexité">Lisibilité et dé-complexité</h3>
<p>Nous avons vu plus haut que nous pouvons réduire des appels de fonction successifs, à une méthode. Notre Wrapper nous a donc permis de gagner en lisibilité.</p>
<p>Ensuite, la prise en main du Wrapper est plus simple car il « cache » la complexité des fonctions qu’il utilise. En effet, la fonctionnalité étant implémentée qu’une seule fois à son niveau, celui-ci la découple de sa complexité.</p>
<p>De plus nous avons la main sur les déclarations des fonctions du Wrapper. (Logique, nous les créons)</p>
<p>On peut donc avoir un objet en paramètre de nos fonctions. Cela est pratique:</p>
<ul>
<li>lorsqu’un ou plusieurs paramètres peuvent être non-définis</li>
<li>documenter : lorsqu’on appelle la fonction, ses arguments seront explicites</li>
</ul>
<p>De cette manière, on peut réecrire la fonction <code class="language-plaintext highlighter-rouge">formatDate</code> qu’on a vue plus haut comme suit:</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// time-wrapper.js</span>
<span class="kd">const</span> <span class="nx">formatDate</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">date</span><span class="p">,</span> <span class="nx">format</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">moment</span><span class="p">(</span><span class="nx">date</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="nx">format</span><span class="p">)</span>
<span class="p">}</span>
<span class="cm">/* au lieu de
const formatDate = (date, format) => {
return moment(date).format(format)
}
*/</span>
</code></pre></div></div>
<p>Qu’on appellera alors en explicitant chaque attribut de l’objet en paramètre:</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">formatDate</span><span class="p">({</span><span class="na">date</span><span class="p">:</span> <span class="mi">1698942175420</span><span class="p">,</span> <span class="na">format</span><span class="p">:</span> <span class="dl">"</span><span class="s2">LLL</span><span class="dl">"</span><span class="p">})</span>
<span class="cm">/* au lieu de
const formatDate(1698942175420, "LLL")
*/</span>
</code></pre></div></div>
<p>Et si <code class="language-plaintext highlighter-rouge">date</code> est non défini, nous n’avons plus besoin de le préciser.</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">formatDate</span><span class="p">({</span><span class="na">format</span><span class="p">:</span> <span class="dl">"</span><span class="s2">LLL</span><span class="dl">"</span><span class="p">})</span> <span class="c1">// date est undefined</span>
<span class="cm">/* au lieu de
const formatDate(undefined, "LLL")
*/</span>
</code></pre></div></div>
<p>La centralisation des fonctionnalités qu’on utilise dans un Wrapper contribue aussi à faciliter leur compréhension. Les moyens suivants permettent la documentation des fonctionnalités wrappées:</p>
<ul>
<li>les tests</li>
<li>les commentaires</li>
<li>un fichier de documentation créé dans le même dossier que le Wrapper</li>
<li>le nom des fonctions</li>
<li>la déclaration des fonctions et leur typage</li>
</ul>
<h3 id="étendre-une-fonctionnalité">Étendre une fonctionnalité</h3>
<p>Parfois, les fonctionnalités proposées par une bibliothèque ne sont pas suffisantes pour un besoin particulier.</p>
<p>Avec un Wrapper, nous pourrions ajouter des fonctionnalités en enrichissant ce que nous propose la bibliothèque. Prenons l’exemple suivant :</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">logDate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">a</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Date input is a=</span><span class="p">${</span><span class="nx">a</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">moment</span><span class="p">(</span><span class="nx">a</span><span class="p">)</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Result Date : </span><span class="p">${</span><span class="nx">result</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="k">return</span> <span class="nx">result</span>
<span class="p">}</span>
</code></pre></div></div>
<p>La fonction <code class="language-plaintext highlighter-rouge">logDate</code> wrappe la fonction <strong>moment</strong> en lui ajoutant de la journalisation.</p>
<p>Par ailleurs, un autre exemple commun de fonctionnalité additionnelle que nous pourrions implémenter est une <em>gestion d’erreur</em>.</p>
<p>En effet lorsque cette dernière n’est pas proposée par l’existant, nous pouvons la créer dans un Wrapper.</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">const</span> <span class="nx">createDate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">date</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="nx">moment</span><span class="p">(</span><span class="nx">date</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">moment</span><span class="p">(</span><span class="nx">res</span><span class="p">).</span><span class="nx">isValid</span><span class="p">())</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Mmmh il y a un souci de Date : </span><span class="p">${</span><span class="nx">date</span><span class="p">}</span><span class="s2"> semble être un mauvais input`</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">res</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">error</span> <span class="k">instanceof</span> <span class="nb">Error</span><span class="p">)</span> <span class="k">throw</span> <span class="nx">error</span>
<span class="p">}}</span>
</code></pre></div></div>
<p>Ainsi, notre <code class="language-plaintext highlighter-rouge">createDate</code> gère un cas, comme étant une erreur, alors que <strong>moment</strong> ne le considère pas comme tel.</p>
<h3 id="modularité">Modularité</h3>
<p>L’utilisation d’un Wrapper nous donne la main pour étendre des fonctionnalités qui sont proposées par une entité.</p>
<p>Pour faire évoluer ces fonctionnalités, nul besoin de les traiter partout dans une application, mais uniquement au sein du Wrapper.</p>
<p>Reprenons l’exemple de la fonction <code class="language-plaintext highlighter-rouge">formatDate</code>. Si nous décidions de changer de bibliothèque de gestion de dates et utiliser <a href="https://day.js.org/">day.js</a> au lieu de <strong>moment</strong>, nous n’aurions qu’à modifier le fichier time-wrapper.js.</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// time-wrapper.js</span>
<span class="kd">const</span> <span class="nx">formatDate</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">date</span><span class="p">,</span> <span class="nx">format</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">dayjs</span><span class="p">(</span><span class="nx">date</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="nx">format</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Et voilà. Ou presque…</p>
<p>Certes, nous avons modifié la fonction <code class="language-plaintext highlighter-rouge">formatDate</code> mais nous n’avons aucune certitude sur son comportement.</p>
<p>Ce qui nous amène au point suivant, <strong>le Wrapper doit être testé dans l’optique de voir son code évoluer.</strong></p>
<p>L’idée n’est pas de tester l’intégralité d’une source tierce, mais plutôt de tester le Wrapper et ses fonctions qui sont utilisées dans notre application.</p>
<p>Si les tests sont au vert lorsque le code évolue, nous serons plus sereins.</p>
<h2 id="kairos-ou-le-temps-de-lopportunisme"><a href="https://www.radiofrance.fr/franceculture/podcasts/le-journal-de-la-philo/qu-est-ce-que-le-kairos-6737760">Kairos</a> ou le temps de l’opportunisme</h2>
<p>Si on se demande quel est le bon moment pour wrapper, voici quelques raisons de mettre en place un Wrapper :</p>
<ul>
<li>Migration : Abstraire l’utilisation du composant externe simplifie sa migration.</li>
<li>Faire l’inventaire des services tiers utilisés dans notre application.</li>
<li>Faciliter la documentation d’une fonctionnalité</li>
<li>Si on souhaite standardiser l’output d’une fonctionnalité dans nos applications. <em>(Exemples : avoir les mêmes constructions de messages d’erreur, les mêmes formats de date, les mêmes url de base pour effectuer des requêtes etc..)</em></li>
<li>Lorsqu’on sent/sait qu’une dépendance risque d’évoluer et qu’il faudra la remplacer</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Nous avons vu ensemble ce qu’est un Wrapper et quels intérêts peut présenter sa mise en place. Effectivement, il facilite la clarté des fonctionnalités qu’il enveloppe ainsi que l’évolution de celles-ci.</p>
<p>Pour ma part, le gros atout d’un Wrapper est la facilitation de la maintenance de fonctionnalités : si une source externe du projet est amenée à changer, la wrapper est une étape qui facilite assurément ces évolutions.</p>
<p>Attention toutefois à son utilisation car cela a un coût. C’est donc après analyse qu’il faudra mettre en balance la création ou non d’un Wrapper. Appliquer le principe <a href="/backend-charter-part-1/">YAGNI</a> (“you ain’t gonna need it” ou “vous n’en aurez pas besoin”) nous évitera de produire du code inutile.</p>
<p>Par exemple : si une fonctionnalité ne changera pas ou peu, ou si elle est peu utilisée dans le code, on ne devrait pas investir dans la mise en place d’un Wrapper.</p>
<p>Dans cet article, nous avons discuté de Wrapper de fonction, néanmoins, il ne se limite pas seulement aux fonctions : on peut par exemple wrapper des classes, des types ou encore des variables…</p>
<h3 id="références">Références</h3>
<ul>
<li><a href="https://refactoring.guru/design-patterns/facade">Façade</a></li>
<li><a href="https://developer.mozilla.org/fr/docs/Glossary/Wrapper">Définition simple d’un Wrapper</a></li>
<li><a href="https://momentjs.com/">Moment</a></li>
<li><a href="https://www.radiofrance.fr/franceculture/podcasts/le-journal-de-la-philo/qu-est-ce-que-le-kairos-6737760">Kairos</a></li>
<li><a href="https://www.youtube.com/watch?v=Cv6tuzHUuuk&ab_channel=TheBanglesVEVO">The Bangles - Walk Like an Egyptian</a></li>
</ul>Khaled FAYSALLe wrapping consiste à envelopper (to wrap en anglais) un code dans un autre code.La charte du développement backend chez Deepki - Partie 4 : opérabilité2023-10-30T00:00:00+00:002023-10-30T00:00:00+00:00https://techblog.deepki.com//backend-charter-part-4<p>Cet article est le dernier de notre série consacrée à la charte de développement backend chez Deepki. Nous y abordons des sujets liés à l’opérationnel : le déploiement, l’observabilité et la sécurité.</p>
<h1 id="sécurité">Sécurité</h1>
<p>Dans le développement logiciel, la sécurité est un élément clé à ne pas négliger. Pour éviter les failles de sécurité, il est important de suivre quelques règles élémentaires.</p>
<p>Tout d’abord, il est essentiel de ne pas faire confiance aux données externes. Les inputs doivent être nettoyés afin d’éviter les injections de code. De plus, il est recommandé de ne pas stocker les credentials dans le repo, notamment les identifiants et mots de passe de base de données ou de service externe. Des solutions techniques existent pour éviter cela, comme <a href="https://docs.ansible.com/ansible/latest/vault_guide/vault_encrypting_content.html">Ansible Vault</a>.</p>
<p>Lorsqu’on stocke des mots de passe, il est important de les chiffrer, de les <a href="https://fr.wikipedia.org/wiki/Salage_(cryptographie)">saler</a> et de les rendre lent à déchiffrer. Idéalement, on utilise un algorithme éprouvé comme bcrypt. De même, il est crucial de ne pas logger de données sensibles comme les mots de passe ou les numéros de carte de crédit.</p>
<p>Pour les APIs exposées sur Internet, il est indispensable d’utiliser SSL. Il est important de toujours partir du principe que le réseau est compromis et donc de prendre les mesures de sécurité nécessaires.</p>
<p>Enfin, il ne faut pas oublier de donner le moins de privilèges possibles à chaque produit. Si une application ne fait que des lectures dans une DB, il est inutile de lui donner un accès en écriture.</p>
<h1 id="déploiements">Déploiements</h1>
<p>Nous préférons des déploiements fréquents, courts et sans interruption de service. Cela implique de gérer la <a href="https://techblog.deepki.com/retrocompat/">rétrocompatibilité</a> dans la mesure du possible, et lorsque ce n’est pas possible ou trop coûteux, de bien réfléchir aux impacts et de prévenir les équipes concernées.</p>
<p>En cas de problème suite à un déploiement, la personne qui a effectué le déploiement doit corriger le problème ou prendre la décision de revenir en arrière. Nous évitons donc les déploiements tardifs dans la journée ou avant le week-end.</p>
<h1 id="logs-et-métriques">Logs et métriques</h1>
<p>Les logs sont des fichiers qui enregistrent les événements du système, tandis que les métriques sont des mesures quantitatives de divers aspects du système. Les logs sont utilisés pour le débogage et la recherche de problèmes spécifiques, tandis que les métriques servent à surveiller le système en temps réel et à détecter les problèmes avant qu’ils ne deviennent critiques.</p>
<p>En tant que développeurs, nous avons pour rôle de produire :</p>
<ul>
<li>Des logs, utilisables par des humains pour comprendre ce qu’il s’est produit dans le passé et pour diagnostiquer des problèmes qui sont déjà arrivés.</li>
<li>Des métriques, utilisables pas des humains ou des automates pour surveiller le système en temps réel et détecter les problèmes avant qu’ils ne deviennent critiques.</li>
</ul>
<h1 id="conclusion">Conclusion</h1>
<p>Cet article clôt la série consacrée à la charte du développement backend chez Deepki. Nous espérons que cette série vous aura donné envie d’appliquer vous aussi ces principes, et qu’ils vous aideront à écrire du code maintenable et de qualité !</p>
<ul>
<li><a href="/backend-charter-part-1/">Les grands principes</a></li>
<li><a href="/backend-charter-part-2/">La lisibilité</a></li>
<li><a href="/backend-charter-part-3/">Les tests automatiques</a></li>
<li><a href="/backend-charter-part-4/">La sécurité et l’opérabilité</a></li>
</ul>Olivier RouxCet article est le dernier de notre série consacrée à la charte de développement backend chez Deepki. Nous y abordons des sujets liés à l’opérationnel : le déploiement, l’observabilité et la sécurité.Les fixtures de données pytest : la fausse bonne idée ?2023-10-23T00:00:00+00:002023-10-23T00:00:00+00:00https://techblog.deepki.com//data-fixtures-in-tests<p>Il est désormais communément admis que les <a href="/introduction-au-tdd/">les tests sont essentiels</a> pour garantir le bon fonctionnement d’une application et faciliter sa maintenance. Ils permettent de s’assurer de la non-régression de certains comportements mais également de les documenter pour de futurs développeurs.
Plus les tests sont lisibles et explicites, plus on est en mesure de comprendre comment le code fonctionne. <a href="/the-clean-coder-part-2/">Robert C. Martin</a>, va jusqu’à déclarer dans son livre “Clean Code”<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>:</p>
<blockquote>
<p>Test code is just as important as production code. It is not a second-class citizen. It requires thought, design, and care. It must be kept as clean as production code.</p>
</blockquote>
<p>Ce qui donne en français:</p>
<blockquote>
<p>Le code de test est aussi important que le code de production. Ce n’est pas un citoyen de seconde zone. Il nécessite réflexion, conception et soin. Il doit être maintenu aussi propre que le code de production.</p>
</blockquote>
<p>La <a href="/deepki-fournisseurs-et-api/">collecte de données</a> étant au cœur de l’activité de Deepki, il est primordial de s’assurer que les fonctions qui persistent, formattent ou manipulent ces données sont testées de manière lisible. Dans cet article, nous nous intéresserons tout particulièrement aux fixtures pytest comme moyen de simuler des données de test.
Et la conclusion n’est peut-être pas celle que vous attendez.</p>
<p>Mais trève de bavardage et rentrons sans plus tarder dans le vif du sujet !</p>
<h2 id="fixtures-en-haute-mer">Fixtures en haute mer</h2>
<h3 id="les-fixtures-kézako-">Les fixtures, kézako ?</h3>
<p>Une fixture est un outil de développement qui permet de générer ou simuler des données, des configurations ou des applications externes. Les fixtures permettent d’initialiser un contexte cohérent et réutilisable d’un test à l’autre (par exemple en définissant un jeu de données de test).
Les <a href="https://docs.pytest.org/en/6.2.x/fixture.html">fixtures pytest</a> (définies grâce au décorateur <code class="language-plaintext highlighter-rouge">@pytest.fixture</code>) correspondent à des fonctions appelées (explicitement ou non) via les arguments des fonctions ou classes de test.
Elles sont particulièrement utiles pour mocker un service, une librairie externe, un client web ou une base de données.</p>
<h3 id="qui-dit-fixture-dit-test-">Qui dit fixture dit test !</h3>
<p>Mais rien de tel qu’un exemple concret pour mieux comprendre le fonctionnement de ces fixtures. Considérons une entreprise qui fabrique des voiliers et assure un suivi informatisé des bateaux qu’elle construit.
Chaque voilier est identifié par les informations suivantes :</p>
<ul>
<li>un identifiant unique</li>
<li>un nom</li>
<li>une année de construction</li>
<li>un type: catamaran, trimaran, monocoque (ou “singlehull” pour nos amis anglophones)</li>
</ul>
<p>Pour l’instant l’exemple est très bateau je vous l’accorde.</p>
<p>On souhaite tester la fonction <code class="language-plaintext highlighter-rouge">get_boat_description</code> qui retourne la description textuelle d’un bateau, identifié par son id.
Évidemment notre but est de rendre ces tests les plus explicites et concis possible. Puisqu’il s’agit de code Python, nous nous orientons assez naturellement vers les fixtures pytest.</p>
<p>La fixture <code class="language-plaintext highlighter-rouge">sailboat</code> définit un dictionnaire qu’il nous est ensuite possible d’utiliser directement comme argument de la fonction <code class="language-plaintext highlighter-rouge">test_get_boat_description</code>.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">pytest</span><span class="p">.</span><span class="n">fixture</span>
<span class="k">def</span> <span class="nf">sailboat</span><span class="p">():</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s">"id"</span><span class="p">:</span> <span class="s">"id_sailboat"</span><span class="p">,</span>
<span class="s">"name"</span><span class="p">:</span> <span class="s">"Santa Maria"</span><span class="p">,</span>
<span class="s">"construction_year"</span><span class="p">:</span> <span class="mi">1492</span><span class="p">,</span>
<span class="s">"type_of_boat"</span><span class="p">:</span> <span class="s">"singlehull"</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">def</span> <span class="nf">test_get_boat_description</span><span class="p">(</span><span class="n">sailboat</span><span class="p">):</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert</span><span class="p">(</span><span class="n">sailboat</span><span class="p">)</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">get_boat_description</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"id_sailboat"</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">description</span> <span class="o">==</span> <span class="s">"Le Santa Maria est un voilier monocoque construit en 1492."</span>
</code></pre></div></div>
<p>Le résultat est clair et concis, les données de tests sont explicites et définies à proximité du test ce qui nous permet une comparaison directe entre l’input et l’output de la fonction. Que demander de plus !</p>
<p>L’utilisation des fixtures nous permet également de réutiliser notre dictionnaire de données dans un autre test.
Imaginons par exemple qu’il faille tester la fonction <code class="language-plaintext highlighter-rouge">get_boats</code> qui retourne la liste de tous les bateaux construits par l’entreprise.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_get_boat</span><span class="p">(</span><span class="n">sailboat</span><span class="p">):</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert</span><span class="p">(</span><span class="n">sailboat</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">get_boats</span><span class="p">()</span> <span class="o">==</span> <span class="p">[{</span>
<span class="s">"id"</span><span class="p">:</span> <span class="s">"id_sailboat"</span><span class="p">,</span>
<span class="s">"name"</span><span class="p">:</span> <span class="s">"Santa Maria"</span><span class="p">,</span>
<span class="s">"construction_year"</span><span class="p">:</span> <span class="mi">1492</span><span class="p">,</span>
<span class="s">"type_of_boat"</span><span class="p">:</span> <span class="s">"sailboat"</span><span class="p">,</span>
<span class="p">}]</span>
</code></pre></div></div>
<h3 id="quen-est-il-si-nous-décidons-de-complexifier-un-peu-notre-exemple-">Qu’en est-il si nous décidons de complexifier un peu notre exemple ?</h3>
<p>Jusque-là, aucune ombre au tableau ! Mais supposons désormais que l’entreprise navale construit des bateaux à moteur en plus des voiliers.
À des fins de simplification, on considèrera que les champs qui décrivent un bateau à moteur sont les mêmes que ceux utilisés pour décrire un voilier.
La description variant légèrement entre les deux types de bateaux, nous voulons nous assurer que le nouveau comportement de la fonction <code class="language-plaintext highlighter-rouge">get_boat_description</code> correspond à celui attendu.
Malheureusement, nous ne pouvons pas réutiliser la fixture <code class="language-plaintext highlighter-rouge">sailboat</code>. Nous sommes contraints de définir une nouvelle fixture <code class="language-plaintext highlighter-rouge">motorboat</code> propre aux données que l’on souhaite tester.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">pytest</span><span class="p">.</span><span class="n">fixture</span>
<span class="k">def</span> <span class="nf">motorboat</span><span class="p">():</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s">"id"</span><span class="p">:</span> <span class="s">"id_motorboat"</span><span class="p">,</span>
<span class="s">"name"</span><span class="p">:</span> <span class="s">"Titanic"</span><span class="p">,</span>
<span class="s">"construction_year"</span><span class="p">:</span> <span class="mi">1909</span><span class="p">,</span>
<span class="s">"type_of_boat"</span><span class="p">:</span> <span class="s">"motorboat"</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">def</span> <span class="nf">test_get_boat_description_for_motorboat</span><span class="p">(</span><span class="n">motorboat</span><span class="p">):</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert</span><span class="p">(</span><span class="n">motorboat</span><span class="p">)</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">get_boat_description</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"id_motorboat"</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">description</span> <span class="o">==</span> <span class="s">"Le Titanic est un bateau à moteur construit en 1909."</span>
</code></pre></div></div>
<p>Notre fichier de test commence déjà à grossir. Pour vérifier que la description est conforme aux informations du bateau recherché, il faut se référer à la fixture associée. Plus le nombre de fixtures est grand,
plus la gymnastique entre les tests et les données générées par chaque fixture devient ardue.</p>
<h3 id="jamais-deux-sans-trois-fixtures-">Jamais deux sans trois fixtures ?</h3>
<p>Ajoutons une nouvelle subtilité. On souhaite distinguer les bateaux qui ont coulé de ceux qui naviguent encore. La fonction <code class="language-plaintext highlighter-rouge">get_number_of_boats_that_sunk</code> retourne le nombre de bateaux construits par l’entreprise et qui ont coulé (en se fondant sur le champ <code class="language-plaintext highlighter-rouge">has_sunk</code>).</p>
<p>Une fois encore, les fixtures implémentées dans les tests décrits précédemment ne permettent pas de tester la fonction <code class="language-plaintext highlighter-rouge">get_number_of_boats_that_sunk</code>. Deux solutions s’offrent à nous :</p>
<ol>
<li>Définir une troisième fixture</li>
<li>Réutiliser une fixture existante que l’on modifiera dans notre test pour se conformer au besoin</li>
</ol>
<p><strong>Option 1</strong>: Définition d’une nouvelle fixture <code class="language-plaintext highlighter-rouge">motorboat_that_sunk</code> comportant le champ <code class="language-plaintext highlighter-rouge">has_sunk</code></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">pytest</span><span class="p">.</span><span class="n">fixture</span>
<span class="k">def</span> <span class="nf">motorboat_that_sunk</span><span class="p">():</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s">"id"</span><span class="p">:</span> <span class="s">"id_motorboat"</span><span class="p">,</span>
<span class="s">"name"</span><span class="p">:</span> <span class="s">"Titanic"</span><span class="p">,</span>
<span class="s">"construction_year"</span><span class="p">:</span> <span class="mi">1909</span><span class="p">,</span>
<span class="s">"type_of_boat"</span><span class="p">:</span> <span class="s">"motorboat"</span><span class="p">,</span>
<span class="s">"has_sunk"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">def</span> <span class="nf">test_get_number_of_boats_that_sunk</span><span class="p">(</span><span class="n">sailboat</span><span class="p">,</span> <span class="n">motorboat_that_sunk</span><span class="p">):</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert_many</span><span class="p">([</span><span class="n">sailboat</span><span class="p">,</span> <span class="n">motorboat_that_sunk</span><span class="p">])</span>
<span class="k">assert</span> <span class="n">get_number_of_boats_that_sunk</span><span class="p">()</span> <span class="o">==</span> <span class="mi">1</span>
</code></pre></div></div>
<p>Notez l’importante duplication entre les fixtures <code class="language-plaintext highlighter-rouge">motorboat</code> et <code class="language-plaintext highlighter-rouge">motorboat_that_sunk</code>. Ici, pour un seul champ qui change, quatre autres sont dupliqués. Cela rend la lecture du test plus compliquée (car l’information pertinente est noyée par le reste).
Pour tester notre fonction, peu nous importe de savoir de quel type de bateau il s’agit ou en quelle année il a été construit !</p>
<p><strong>Option 2</strong>: Réutilisation puis modification d’une fixture existante</p>
<p>Une autre solution consiste à réutiliser la fixture <code class="language-plaintext highlighter-rouge">motorboat</code> en ajoutant un champ <code class="language-plaintext highlighter-rouge">has_sunk</code> égal à <code class="language-plaintext highlighter-rouge">True</code>.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_get_number_of_boats_that_sunk</span><span class="p">(</span><span class="n">sailboat</span><span class="p">,</span> <span class="n">motorboat</span><span class="p">):</span>
<span class="n">motorboat_that_sunk</span> <span class="o">=</span> <span class="p">{</span>
<span class="o">**</span><span class="n">motorboat</span><span class="p">,</span>
<span class="s">"has_sunk"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert_many</span><span class="p">([</span><span class="n">sailboat</span><span class="p">,</span> <span class="n">motorboat_that_sunk</span><span class="p">])</span>
<span class="k">assert</span> <span class="n">get_number_of_boats_that_sunk</span><span class="p">()</span> <span class="o">==</span> <span class="mi">1</span>
</code></pre></div></div>
<p>On perd à nouveau en lisibilité à cause des détours de code que nous imposent cette solution. Le lecteur est obligé de se référer à la fixture <code class="language-plaintext highlighter-rouge">motorboat</code> alors qu’aucun des champs qui la compose ne nous intéresse. On a détourné l’usage initial de la fixture, et pire encore, on déroge à la sacrosainte règle de lisibilité !</p>
<p>Écrire les tests ainsi rend plus difficile leur prise en main. On risque également de faire apparaître des effets de bord, causés par les autres champs de la fixture utilisée et qui ne seraient pas censés impacter la fonction testée mais dont on aurait oublié de changer la valeur.</p>
<h2 id="les-fixtures-cest-pas-automatique">Les fixtures c’est pas automatique</h2>
<p>Ces quelques exemples mettent en lumière une vérité : <strong>les fixtures ne sont pas adaptées à tous les besoins</strong>. Lorsque les données manipulées sont complexes ou que le comportement des fonctions testées varie grandement en fonction de la valeur des champs, il est souvent
nécessaire de multiplier le nombre de fixtures pour répondre à tous les cas de tests.</p>
<p>Et ne parlons pas des fixtures en <a href="https://docs.pytest.org/en/7.1.x/reference/fixtures.html#autouse-order">autouse</a> pour des données de test ! Ces fixtures n’ont pas besoin d’être appelées explicitement au sein des fonctions de test pour être invoquées.
On a alors l’impression que les données sont instanciées par <a href="https://www.wikiwand.com/en/Magic_(programming)">magie</a> ce qui invisibilise complètement les champs manipulés. Utiliser des fixtures de données en autouse, c’est ouvrir la porte aux effets de bord que les développeurs peuvent parfois mettre longtemps à identifier.</p>
<p>Ce qui nous manque avec ces fixtures, c’est davantage de flexibilité et notamment la possibilité de paramétrer nos données. Le problème est que pytest rend cette paramétrisation complexe puisqu’elle nécessite de faire appel à <code class="language-plaintext highlighter-rouge">@pytest.mark.parametrize</code> et aux <a href="https://docs.pytest.org/en/latest/example/parametrize.html#apply-indirect-on-particular-arguments">paramètres indirects</a>.
Cela alourdit la syntaxe et ne résout guère notre problème de clarté de code.</p>
<p><strong>Mais alors que faire pour éviter le naufrage ?</strong> Comment concilier lisibilité des tests, limitation de la duplication de code et documentation des données manipulées? Eh bien tout simplement en revenant à l’essentiel : nos bonnes vieilles fonctions !</p>
<h3 id="un-test-un-bateau-une-fonction-dinitialisation-de-données">Un test, un bateau, une fonction d’initialisation de données</h3>
<p>Nous sommes tellement habitués à tester l’implémentation de nouveaux frameworks tendance ou à manipuler des concepts complexes que l’on en oublie bien souvent la base de l’informatique. Car une fonction, cela reste une portion de code, que l’on peut invoquer plusieurs fois, avec potentiellement des paramètres
et qui effectue une action ou retourne un résultat en fonction de ces paramètres.</p>
<p>C’est bien ce que l’on souhaite faire ici (en dépit de la simplicité apparente): accéder, depuis nos tests, à des données qui auraient été générées pour des cas de test (et donc en fonction des valeurs passées en argument).
Prenons un peu de recul et essayons de simplifier tout cette histoire de tests. Dans le cas où seuls des voiliers sont persistés, l’utilisation de fonctions donne :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">new_boat</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">construction_year</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">type_of_boat</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s">"id"</span><span class="p">:</span> <span class="nb">id</span><span class="p">,</span>
<span class="s">"name"</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span>
<span class="s">"construction_year"</span><span class="p">:</span> <span class="n">construction_year</span><span class="p">,</span>
<span class="s">"type_of_boat"</span><span class="p">:</span> <span class="n">type_of_boat</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">def</span> <span class="nf">test_get_boat_description</span><span class="p">():</span>
<span class="n">boat</span> <span class="o">=</span> <span class="n">new_boat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"id_sailboat"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"Santa Maria"</span><span class="p">,</span> <span class="n">construction_year</span><span class="o">=</span><span class="mi">1492</span><span class="p">,</span> <span class="n">type_of_boat</span><span class="o">=</span><span class="s">"singlehull"</span><span class="p">)</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert</span><span class="p">(</span><span class="n">boat</span><span class="p">)</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">get_boat_description</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"id_sailboat"</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">description</span> <span class="o">==</span> <span class="s">"Le Santa Maria est un voilier monocoque construit en 1492."</span>
</code></pre></div></div>
<p>L’initialisation des données est certes légèrement plus longue que dans le cas des fixtures (une ligne de plus) mais elle est également bien plus explicite.
À la lecture du test on identifie immédiatement les différentes parties qui le composent :</p>
<ol>
<li>l’initialisation des données de test</li>
<li>l’appel à la fonction à tester</li>
<li>l’assertion sur le résultat (qu’on peut comparer immédiatement avec les données en entrée)</li>
</ol>
<p>Le test se suffit à lui-même et l’on n’a pas besoin de se référer à la fonction <code class="language-plaintext highlighter-rouge">new_boat</code> pour comprendre ce que retourne la fonction <code class="language-plaintext highlighter-rouge">get_boat_description</code>.
Tout ce que l’on teste dans l’output apparaît explicitement dans l’input.</p>
<h3 id="mais-une-fonction-réutilisable-">Mais une fonction réutilisable !</h3>
<p>Ce gain de lisibilité est d’autant plus évident lorsque l’on rajoute un nouveau test sur des données de bateaux à moteur. Il nous suffit en effet d’appeler la fonction <code class="language-plaintext highlighter-rouge">new_boat</code> avec des paramètres différents
(sans même avoir à la modifier) :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_get_boat_description_for_motorboat</span><span class="p">():</span>
<span class="n">motorboat</span> <span class="o">=</span> <span class="n">new_boat</span><span class="p">(</span> <span class="nb">id</span><span class="o">=</span><span class="s">"id_motorboat"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">"Titanic"</span><span class="p">,</span> <span class="n">construction_year</span><span class="o">=</span><span class="mi">1909</span><span class="p">,</span> <span class="n">type_of_boat</span><span class="o">=</span><span class="s">"motorboat"</span><span class="p">)</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert</span><span class="p">(</span><span class="n">motorboat</span><span class="p">)</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">get_boat_description</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"id_motorboat"</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">description</span> <span class="o">==</span> <span class="s">"Le Titanic est un bateau à moteur construit en 1909."</span>
</code></pre></div></div>
<h3 id="lavantage-des-paramètres-par-défaut">L’avantage des paramètres par défaut</h3>
<p>Enfin, si l’on souhaite tester un seul des champs de notre document (ici le champ <code class="language-plaintext highlighter-rouge">has_sunk</code>), il n’est plus nécessaire de dupliquer du code. En ajoutant des valeurs par défaut aux
paramètres de notre fonction <code class="language-plaintext highlighter-rouge">new_boat</code>, on a la possibilité de ne passer en argument que les valeurs des champs que l’on souhaite tester, rendant ainsi très explicite le cas de test.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">new_boat</span><span class="p">(</span>
<span class="nb">id</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"boat_id"</span><span class="p">,</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"boat_name"</span><span class="p">,</span>
<span class="n">construction_year</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1970</span><span class="p">,</span>
<span class="n">type_of_boat</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"sailboat"</span><span class="p">,</span>
<span class="n">has_sunk</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s">"id"</span><span class="p">:</span> <span class="nb">id</span><span class="p">,</span>
<span class="s">"name"</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span>
<span class="s">"construction_year"</span><span class="p">:</span> <span class="n">construction_year</span><span class="p">,</span>
<span class="s">"type_of_boat"</span><span class="p">:</span> <span class="n">type_of_boat</span><span class="p">,</span>
<span class="s">"has_sunk"</span><span class="p">:</span> <span class="n">has_sunk</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">def</span> <span class="nf">test_get_number_of_boats_that_sunk</span><span class="p">():</span>
<span class="n">boat</span> <span class="o">=</span> <span class="n">new_boat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"boat_still_floating"</span><span class="p">,</span> <span class="n">has_sunk</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">boat_that_sunk</span> <span class="o">=</span> <span class="n">new_boat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"boat_that_sunk"</span><span class="p">,</span> <span class="n">has_sunk</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert_many</span><span class="p">([</span><span class="n">boat</span><span class="p">,</span> <span class="n">boat_that_sunk</span><span class="p">])</span>
<span class="k">assert</span> <span class="n">get_number_of_boats_that_sunk</span><span class="p">()</span> <span class="o">==</span> <span class="mi">1</span>
</code></pre></div></div>
<p>Un comparatif des deux versions (et notamment du nombre de lignes de code requis par l’usage des fonctions ou des fixtures) est disponible <a href="https://codesandbox.io/p/sandbox/data-fixtures-in-tests-vr6w2s">ici</a>.</p>
<h2 id="et-si-on-veut-aller-plus-loin-">Et si on veut aller plus loin ?</h2>
<p>On peut évidemment complexifier ces fonctions pour les adapter à nos besoins. Par exemple en effectuant les interactions avec notre base de données directement au sein de la fonction pour ne pas avoir à répéter les insertions successives.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">persist_new_boat</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">construction_year</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">type_of_boat</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
<span class="n">boat</span> <span class="o">=</span> <span class="n">new_boat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="nb">id</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="n">name</span><span class="p">,</span> <span class="n">construction_year</span><span class="o">=</span><span class="n">construction_year</span><span class="p">,</span> <span class="n">type_of_boat</span><span class="o">=</span><span class="n">type_of_boat</span><span class="p">)</span>
<span class="n">get_entity</span><span class="p">(</span><span class="s">"boats"</span><span class="p">).</span><span class="n">insert</span><span class="p">(</span><span class="n">boat</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_get_number_of_boats_that_sunk</span><span class="p">():</span>
<span class="n">persist_new_boat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"boat_still_floating"</span><span class="p">,</span> <span class="n">has_sunk</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">persist_new_boat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s">"boat_that_sunk"</span><span class="p">,</span> <span class="n">has_sunk</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">get_number_of_boats_that_sunk</span><span class="p">()</span> <span class="o">==</span> <span class="mi">1</span>
</code></pre></div></div>
<p>On peut également manipuler des structures de données plus complexes, voire introduire la notion de <a href="https://docs.python.org/3/library/dataclasses.html">Dataclass</a> pour vérifier le format des données testées.
La rigueur imposée par ce format nous permet de mieux documenter nos modèles de données.</p>
<p>Plus ces fonctions seront utilisées par les tests, plus elles deviendront robustes. Chaque cas de test permettra de complexifier la logique d’insertion de données
pour prendre en compte cette diversité de champs et de valeurs. C’est à l’usage qu’elles seront éprouvées, permettant ainsi de documenter un peu plus chaque jour les cas possibles et les
comportements attendus. Je ne saurais que vous conseiller de commencer par des fonctions très simples quitte à rajouter des champs ou des exceptions au fur et à mesure que l’application (et donc vos tests) évolue.</p>
<p>C’est à ce moment là que les <a href="https://pytest-factoryboy.readthedocs.io/en/stable/">Factories</a> peuvent entrer en jeu. Elles permettent d’instancier plusieurs types d’objets différents (qui donneront lieu à des comportements différents dans les tests) à partir d’une même classe abstraite.
Ce <a href="/some-python-design-patterns/">design pattern</a> est particulièrement adapté aux données complexes telles que celles collectées chez Deepki.</p>
<h2 id="le-mot-de-la-fin">Le mot de la fin</h2>
<p>On l’a vu, l’utilisation des fixtures pytest soulève un certain nombre de problèmes. On ne peut pas les paramétrer facilement pour correspondre à différents cas de test ce qui nous oblige souvent à dupliquer le code.
La lisibilité des tests en est également impactée. Les fixtures se fondent sur une instanciation implicite des données de test qui nous impose souvent des allers-retours entre les fixtures et les tests qui les invoquent (les deux étant parfois définis dans des fichiers distincts).
Les tests ne se suffisent plus à eux-même (et dépendent de données “magiques”) alimentées par les fixtures) et c’est ce qui me déplaît le plus ici.</p>
<p>Retourner aux basiques pour initialiser les données de test est bien souvent préférable. Les simples fonctions, si elles sont découpées correctement, nommées de manière explicite et conformes
à la réalité du modèle de données manipulé suffisent généralement à représenter la diversité des cas que l’on souhaite tester. Leur flexibilité, leur facilité d’utilisation et leur lisibilité sont autant d’atouts à faire
pâlir les fixtures.</p>
<p><strong>Vive la simplicité, vive la lisibilité… et vive les fonctions !</strong></p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>Robert C. Martin, <em>Clean Code: A Handbook of Agile Software Craftsmanship</em>, Pearson, 2008 <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>Julia CavicchiIl est désormais communément admis que les les tests sont essentiels pour garantir le bon fonctionnement d’une application et faciliter sa maintenance. Ils permettent de s’assurer de la non-régression de certains comportements mais également de les documenter pour de futurs développeurs. Plus les tests sont lisibles et explicites, plus on est en mesure de comprendre comment le code fonctionne. Robert C. Martin, va jusqu’à déclarer dans son livre “Clean Code”1: Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, Pearson, 2008 ↩Introduction a l’éco-conception numérique2023-10-17T00:00:00+00:002023-10-17T00:00:00+00:00https://techblog.deepki.com//introduction-to-green-it<h1 id="léco-conception-numérique">L’éco-conception numérique</h1>
<h2 id="cest-quoi-léco-conception">C’est quoi l’éco-conception ?</h2>
<p>L’éco-conception ou conception écologique, est une approche générale qui vise a intégrer des considérations environnementales tout au long du cycle de vie d’un produit ou d’un service.
Son principe fondamental repose sur les 3 R : Réduire, Réutiliser et Recycler.</p>
<ul>
<li><strong>Réduire</strong> : consiste à limiter l’empreinte écologique en utilisant moins de matières premières, d’énergie et en évitant le gaspillage.</li>
<li><strong>Réutiliser</strong> : encourage l’utilisation prolongée des produits en les concevant pour être réparables, modulables ou transformables. Cela permet d’allonger leur durée de vie et de diminuer la quantité de déchets générés.</li>
<li><strong>Recycler</strong> : consiste à valoriser les déchets en les transformant en nouvelles matières premières.</li>
</ul>
<p>L’éco-conception est un pilier essentiel de la transition vers un mode de consommation responsable, car elle offre une voie concrète et pragmatique pour réduire l’impact sur l’environnement et favoriser l’essor de solutions durables.</p>
<h2 id="cest-quoi-léco-conception-appliquée-au-numérique">C’est quoi l’éco-conception appliquée au numérique ?</h2>
<p>L’éco-conception numérique qu’on peut appeler aussi “green IT”, est une approche qui vise à intégrer des critères environnementaux à la conception, au développement et à l’usage de solutions numériques.<br />
Dans le monde du développement, cette approche cherche à réduire l’impact environnemental des applications tout en favorisant l’efficacité, la durabilité et la sobriété.</p>
<h2 id="limpact-environnemental-du-numérique--des-chiffres">L’impact environnemental du numérique : des chiffres !</h2>
<ul>
<li>En 2016, Internet atteint <a href="https://fr.statista.com/infographie/26337/evolution-du-nombre-de-personnes-ayant-acces-ou-pas-a-internet-dans-le-monde/">62%</a> de la population mondiale. En 2021, 62% de la population était concerné, ce qui représentait déjà 4,9 milliards d’êtres humains. Sachant qu’il faut compter une croissance en terme de consommation de <a href="https://theshiftproject.org/article/pour-une-sobriete-numerique-rapport-shift/">9%</a> par an.</li>
<li>Le numérique représente <a href="https://www.arcep.fr/la-regulation/grands-dossiers-thematiques-transverses/lempreinte-environnementale-du-numerique.html#:~:text=L'impact%20des%20r%C3%A9seaux%20de,empreinte%20carbone%20nationale%20%5B4%5D.">3 à 4%</a> des émissions de gaz à effet de serre (GES) dans le monde.</li>
<li>La consommation mondiale de streaming vidéo émet <a href="https://www.grizzlead.com/lincroyable-impact-de-la-pollution-numerique-et-les-bonnes-pratiques-a-adopter-tres-vite/">300 millions de tonnes</a> de CO₂ chaque année.</li>
<li>En France <a href="https://www.blogdumoderateur.com/digital-france-monde-2022/">41%</a> des TPE, PME et ETI confirment que le recours au numérique est nécessaire pour transformer leurs actions business en croissance.</li>
<li>La quantité de déchets électriques ou électroniques représentait <a href="https://www.strategie.gouv.fr/publications/consommation-de-metaux-numerique-un-secteur-loin-detre-dematerialise#:~:text=Elle%20devrait%20ainsi%20atteindre%2052,traitement%20non%20conforme%20aux%20normes.">52 millions</a> de tonnes en 2021, contre 45 millions de tonnes en 2016.</li>
</ul>
<h2 id="les-bonnes-pratiques">Les bonnes pratiques</h2>
<h3 id="conception">Conception</h3>
<h4 id="limiter-les-fonctionnalités-inutiles">Limiter les fonctionnalités inutiles</h4>
<p><a href="https://www.axiocode.com/eco-conception-les-bonnes-pratiques">45%</a> des fonctionnalités demandées ne sont jamais utilisées. Il faut donc, avant toute chose, se demander si la fonctionnalité que l’on veut développer est vraiment utile, éventuellement sonder les utilisateurs finaux et enfin analyser l’usage de la fonctionnalité et ne pas hésiter à la supprimer si elle apporte peu de valeur.
Cela fera du code en moins, des appels réseaux en moins, du temps de processeur en moins et au final une consommation d’énergie moindre.</p>
<h4 id="estimer-limpact-environnemental-de-vos-pages">Estimer l’impact environnemental de vos pages</h4>
<p>En faisant ce travail d’analyse, vous pouvez prendre conscience de l’impact de votre application et visualiser la progression de vos améliorations.
Il existe plusieurs outils permettant d’analyser et de noter l’impact environnemental des applications web. En voici quelques-uns :</p>
<ul>
<li>GreenIT, disponible sous forme d’<a href="https://chrome.google.com/webstore/detail/greenit-analysis/mofbfhffeklkbebfclfaiifefjflcpad?hl=fr">extension</a></li>
<li><a href="https://www.websitecarbon.com/">websitecarbon.com</a></li>
<li><a href="http://www.ecometer.org/">ecometer.org</a></li>
<li>EcoIndex, disponible sour form d’<a href="https://chrome.google.com/webstore/detail/ecoindexfr/apeadjelacokohnkfclnhjlihklpclmp">Extension</a></li>
</ul>
<p>Il est également possible de mesurer notre impact au niveau de l’intégration continue. Ainsi, il nous est possible de visualiser notre progression et de voir concrètement les progrès réalisés.
Il existe différents scripts réalisant cette mesure disponible sur Github. Mais une solution un peu plus complète semble être <a href="https://greenframe.io/">GreenFrame</a>.</p>
<h4 id="simplifier-lutilisation">Simplifier l’utilisation</h4>
<p>Simplifier les parcours utilisateur permet de limiter les chargements de pages et les appels réseau non sollicités. Une utilisation fluide permet également de limiter la présence de l’utilisateur sur l’application et donc de limiter l’utilisation de ressources clientes comme serveur.</p>
<h4 id="mobile-first">Mobile first</h4>
<p>Une approche de conception mobile-first est, par essence, plus éco-conçue qu’une conception classique. Une telle approche doit mettre l’accent sur l’optimisation des performances, car la puissance de l’appareil et la consommation de données sont des facteurs limitants dans ce contexte.
La taille de l’écran impose aussi de réfléchir à une interface plus simple, qui va a l’essentiel, donc avec moins d’interactions et de données à charger. Une interface plus simple permet un usage plus aisé et donc moins de temps passé par l’utilisateur à utiliser son appareil et solliciter les serveurs.</p>
<h3 id="réalisation">Réalisation</h3>
<h4 id="choix-des-technologies">Choix des technologies</h4>
<p>Faire un benchmark des solutions disponibles sur le marché avec comme premiers critères le poids et la vitesse d’exécution peuvent être des facteurs de réduction de l’impact environnemental.
Utiliser un framework Javascript plutôt que du code vanilla n’est pas sans conséquences. On peut par exemple voir un écart de 27% de la consommation d’énergie dans <a href="https:,,www.diva-portal.org,smash,get,diva2:1768632,FULLTEXT01.pdf">cette étude (pdf)</a>, entre vanilla et React. </p>
<h4 id="optimiser-les-poids-des-images">Optimiser les poids des images</h4>
<p>Les images et illustrations représentent une part importante du poids d’une application web. C’est pour cela qu’il est important de les optimiser :</p>
<ul>
<li>La taille : si l’image doit-être redimensionnée via du CSS, c’est qu’elle n’est pas à la bonne taille. Il faut limiter cela en utilisant des fichiers déjà aux bonnes dimensions.</li>
<li>Le format : certains formats sont plus lourds que d’autres. Pour les photos, privilégiez JPEG ou WEBP. Pour les illustrations, le format SVG. Pour les icônes, plutôt GLYPHS, CSS ou SVG à défaut. Le SVG a aussi l’avantage de pouvoir être intégré dans le HTML directement au build de l’application, ce qui annulera le besoin de faire une requête pour l’utilisateur.<br />
Dans <a href="https://greenspector.com/en/which-image-format-to-choose-to-reduce-its-energy-consumption-and-its-environmental-impact/">cet article</a> nous pouvons voir l’impact en terme de consommation d’énergie en fonction de certains formats.</li>
</ul>
<p>Il existe aussi des moyens en HTML pour charger la bonne image en fonction de la taille de l’écran. Ainsi l’élément <code class="language-plaintext highlighter-rouge">source</code> peut être associé à l’élément <code class="language-plaintext highlighter-rouge">picture</code> afin de charger la bonne image.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><picture></span>
<span class="nt"><source</span> <span class="na">media=</span><span class="s">"(min-width:650px)"</span> <span class="na">srcset=</span><span class="s">"img_pink_flowers.jpg"</span><span class="nt">></span>
<span class="nt"><source</span> <span class="na">media=</span><span class="s">"(min-width:465px)"</span> <span class="na">srcset=</span><span class="s">"img_white_flower.jpg"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"img_orange_flowers.jpg"</span> <span class="na">alt=</span><span class="s">"Flowers"</span> <span class="na">style=</span><span class="s">"width:auto;"</span><span class="nt">></span>
<span class="nt"></picture></span>
</code></pre></div></div>
<p>Héberger ses images dans un CDN permet de réduire la distance entre l’utilisateur et le serveur et donc d’utiliser moins d’énergie pour charger l’élément (Ex: serveur d’image en Europe pour un utilisateur européen).</p>
<p>Charger ses images en mode <code class="language-plaintext highlighter-rouge">lazy</code>, donc seulement lorsque l’utilisateur cherche à l’afficher est également une bonne pratique.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><img</span> <span class="na">src=</span><span class="s">"image.jpg"</span> <span class="na">alt=</span><span class="s">"..."</span> <span class="na">loading=</span><span class="s">"lazy"</span> <span class="nt">/></span>
</code></pre></div></div>
<h4 id="limiter-les-vidéos">Limiter les vidéos</h4>
<p>Les vidéos représentent une part importante du trafic mondial. Plus de la moitié de la bande passante y est consacrée.</p>
<p>La première chose à faire est de n’en utiliser que lorsque c’est important. Peut-être qu’une illustration, une infographie ou encore mieux, un texte peut remplacer la vidéo. Il est possible d’optimiser la taille et le poids des vidéos si vraiment c’est nécessaire, mais en fonction de la durée, son poids restera important en comparaison des autres options possibles.
Enfin, il ne faut pas que les vidéos se lancent automatiquement. Pourquoi charger une vidéo que l’utilisateur n’a pas demandé à visionner ?</p>
<h4 id="utiliser-des-polices-natives">Utiliser des polices natives</h4>
<p>Certaines polices sont intégrées aux différents systèmes d’exploitation. Les utiliser permet de limiter le poids de certaines applications et l’usage du réseau nécessaire pour les charger. Voici une liste de polices présentent sur la majorité des systèmes d’exploitation :</p>
<ul>
<li>Arial (sans-serif)</li>
<li>Verdana (sans-serif)</li>
<li>Helvetica (sans-serif)</li>
<li>Tahoma (sans-serif)</li>
<li>Trebuchet MS (sans-serif)</li>
<li>Times New Roman (serif)</li>
<li>Georgia (serif)</li>
<li>Garamond (serif)</li>
<li>Courier New (monospace)</li>
<li>Brush Script MT (cursive)</li>
</ul>
<h4 id="limiter-les-interactions-et-les-animations">Limiter les interactions et les animations</h4>
<p>Les applications web modernes font la part belle à l’interactivité. Mais cela a un coût énergétique non-négligeable. Il est essentiel de les limiter quand cela est possible.
L’exemple des chatbots est très parlant, car il est là pour remplacer un parcours en général simple par un dialogue simulé. Cela nécessite beaucoup de requêtes et d’interactions au lieu de quelques clicks.
Les animations consomment également de l’énergie, principalement sur le terminal client. Elles permettent souvent une interaction plus ludique, mais plus consommatrice d’énergie en contrepartie.</p>
<h4 id="limiter-lusage-de-script-tiers">Limiter l’usage de script tiers</h4>
<p>L’intégration de script tiers dans votre application peut embarquer parfois du code inutile pour vous. Un exemple est celui des intégrations de réseaux sociaux. Beaucoup de code est inséré dans la page là où une image (voire un texte) cliquable pourrait suffire.</p>
<h4 id="bundler-et-minification">Bundler et minification</h4>
<p>Faire en sorte que le bundle de l’application soit le plus léger possible en n’y intégrant que ce qui est utile à l’application (Ex: tree shaking) et minifier son code permet de réduire le poids du bundle de 20 à 80%.</p>
<h2 id="les-bénéfices-non-environnementaux">Les bénéfices non-environnementaux</h2>
<h3 id="seo">SEO</h3>
<p>La vitesse de chargement et la compatibilité mobile sont deux facteurs importants dans la SEO. Un site éco-conçu se doit d’optimiser ces deux aspects et donc, par essence, un site éco-conçu sera favorisé par les moteurs de recherche.</p>
<h3 id="accéssibilité">Accéssibilité</h3>
<p>Une conception légère et une optimisation du code améliorent la compatibilité avec les lecteurs d’écran et tout autre dispositif d’assistance.
Une interface plus simple, souvent une conséquence de l’éco-conception, limite les obstacles cognitifs pour les utilisateurs ayant des troubles de l’apprentissage ou de l’attention.</p>
<h2 id="conclusion">Conclusion</h2>
<p>L’éco-conception numérique est une approche globale qui va de la conception, au développement, jusqu’à l’exploitation d’une application. C’est notre premier levier d’action à nous, artisans du numérique, dans un monde en quête de croissance infinie.</p>
<h2 id="pour-aller-plus-loin">Pour aller plus loin</h2>
<ul>
<li><a href="https://ecoresponsable.numerique.gouv.fr/publications/referentiel-general-ecoconception/">Référentiel général d’écoconception de services numériques (RGESN)</a></li>
<li><a href="https://www.w3.org/blog/2023/introducing-web-sustainability-guidelines/">W3C - Introducing Web Sustainability Guidelines</a></li>
<li><a href="https://github.com/cnumr/best-practices">Green IT - Les 115 bonnes pratiques</a></li>
<li><a href="https://collectif.greenit.fr/ecoconception-web/2022-05-Ref-eco_web-checklist.v4.pdf">Green IT - check-list (pdf)</a></li>
<li><a href="https://theshiftproject.org/article/pour-une-sobriete-numerique-rapport-shift/">The shift project - Pour une sobriété numérique</a></li>
<li><a href="https://www.grizzlead.com/lincroyable-impact-de-la-pollution-numerique-et-les-bonnes-pratiques-a-adopter-tres-vite/">L’incroyable impact de la pollution numérique et les bonnes pratiques à adopter très vite !</a></li>
<li><a href="https://greenmetrics.io/blog/infographie-impact-numerique-environnement">Infographie - L’impact du numérique sur l’environnement</a></li>
<li><a href="https://sustainablewebdesign.org/">What is sustainable web design</a></li>
</ul>Jean-Baptiste BergyL’éco-conception numérique C’est quoi l’éco-conception ?La charte du développement backend chez Deepki - Partie 3, les tests automatisés2023-09-11T00:00:00+00:002023-09-11T00:00:00+00:00https://techblog.deepki.com//backend-charter-part-3<p>Cet article est le troisième d’une série qui détaille la charte du développement backend que nous avons mise en place chez Deepki. Dans cet article, nous abordons en détail le sujet des tests automatisés.</p>
<ul>
<li><a href="/backend-charter-part-1/">Partie 1, les grands principes</a></li>
<li><a href="/backend-charter-part-2/">Partie 2, la lisibilité</a></li>
</ul>
<h1 id="limportance-des-tests-automatiques">L’importance des tests automatiques</h1>
<p>Dans le développement d’une application, les tests automatiques sont indispensables pour garantir la qualité et la stabilité du code. Les tests automatisés permettent de s’assurer que les modifications apportées au code n’ont pas accidentellement altéré son comportement existant.</p>
<p>Cependant, pour que les tests soient efficaces et utiles, il est essentiel de garder à l’esprit les principes suivants :</p>
<h3 id="tester-pour-les-autres-développeurs">Tester pour les autres développeurs</h3>
<p>L’objectif principal des tests automatisés est d’assurer la stabilité du code pour les autres développeurs. Les tests ne sont pas conçus pour prouver que le code fonctionne, mais pour empêcher les modifications futures de casser accidentellement le code existant.</p>
<p>Les tests sont un filet de sécurité. En tant que tel, il est donc essentiel de tester tous les cas possibles, pas seulement les cas nominaux, et d’inclure les cas “limites” : par exemple, si on teste une librairie de pagination, que se passe-t-il lorsqu’on demande la page 0 ? Ou la page 9999999 ? Ou qu’on demande à afficher -1 objets par page ?</p>
<p>Tous ces tests servent également de documentation sur des comportements <em>prévus</em>.</p>
<h3 id="garantir-des-comportements">Garantir des comportements</h3>
<p>On écrit toujours un test automatisé pour protéger un <em>comportement</em> contre une altération accidentelle. Il faut donc se poser la question de quels comportements on souhaite garantir.</p>
<p>Le contrat d’interface d’une API web par exemple ne se limite pas toujours à la liste des arguments en entrée et au format de la réponse. On peut également vouloir garantir que lorsqu’on appelle cette API, on émet une métrique, on ajoute une ligne de log, on publie un évènement, on appelle un service externe, etc. Si ces comportements sont importants pour “le monde extérieur”, il faut les protéger et les documenter à travers un test automatisé.</p>
<h3 id="se-découpler-de-limplémentation">Se découpler de l’implémentation</h3>
<p>On a déjà énoncé dans cette charte l’idée que pour garder notre application moderne et facile à maintenir, il est essentiel de la “refactorer” régulièrement. Pour rappel, le “refactoring” consiste à modifier l’implémentation d’une fonctionnalité sans en changer le comportement.</p>
<p>Les tests automatisés doivent être au service du refactoring. En protégeant tous les comportements attendus de l’application, ils offrent au développeur un filet de sécurité qui lui permet de sereinement réécrire les rouages internes de son application en sachant qu’il n’en n’a pas altéré le comportement attendu par “le monde extérieur”.</p>
<p>Mais pour ça, il faut bien entendu que les tests n’utilisent pas ces rouages internes directement, car ce sont typiquement eux qu’on va vouloir remplacer !</p>
<p>Il est donc important de tester tous les comportements de son application, mais de le faire en partant de sa surface d’attaque (qui ne change pas lors d’un refactoring) plutôt que de l’intérieur (qui doit être facile à changer aussi souvent qu’on veut).</p>
<p>Tout ce qui rend le refactoring plus difficile doit être évité, et un test sur une fonction interne qu’on souhaite retirer ou fusionner alors que le comportement global du produit reste le même entre dans cette catégorie.</p>
<h3 id="écrire-des-tests-rapides-indépendants-autonomes-stables-et-concis">Écrire des tests rapides, indépendants, autonomes, stables et concis</h3>
<p>Les tests automatisés doivent être rapides, indépendants, autonomes, stables et concis pour être efficaces. Voici ce que cela signifie :</p>
<ul>
<li>Ils doivent être rapides pour que les développeurs puissent les exécuter fréquemment.</li>
<li>Ils doivent être indépendants pour que les tests aient le même résultat quel que soit l’ordre dans lequel ils sont lancés. Idéalement, il doit être possible de les lancer en parallèle, ce qui participe à l’objectif de rapidité.</li>
<li>Ils doivent être autonomes, c’est-à-dire que la lecture du code du test doit suffire à comprendre ce qu’il fait et ce qu’il teste. Quand un test casse, on veut être capable de comprendre pourquoi le plus rapidement possible. Plus le code du test se suffit à lui-même, plus ce sera le cas.</li>
<li>Ils doivent être stables pour que les résultats des tests soient cohérents : un test qui échoue 1 fois sur 10 sans que le code ait été modifié est très frustrant. Ceci doit être vrai indépendamment de l’environnement depuis lequel les tests sont lancés (laptop, CI, etc.).</li>
<li>Ils doivent être concis pour être compréhensibles et pour faciliter la maintenance. Il faut cependant trouver le bon équilibre entre concision et autonomie, l’objectif étant toujours que la personne en charge de réparer un test comprenne ce que fait le test le plus rapidement possible.</li>
</ul>
<h3 id="qualité-du-code-de-test">Qualité du code de test</h3>
<p>La qualité du code de test est aussi importante que celle du code de production. Il est donc normal de passer autant de temps à écrire des tests qu’à écrire du code de production, et il n’est pas rare ou choquant que le volume de code des tests soit plus important que celui du code de production.</p>
<h3 id="couverture-de-code">Couverture de code</h3>
<p>Il est souhaitable que les tests automatisés couvrent 100 % du code, mais il ne faut pas le forcer. La couverture n’est pas une mesure de la qualité des tests, car ce qui compte, c’est de faire les bonnes assertions. La qualité des tests ne peut être représentée par un simple chiffre : seul une relecture attentive du code permettra de juger de la qualité des tests produits.</p>
<h1 id="conclusion">Conclusion</h1>
<p>En respectant ces principes, les tests automatisés peuvent aider à améliorer la qualité et la fiabilité du code tout en facilitant le processus de développement et de maintenance.</p>Olivier RouxCet article est le troisième d’une série qui détaille la charte du développement backend que nous avons mise en place chez Deepki. Dans cet article, nous abordons en détail le sujet des tests automatisés.Le duel de programmation : Python et Elixir face à face2023-07-17T00:00:00+00:002023-07-17T00:00:00+00:00https://techblog.deepki.com//python_vs_elixir<p><img src="/assets/img/python_vs_elixir/python-vs-elixir.png" class="" /></p>
<meta property="og:image" content="https://techblog.deepki.com/assets/img/python_vs_elixir/python-vs-elixir.png" />
<p>L’univers de la programmation offre une multitude de langages, chacun avec ses propres avantages, inconvénients et cas d’utilisation spécifiques.</p>
<p>Aujourd’hui, nous allons discuter de deux langages puissants et flexibles : <a href="https://www.python.org/">Python</a> et <a href="https://elixir-lang.org/">Elixir</a>.</p>
<h2 id="-avantages-et-inconvénients">📊 Avantages et inconvénients</h2>
<h3 id="python-">Python 🐍</h3>
<p>Python est l’un des langages les plus populaires et les plus utilisés dans le monde de la programmation.</p>
<table>
<thead>
<tr>
<th>👍 <strong>Avantages:</strong></th>
<th>👎 <strong>Inconvénients:</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Syntaxe simple et lisible :</strong> Python est connu pour sa syntaxe claire et concise qui favorise la lisibilité du code. Cela le rend particulièrement bon pour les débutants en programmation.</td>
<td><strong>Vitesse :</strong> Python n’est pas aussi rapide que d’autres langages tels que C ou Java. Cela peut être un inconvénient pour les applications nécessitant une performance de haut niveau.</td>
</tr>
<tr>
<td><strong>Polyvalent :</strong> Python peut être utilisé dans divers domaines, tels que le développement web, l’analyse de données, l’apprentissage automatique, l’IA, etc.</td>
<td><strong>Absence de concurrence native :</strong> Bien que Python supporte le multi-threading, il ne le gère pas aussi bien que d’autres langages en raison de son <a href="https://realpython.com/python-gil/" title="GIL (Global Interpreter Lock)">GIL (Global Interpreter Lock)</a>.</td>
</tr>
<tr>
<td><strong>Grande communauté :</strong> En raison de son ancienneté et de sa popularité, Python a une <a href="https://survey.stackoverflow.co/2023/#technology-most-popular-technologies">grande communauté</a> de développeurs (source: Enquête Stack Overflow 2023). Cela signifie que vous pouvez trouver de l’aide facilement et que beaucoup de bibliothèques sont disponibles pour faciliter le développement.</td>
<td><strong>Dépendance sur le système de type dynamique :</strong> Python utilise un système de typage dynamique, ce qui signifie que les erreurs de type ne sont souvent découvertes qu’à l’exécution du programme. Cela peut entraîner des problèmes de débogage et rendre le code plus difficile à comprendre, surtout pour les grands projets.</td>
</tr>
</tbody>
</table>
<h3 id="elixir-">Elixir 💧</h3>
<p>Elixir est un langage relativement nouveau qui fonctionne sur la machine virtuelle <a href="https://www.erlang.org/">Erlang</a>. Il est conçu pour la concurrence, la distribution et la tolérance aux pannes.</p>
<table>
<thead>
<tr>
<th>👍 <strong>Avantages:</strong></th>
<th>👎 <strong>Inconvénients:</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Haute performance :</strong> Elixir est performant et très adapté à la concurrence grâce à son exécution sur la <a href="https://www.erlang.org/blog/a-brief-beam-primer/" title="VM Erlang">VM Erlang</a>.</td>
<td><strong>Petite communauté :</strong> Elixir a une communauté beaucoup plus petite que Python. Cela signifie moins de ressources, de bibliothèques et d’opportunités d’apprendre auprès d’autres.</td>
</tr>
<tr>
<td><strong>Scalable :</strong> Elixir peut gérer un grand nombre de connexions simultanées, ce qui le rend idéal pour les applications Web en temps réel et les systèmes distribués.</td>
<td><strong>Courbe d’apprentissage :</strong> En tant que langage fonctionnel, Elixir peut être plus difficile à apprendre pour ceux qui sont habitués aux langages impératifs comme Python.</td>
</tr>
<tr>
<td><strong>Programmation fonctionnelle :</strong> Elixir, en tant que langage fonctionnel, facilite la création de code testable et maintenable.</td>
<td><strong>Moins d’opportunités d’emploi :</strong> En raison de sa jeunesse et de sa nature de niche, il y a moins d’opportunités d’emploi pour les développeurs Elixir par rapport à des langages plus établis comme Python. Cependant, c’est une tendance qui peut changer avec le temps et la croissance du langage.</td>
</tr>
<tr>
<td> </td>
<td><strong>Système de type dynamique :</strong> Tout comme Python, Elixir utilise un système de typage dynamique. Cela signifie que les erreurs de type ne sont généralement découvertes qu’à l’exécution du programme, ce qui peut rendre le débogage plus difficile, particulièrement pour les grands projets.</td>
</tr>
</tbody>
</table>
<h2 id="-syntaxe">📖 Syntaxe</h2>
<h3 id="python--1">Python 🐍</h3>
<p>Python a une syntaxe très propre et facile à lire. Voici un exemple de code en Python :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">saluer</span><span class="p">(</span><span class="n">nom</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Bonjour </span><span class="si">{</span><span class="n">nom</span><span class="si">}</span><span class="s">!"</span><span class="p">)</span>
<span class="n">saluer</span><span class="p">(</span><span class="s">"Alice"</span><span class="p">)</span>
</code></pre></div></div>
<p>Dans cet exemple, une fonction <code class="language-plaintext highlighter-rouge">saluer</code> est définie avec le mot-clé <code class="language-plaintext highlighter-rouge">def</code>, puis elle est appelée en passant une chaîne de caractères comme argument.</p>
<h3 id="elixir--1">Elixir 💧</h3>
<p>Elixir a une syntaxe qui ressemble à <a href="https://www.ruby-lang.org/en/" title="Ruby">Ruby</a> et qui est également assez lisible. Voici un exemple similaire en Elixir :</p>
<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">defmodule</span> <span class="no">Salutation</span> <span class="k">do</span>
<span class="k">def</span> <span class="n">saluer</span><span class="p">(</span><span class="n">nom</span><span class="p">)</span> <span class="k">do</span>
<span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="s2">"Bonjour </span><span class="si">#{</span><span class="n">nom</span><span class="si">}</span><span class="s2">!"</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Salutation</span><span class="o">.</span><span class="n">saluer</span><span class="p">(</span><span class="s2">"Alice"</span><span class="p">)</span>
</code></pre></div></div>
<p>Dans cet exemple, une fonction <code class="language-plaintext highlighter-rouge">saluer</code> est définie à l’intérieur du module <code class="language-plaintext highlighter-rouge">Salutation</code> avec le mot-clé <code class="language-plaintext highlighter-rouge">def</code>, puis elle est appelée en passant une chaîne de caractères comme argument.</p>
<p>Comme vous pouvez le voir, la syntaxe de ces deux langages est assez différente, même si les deux sont lisibles. Python a une syntaxe plus minimaliste, tandis qu’Elixir nécessite un peu plus de structure en enveloppant les fonctions à l’intérieur de modules. Toutefois, ce n’est pas nécessairement une mauvaise chose, car cela peut contribuer à une meilleure organisation du code dans de grands projets.</p>
<h2 id="-performance">🚀 Performance</h2>
<p>La performance peut être un critère important dans le choix d’un langage de programmation, en particulier pour des applications qui doivent gérer de grandes quantités de données en temps réel ou qui ont des exigences de temps d’exécution strictes.</p>
<table>
<thead>
<tr>
<th><strong>Python</strong> 🐍</th>
<th><strong>Elixir</strong> 💧</th>
</tr>
</thead>
<tbody>
<tr>
<td>Python, en tant que langage interprété, a des performances respectables dans de nombreux scénarios, en particulier lorsqu’il s’agit de manipuler de grandes structures de données. Cependant, son principal obstacle en termes de performance est sa dépendance à un unique processeur (CPU) à cause du Global Interpreter Lock (GIL). Toutefois, Python propose des fonctionnalités comme <a href="https://docs.python.org/3/library/multiprocessing.html">multiprocessing</a>, qui permettent d’exécuter du code en parallèle sur plusieurs cœurs en créant des processus séparés. Cela peut être très efficace pour les tâches indépendantes qui peuvent être divisées en unités de travail parallèles. Néanmoins, pour les tâches plus complexes nécessitant une communication interprocessus où le partage d’état, cette approche peut devenir plus difficile à gérer et présenter un surcoût en termes de performances. Le support de <a href="https://docs.python.org/3/library/asyncio-task.html">async/await</a> en Python offre également une manière d’effectuer des opérations asynchrones, bien que ces tâches restent limitées à un seul cœur.</td>
<td>Elixir, en revanche, brille dans la manière dont il exploite les ressources de la machine. Fondé sur la VM Erlang et implémentant <a href="https://blog.zenika.com/2019/01/30/la-machine-virtuelle-erlang/">le modèle d’acteur</a>, Elixir est capable de distribuer le travail de manière transparente sur tous les processeurs d’une machine ou même d’un cluster de plusieurs machines. Cela le rend particulièrement efficace pour les serveurs web ou toute application nécessitant des interactions fréquentes avec une base de données ou un cache en mémoire.</td>
</tr>
</tbody>
</table>
<h2 id="-facilité-dapprentissage">🎓 Facilité d’apprentissage</h2>
<table>
<thead>
<tr>
<th><strong>Python</strong> 🐍</th>
<th><strong>Elixir</strong> 💧</th>
</tr>
</thead>
<tbody>
<tr>
<td>Python est réputé pour sa facilité d’apprentissage. Sa syntaxe claire et concise le rend accessible aux débutants, et il est souvent recommandé comme premier langage de programmation. De plus, il existe de nombreuses ressources d’apprentissage disponibles pour Python, ce qui facilite encore plus l’apprentissage.</td>
<td>Elixir a une courbe d’apprentissage plus prononcée, en particulier pour ceux qui ne sont pas familiers avec la programmation fonctionnelle. Cependant, une fois que vous maîtrisez la syntaxe et le paradigme d’Elixir, vous pouvez trouver qu’il offre une approche puissante et flexible pour la programmation.</td>
</tr>
</tbody>
</table>
<h2 id="-bibliothèques-et-frameworks">📚 Bibliothèques et frameworks</h2>
<p>Les bibliothèques et frameworks disponibles pour un langage peuvent grandement affecter sa facilité d’utilisation et sa polyvalence.</p>
<table>
<thead>
<tr>
<th><strong>Python</strong> 🐍</th>
<th><strong>Elixir</strong> 💧</th>
</tr>
</thead>
<tbody>
<tr>
<td>Python a une énorme sélection de bibliothèques pour presque tout, y compris des bibliothèques populaires et puissantes comme <a href="https://pandas.pydata.org/">Pandas</a> pour l’analyse de données, <a href="https://numpy.org/">NumPy</a> pour la programmation numérique, <a href="https://scikit-learn.org/stable/">Scikit-learn</a> pour le machine learning, <a href="https://matplotlib.org/">Matplotlib</a> pour la visualisation de données, et <a href="https://www.tensorflow.org/">TensorFlow</a> et <a href="https://pytorch.org/">PyTorch</a> pour l’IA et le deep learning. En plus de ces bibliothèques, Python propose des frameworks comme <a href="https://www.djangoproject.com/">Django</a> et <a href="https://flask.palletsprojects.com/">Flask</a> pour le développement web.</td>
<td>Elixir, bien qu’il n’ait pas autant de bibliothèques que Python, a des outils puissants comme le framework <a href="https://www.phoenixframework.org/">Phoenix</a> pour le développement web et la bibliothèque <a href="https://hexdocs.pm/ecto/Ecto.html">Ecto</a> pour l’interaction avec les bases de données. De plus, étant construit sur Erlang, Elixir bénéficie également des bibliothèques Erlang, comme <a href="https://hex.pm/packages/cowboy">Cowboy</a> (un serveur HTTP petit mais robuste). En outre, l’écosystème d’Elixir continue de se développer et d’innover. Par exemple, Elixir a récemment introduit <a href="https://github.com/elixir-nx/">Nx</a>, une bibliothèque pour la programmation numérique et le machine learning, qui pourrait potentiellement rivaliser avec Python sur ces sujets à l’avenir. Nx montre comment Elixir évolue pour répondre aux défis modernes de la programmation et pourrait devenir un avantage significatif pour le langage.</td>
</tr>
</tbody>
</table>
<h2 id="-cas-dutilisation-de-chaque-langage">🎯 Cas d’utilisation de chaque langage</h2>
<p>Python est un langage polyvalent qui peut être utilisé dans une variété de domaines, tels que le développement web, le calcul scientifique, l’analyse de données, l’IA, etc. Son caractère généraliste le rend utile pour une grande variété de projets.</p>
<p>Elixir, en revanche, excelle dans la construction d’applications distribuées, tolérantes aux pannes et en temps réel. Il est particulièrement bien adapté pour les applications web et les systèmes nécessitant une haute performance et une concurrence élevée.</p>
<h2 id="-pourquoi-choisir-lun-ou-lautre">🤔 Pourquoi choisir l’un ou l’autre</h2>
<p>Le choix entre Python et Elixir dépend de votre projet et de vos besoins spécifiques. Python pourrait être le choix optimal si vous recherchez un langage facile à apprendre, avec une grande communauté et des bibliothèques pour presque tout. Si vous travaillez sur un projet qui nécessite de la performance, de la concurrence et de la distribution, Elixir pourrait être une meilleure option.</p>
<h2 id="-mon-expérience-avec-les-deux-langages">🔍 Mon expérience avec les deux langages</h2>
<p>J’ai travaillé avec Python et Elixir pendant plusieurs années et j’ai trouvé que chacun a ses forces. Python est mon choix pour l’exploration rapide de données et la création rapide de prototypes, tandis qu’Elixir est mon préféré pour la construction d’applications web performantes.</p>
<h2 id="-mon-avis-sur-les-deux-langages">📝 Mon avis sur les deux langages</h2>
<p>Personnellement, je pense que Python et Elixir sont d’excellents outils avec leurs propres forces. Le choix entre les deux doit être fondé sur les exigences spécifiques de votre projet. Apprendre à travailler avec les deux peut être un grand atout pour tout développeur.</p>
<h2 id="-question-ouverte-">💬 Question ouverte 📣</h2>
<p>À la lumière de cette comparaison, avez-vous une préférence entre Python et Elixir ? Quels facteurs ont le plus d’impact sur votre choix de langage de programmation ? Partagez vos expériences et vos réflexions dans les commentaires ci-dessous.</p>
<hr />
<blockquote>
<p>Cet article s’appuie sur mon expérience et mes opinions. Les besoins spécifiques de votre projet peuvent différer. Il est toujours recommandé de faire une recherche approfondie avant de choisir un langage de programmation.</p>
</blockquote>Yahya LazaarLa charte du développement backend chez Deepki - Partie 2, la lisibilité2023-06-05T00:00:00+00:002023-06-05T00:00:00+00:00https://techblog.deepki.com//backend-charter-part-2<p>Dans <a href="/backend-charter-part-1/">un précédent article</a>, nous avons exposé les principes fondamentaux que nous suivons et que nous avons formalisés dans une charte pour le développement backend.</p>
<p>Dans cet article, nous allons examiner en détail le sujet de la lisibilité du code et la collaboration avec les autres développeurs.</p>
<h1 id="lisibilité">Lisibilité</h1>
<p>Comme présenté lors de la première partie, notre priorité absolue est de produire du code fonctionnel et maintenable par d’autres développeurs.</p>
<p>Pour y parvenir, nous privilégions toujours la lisibilité du code plutôt que la performance, la brièveté ou d’autres aspects du code.</p>
<p>Toutefois, nous sommes conscients que certaines situations exigent des performances supérieures et dans ces cas, nous accordons une attention particulière à la documentation de nos choix.</p>
<h1 id="commentaires">Commentaires</h1>
<p>En général, notre objectif est de créer du code autodocumenté, c’est-à-dire du code qui “parle de lui-même” et ne nécessite pas de commentaires. Nous cherchons donc à remplacer les commentaires par des noms de variables plus explicites ou en extrayant du code dans des fonctions plus parlantes.</p>
<p>On peut dire que nous considérons généralement qu’un commentaire est un signe d’échec : cela implique que le code ne parle pas suffisamment de lui-même.</p>
<p>Toutefois, il y a des situations où les commentaires sont nécessaires et appropriés. Par exemple, pour expliquer des règles métiers ou dans la <a href="https://en.wikipedia.org/wiki/Docstring">docstring</a> des fonctions faisant partie d’une API.</p>
<p>Il est important de noter que les commentaires de type TODO ou FIXME doivent obligatoirement être liés à un ticket pour éviter qu’ils ne soient oubliés et laissés dans le code pendant des années.</p>
<h1 id="uniformité-dans-le-style-utilisé">Uniformité dans le style utilisé</h1>
<p>Nous pensons que la familiarité est une caractéristique clé de la lisibilité du code. Nous croyons que chaque développeur devrait se sentir à l’aise lorsqu’il lit le code écrit par ses collègues. Pour atteindre cet objectif, nous sommes conscients que nous devons produire un code uniforme, qui respecte les conventions adoptées par l’équipe, même si cela signifie que l’individualité de chacun est moins mise en avant.</p>
<p>Nous nous engageons donc à respecter les conventions (parfois implicites) établies par l’équipe. Si nous souhaitons modifier ces conventions, nous devons le faire d’un commun accord et établir un plan de migration réaliste. Nous faisons tout notre possible pour éviter que du code écrit selon d’anciennes conventions ne soit en circulation plus de quelques mois.</p>
<p>Cette approche s’applique aussi bien aux aspects purement stylistiques, tels que le respect de <a href="https://peps.python.org/pep-0008/">PEP8</a>, qu’aux aspects plus techniques ou “philosophiques”. Par exemple, nous veillons à éviter que de nouvelles fonctionnalités utilisent leur propre version d’un algorithme métier simplement parce que l’auteur n’était pas satisfait de l’existant. Nous encourageons la migration de l’existant ou l’acceptation des conventions existantes.</p>
<h1 id="utilisation-de-git">Utilisation de Git</h1>
<p>L’importance d’un bon message de commit dans le développement backend ne doit pas être sous-estimée. Chez Deepki, nous utilisons Git pour retracer l’historique des modifications apportées au code et collaborer lors du développement d’une fonctionnalité. Les messages et le contenu des commits sont essentiels dans deux contextes différents : les revues de code et le développement lui-même.</p>
<p>Pour les revues de code, les commits permettent d’isoler certaines modifications du code pour se concentrer dessus. Ainsi, la multiplication des commits et les messages techniques ne sont pas un problème dans ce contexte.</p>
<p>Cependant, lors du développement, un bon message de commit peut clarifier l’intention derrière une partie du code. Lorsqu’on fusionne une branche, on préférera donc fusionner (squash) tous les commits pour ne garder qu’un seul message décrivant le besoin auquel on a répondu.</p>
<p>Pour rédiger un message de commit pertinent, nous avons une astuce : imaginer une phrase qui commence par “when merged, this commit will…” et la suite de cette phrase est le message de commit. En suivant cette méthode, nous pouvons écrire des messages clairs qui décrivent l’intention derrière chaque modification.</p>
<p>Par exemple :</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">fix a display bug where the id was used instead of the name</code></li>
<li><code class="language-plaintext highlighter-rouge">let the user enter multiple references for their asset</code></li>
<li><code class="language-plaintext highlighter-rouge">speed up the export of invoices when a filter is applied</code></li>
</ul>
<h1 id="collaboration-avec-les-autres">Collaboration avec les autres</h1>
<p>Nous privilégions l’utilisation d’Open API pour une bonne coordination avec les développeurs front-end. Un contrat d’interface est convenu afin que chaque partie puisse avancer avant que l’implémentation ne soit terminée. Il est cependant accepté que ce contrat puisse être amené à changer en cours de développement.</p>
<p>Les responsables produit (PM, PO, chef de projet) sont informés de l’avancement le plus souvent possible, surtout en cas de changements.</p>
<p>Le pair programming est fortement valorisé et pratiqué autant que possible par les développeurs backend, mais il ne remplace pas le processus de relecture du code (code review) qui apporte un regard neuf et du recul à ce qui a été produit, en plus de permettre au reste de l’équipe de simplement se tenir informé.</p>
<p>En cas de désaccord lors de la relecture, un compromis est recherché auprès du reste de l’équipe. D’une manière générale, il est attendu de l’auteur qu’il soit réceptif aux commentaires et sache amender son code dans le sens suggéré par ses pairs.</p>
<h1 id="conclusion">Conclusion</h1>
<p>Dans cet article, nous avons examiné l’importance de la lisibilité du code pour produire du code maintenable. Chez Deepki, nous sommes convaincus que la lisibilité est essentielle pour permettre une collaboration efficace et maintenir un code de qualité à long terme.</p>Olivier RouxDans un précédent article, nous avons exposé les principes fondamentaux que nous suivons et que nous avons formalisés dans une charte pour le développement backend.