Blogue
Savoir-faire et technologie
Histoires, idées et perspectives sur la stratégie, la technologie et les solutions d’affaires.
Articles à la une
<h2>Qu'est-ce que la certification SOC 2 ?</h2><p>La certification SOC 2 (Service Organization Control 2) est une norme élaborée par l'American Institute of Certified Public Accountants (AICPA) qui évalue la capacité d'une organisation à gérer les risques liés à la sécurité, à la disponibilité, à l'intégrité du traitement, à la confidentialité et à la protection de la vie privée des données qu'elle traite pour le compte de ses clients.</p><p>La certification SOC 2 repose sur cinq principes, appelés critères de confiance, qui définissent les exigences minimales que doit respecter une organisation pour assurer la sécurité et la qualité de ses services. Ces critères sont les suivants :</p><ul> <li><strong>Sécurité</strong> : l'organisation protège les données contre les accès non autorisés, les modifications, les divulgations, les dommages ou la perte.</li> <li><strong>Disponibilité</strong> : l'organisation assure la disponibilité et le fonctionnement continu de ses services conformément aux accords conclus avec ses clients.</li> <li><strong>Intégrité du traitement</strong> : l'organisation traite les données de manière complète, valide, exacte, opportune et autorisée.</li> <li><strong>Confidentialité</strong> : l'organisation respecte les engagements et les obligations de confidentialité envers ses clients et les tiers concernant les données qu'elle traite.</li> <li><strong>Protection de la vie privée</strong> : l'organisation respecte les principes de protection de la vie privée définis par l'AICPA et les lois applicables en matière de collecte, d'utilisation, de conservation, de divulgation et d'élimination des données personnelles.</li></ul><p>« Obtenir et maintenir la certification SOC 2, je le vois comme un ultramarathon et non un sprint sur 100 mètres. C'est une première étape, dans un long processus en constante évolution. La cybersécurité, dans son ensemble, nécessite une rigueur et une attention aux détails constante auquel notre équipe est prête à s’attarder. »</p><p>– Vincent Huard, Vice-Président, gestion et analyse des données</p><p>Pour obtenir la certification SOC 2, une organisation doit faire l'objet d'un audit indépendant réalisé par un cabinet comptable qualifié qui vérifie qu’elle respecte les critères de confiance applicables à ses services. L'audit porte sur la conception et l'efficacité des contrôles mis en place par l'organisation pour assurer la conformité aux critères de confiance.</p><h2>Quelle est la différence entre la certification SOC 2 Type 1 et Type 2 ?</h2><p>Il existe deux types de certification SOC 2. C’est entre autres la durée de l’audit qui les distingue. SOC 2 Type 2 est couvert par l’audit le plus long et rigoureux.</p><ul> <li>La certification SOC 2 Type 1 atteste que l'organisation respecte les critères de confiance à une date donnée à une date précise. Elle évalue la conception des contrôles, mais pas leur efficacité dans le temps.</li> <li>La certification SOC 2 Type 2 atteste que l'organisation respecte les critères de confiance sur une période de temps définie, généralement de trois à douze mois. Elle évalue la conception, mais également l'efficacité des contrôles, en tenant compte de leur fonctionnement réel et de leur évolution.</li></ul><p>En d’autres mots, la certification SOC 2 Type 2 répond à des critères plus exigeants et rigoureux, car elle implique un suivi continu et une vérification régulière des contrôles. Elle offre une assurance plus élevée sur la qualité et la sécurité des services fournis par l'organisation.</p><h2>Quels sont les bénéfices pour nos clients ?</h2><p>En obtenant la certification SOC 2 Type 2, Spiria réaffirme sa posture de partenaire de confiance dans la réalisation de projets de développement de solutions numériques pour ses clients. Voici quelques bénéfices principaux qui permettent à nos clients de se lancer la tête tranquille dans des projets d’envergure avec Spiria :</p><ul> <li>La garantie que nous respectons les normes les plus élevées en matière de sécurité de l'information</li> <li>La garantie que nous protégeons les données de nos clients contre les menaces internes et externes.</li> <li>La confiance que nous assurons la disponibilité et la performance de nos services</li> <li>La confiance que nous sommes capables de réagir rapidement et efficacement en cas d'incident.</li> <li>La certitude que nous traitons vos données avec intégrité, en respectant les règles de validation, d'exactitude, de traçabilité et d'autorisation.</li> <li>La tranquillité d'esprit que nous respectons vos obligations de confidentialité et que nous ne divulguons pas vos données à des tiers non autorisés.</li> <li>La sécurité que nous respectons les principes de protection de la vie privée et que nous nous conformons aux lois applicables en matière de données personnelles.</li></ul><p>La certification SOC 2 Type 2 est un gage de confiance et de sécurité pour nos clients qui témoigne de notre engagement à fournir des services de qualité et à respecter les meilleures pratiques du secteur. Elle représente l’excellence en matière de sécurité des données dans le marché tout en étant de plus en plus prisée pour les projets de développement logiciels. Il était donc tout naturel pour Spiria d’être parmi les quelques firmes d’experts à s’y conformer en Amérique du Nord. Nous sommes fiers d’arborer cette certification et d'assurer à la fois l'excellence, la fiabilité et la rigueur de nos pratiques d’affaires.</p><p>Démarrez un projet en toute confiance : <a href="mailto:nouveauprojet@spiria.com">nouveauprojet@spiria.com</a>.</p>
<p>Les équipes de Spiria ont une longue et riche expérience avec les deux types de contrats, et nous vous dévoilons ici ce que nous avons appris au fil du temps sur le sujet et quels sont les critères de succès pour chaque option.</p><p>Clarifions tout d’abord ce que sont ces deux types de projets :</p><h3>Projets temps & matériel</h3><p>Projets dont la portée (activités, livrables, inclusions comme exclusions, etc.) peut être plus ou moins clairement définie. L’évaluation initiale des coûts présente une fourchette de prix probable pour la réalisation du dit projet. Les coûts sont facturés selon les heures réelles exécutées et le matériel/ressources (autres coûts, par exemple des licences logicielles ou des services infonuagiques) nécessaire. Cette approche est plus flexible, car elle permet des changements de spécifications tout au long du processus de développement. L’agilité est encouragée et les contrôles de gestion de projets sont mis de l’avant.</p><h3>Projets forfaitaires ou fixes</h3><p>Projets dont la portée est plus souvent bien ou très bien définie. Le niveau de confiance de l’évaluation initiale des coûts repose sur des informations plus claires que le précédent type de projet. Comme son nom l’indique, les coûts sont fixés au départ, peu importe les heures réellement exécutées et le coût en matériel et ressources. Par conséquent, les notions de risques et de profitabilité sont des considérations plus critiques à évaluer dans ce type de projet. Toute modification des spécifications est encadrée par un processus de demande de changement et est facturée en tant que travail supplémentaire.</p><p>Dans un premier scénario, pour un projet préalablement qualifié, le type de projet (temps/matériel vs fixe) peut être imposé par le client, les exigences internes des organisations ou encore des réglementations, par exemple dans le cas des appels d’offres (majoritairement fixes). Lorsque possible, Spiria peut proposer une approche pour mitiger les risques et mieux saisir la portée du projet, comme proposer au client un investissement initial dans une phase découverte, en mode temps/matériel ou forfaitaire, dans l’intention de pouvoir proposer par la suite les phases de développement et de déploiement en mode forfaitaire. Ceci n’empêche bien sûr pas le client de changer de priorité ou de modifier la portée à la suite de la phase de découverte. Notre flexibilité doit nous permettre de négocier avec le client la portée définie en variant les inclusions/exclusions, dans l’objectif de rester dans l’enveloppe budgétaire forfaitaire contractuelle entendue.</p><p style="text-align: center;"><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/68470cad54d506e6dad2ea43_process-fr.webp" style="width: 60%; border: none;" alt="Un cycle projet type." title="Un cycle projet type."></p><p style="text-align: center; font-style: italic;">Figure 1. Un cycle projet type.</p><p>Dans un deuxième scénario, si le type de projet n’est pas imposé, ceci nous donne la latitude du choix de la stratégie. Habituellement, les clients prévoient des sessions de rencontres avec les différents fournisseurs pour répondre à leurs questions. Une réflexion interne s’impose ensuite pour bien évaluer les facteurs décisionnels menant à la meilleure stratégie. À cet effet, le tableau ci-dessous présente une liste non exhaustive de points qui éclairent les équipes dans cette réflexion. Ces points sont pondérables (facilement identifiables, quantifiables ou mesurables) ou impondérables, en fonction des informations fournies lors des rencontres initiales, dans les cahiers de charge, ou pouvant être obtenues par des demandes au client. Les annotations des deux colonnes de droite sont simplement des suggestions de poids relatifs aux deux types de projets.</p><table cellpadding="0" cellspacing="0" style="width:100%"> <tbody> <tr> <td style="width:76%"><strong>Points</strong></td> <td style="width:12%"><strong>Fixe</strong></td> <td style="width:12%"><strong>T&M</strong></td> </tr> <tr> <td>Le plan d’affaires, les requis, les besoins et les attentes sont claires.</td> <td>➕➕</td> <td>➕</td> </tr> <tr> <td>Les processus et règles d’affaires sont nombreux et complexes.</td> <td>➕</td> <td>➕➕</td> </tr> <tr> <td>Le budget client est identifié et la planification budgétaire est cadrée.</td> <td>➕</td> <td>➖</td> </tr> <tr> <td>L’échéancier est strict ou critique en raison du contexte client ou d’affaires.</td> <td>➕</td> <td>➖</td> </tr> <tr> <td>Les expertises nécessaires sont identifiables.</td> <td>➕</td> <td>➕</td> </tr> <tr> <td>La structure organisationnelle et décisionnelle est grande et complexe.</td> <td>➖</td> <td>➕</td> </tr> <tr> <td>Les aspects légaux sont complexes.</td> <td>➖</td> <td>➕</td> </tr> <tr> <td>Les relations sont déjà établies (historique) ou des contacts sont nos promoteurs.</td> <td>➕</td> <td>➕</td> </tr> <tr> <td>Le calcul de risques, les incertitudes et la contingence sont élevés.</td> <td>➖</td> <td>➕</td> </tr> <tr> <td>Les risques de dérives sont probables.</td> <td>➖</td> <td>➕</td> </tr> <tr> <td>Le client détient une capacité en effectifs ou en connaissances internes<br> (designer, équipe de développement, AQ, etc.).</td> <td>➕</td> <td>➕</td> </tr> <tr> <td>L’environnement technologique est connu.</td> <td>➕</td> <td>➕</td> </tr> <tr> <td>Les contraintes technologiques sont importantes (ex. : système hérité).</td> <td>➖</td> <td>➕</td> </tr> <tr> <td>Les défis d’intégration sont nombreux et complexes.</td> <td>➖</td> <td>➕</td> </tr> <tr> <td>Les choix technologiques sont imposés.</td> <td>➕</td> <td>➕</td> </tr> <tr> <td>Les données sont disponibles pour faire l’assurance qualité fidèlement.</td> <td>➕</td> <td>➕</td> </tr> <tr> <td>La solution est assujettie à des certifications spéciales.</td> <td>➖</td> <td>➕</td> </tr> </tbody></table><p><br>Le résultat de cette réflexion peut amener vers différentes approches représentées dans le diagramme suivant :</p><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/68470cb0f4619dcfb509565b_strategies-fr.png" style="width: 100%; border-style:solid; border-width:1px;" alt="Les différentes stratégies (approches)." title="Les différentes stratégies (approches)."></p><p style="text-align: center; font-style: italic;">Figure 2. Les différentes stratégies. (Cliquer pour agrandir.)</p><p>La stratégie sélectionnée dicte la façon donc les ententes contractuelles sont conclues. Ce choix d’approche a des incidences sur tout le déroulement du projet et son succès final. La transparence du processus de choix et la justification des motifs auprès du client permettent de démarrer la relation sur des bases saines. Les objectifs ultimes sont de livrer un projet qui respecte nos valeurs spiriennes et qui apporte la valeur attendue au client.</p>
Tous les articles
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
<h2>Le concept Fabrik8</h2><p>Cofondé par Pierre-Antoine Fernet et Vanessa Brochu, Fabrik8 est un espace de travail partagé qui peut accueillir des travailleurs autonomes comme des PME de toutes tailles et de tous horizons, favorisant la création de synergies entre les entrepreneurs qui s’y installent et proposant de nombreux services destinés à faciliter le bien-être des travailleurs.</p><p>Initialement installé sur la rue Saint-Urbain, Fabrik8 a vite rencontré le succès en répondant aux besoins de jeunes entreprises dynamiques en quête d’un environnement de travail stimulant. Afin de répondre à la demande croissante, elle a par la suite déménagé dans un ancien bâtiment de style industriel de la rue Waverly, ayant une superficie de plus de 3 000 m<sup>2</sup> (32 000 pi<sup>2</sup>). L’espace offre plusieurs configurations flexibles de bureaux fermés, pour des équipes de 1 à 35 employés, et de nombreux espaces communs comme des salles de conférence, une cafétéria, une salle de jeux, des salles de détente, etc.</p><p>L’ascension fulgurante de Fabrik8 n’allait pas s’arrêter là : ses confondateurs ont mis sur pied le projet de construire un ambitieux complexe de 18 600 m<sup>2</sup> (200 000 pi<sup>2</sup>) sur le même site, afin d’offrir des prestations hors du commun aux entreprises qu’elle accueille. Les plans des nouveaux bâtiments ont été confiés au cabinet d’architecture de Rocio H. Venegas, à qui l’on doit dans le même quartier la restructuration certifiée LEED du 7250 Marconi qui abrite les bureaux de Gameloft Montréal. La construction s’effectue en deux phases. La première, du côté de la rue Jean-Talon, a été achevée cet hiver et accueillera Spiria dans quelques mois, et la seconde, du côté de la rue de Castelnau, débutera dès ce printemps, une fois que toutes les entreprises logées par Fabrik8 auront pu déménager dans le nouvel immeuble de la première phase.</p><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/68470750aa91262c5d2e06ef_fabrik8_lounge.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="Lounge and café at Fabrik8." title="Lounge and café at Fabrik8."></p><p>L’espace bar/salon. © Fabrik8.</p><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/684707546bc41b5f7007d700_fabrik8_sport.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="The gym at Fabrik8." title="The gym at Fabrik8."></p><p>La salle de sport. © Fabrik8.</p><p>Le nouveau complexe offre trois niveaux de bureaux pour les entreprises de 1 à 50 personnes et trois niveaux pour de plus grandes entreprises qui ont la possibilité de louer un plateau au complet (1 765 m<sup>2</sup>, 19 000 pi<sup>2</sup>) ou seulement une partie qu’elles peuvent aménager selon leurs souhaits. Sa particularité est d’obéir aux conditions de la certification WELL, la première norme de construction qui se concentre sur l’amélioration de la santé et du bien-être des occupants. Pour obtenir ce label d’excellence, Fabrik8 et son architecte ont dû travailler sur de nombreux aspects de l’environnement : le confort thermique, l’acoustique, la qualité des espaces, de l’éclairage, de l’air et de l’eau, etc. Et tout le projet se doit de mettre l’accent sur l’intégration de l’activité physique et de l’alimentation saine au quotidien. C’est ainsi qu’on y trouve un centre sportif tout équipé sur le toit ainsi qu’une spacieuse cafétéria-santé au rez-de-chaussée, mis à la disposition de tous les locataires. Et la mobilité active n’est pas oubliée avec un grand espace dédié au stationnement sécurisé des vélos.</p><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/68470757aa91262c5d2e0c9a_fabrik8_hockey.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="The rooftop ice rink at Fabrik8." title="The rooftop ice rink at Fabrik8."></p><p>La patinoire sur le toit. © Fabrik8.</p><p>Enfin, il y a une cerise sur le sundae… Une particularité tout à fait unique qui distingue Fabrik8 de tous les autres immeubles de bureaux : la présence d’une patinoire sur le toit. Oui, vous avez bien lu, une vraie patinoire, surfaceuse à glace incluse, qui l’été venu, se transforme en plateau multisport pouvant accueillir des parties de basketball, de soccer ou de handball entre collègues.</p><h2>Pourquoi avoir choisi Fabrik8 ?</h2><p>Spiria a toujours été extrêmement attachée au bien-être de ses employés et s’est reconnue dans le concept et les valeurs portées par Fabrik8. En prenant un étage au complet, il s’agit d’offrir un environnement de travail fonctionnel, plaisant et inspirant qui dépasse ce que la plupart des entreprises peuvent offrir. L’aménagement du plateau a été confié à Ædifica, un cabinet de design et d’architecture spécialisé dans les environnements de travail, qui a déjà collaboré avec d’innombrables grandes entreprises comme L’Oréal Canada, IBM, Air Transat, Bell, CN, Sanofi, WB Games, etc. Les choix sur la distribution de l’espace, le mobilier, les matériaux et les couleurs se font en collaboration avec un comité d’employés de Spiria qui veille à ce que nos futurs bureaux soient vraiment à notre image, que chacun s’y sente bien et qu’on puisse y avoir du fun.</p><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/6847075aa0418b84dddbb999_aedifica_1.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="Spiria office at Fabrik8." title="Spiria office at Fabrik8."></p><p>© Ædifica.</p><p>Tous les Spiriens et Spiriennes vont ainsi pouvoir bénéficier de toute une nouvelle gamme de services que notre actuelle implantation, dans les anciens ateliers de confection <i>Kiddies Togs</i>, ne pouvait offrir. Stationnement couvert, salle de sport avec cours, patinoire, cafétéria, terrasses, environnement ultramoderne, autant d’atouts qui contribueront à la qualité de vie de tous, tout en gardant tous les avantages du quartier auxquels nous sommes déjà habitués : la proximité des transports en commun avec les stations Parc et De Castelnau, du parc Jarry pour s’aérer l’esprit et pour les pique-niques entre collègues, du plus beau marché alimentaire de la ville, des microbrasseries dont nous connaissons la carte par cœur, des merveilleuses soupes de Soupson, entre autres choses.</p><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/6847075d3197c61dc92c8833_aedifica_2.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="Spiria office at Fabrik8." title="Spiria office at Fabrik8."></p><p>© Ædifica.</p><p>Tout cela vous fait-il envie ? Souhaiteriez-vous travailler dans un environnement d’exception au sein d’une entreprise pas moins exceptionnelle ? Sachez que nous avons de très nombreux postes actuellement offerts, alors n’hésitez pas à <a href="https://www.spiria.com/fr/carriere/">les consulter</a> et peut-être aurez-vous la chance de profiter vous aussi du style de vie proposé par Spiria et Fabrik8.</p>
<h2>Le recours accru aux illustrations</h2><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/68470690aa91262c5d2dac1f_zahidul.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="Zahidul/Dribbble." title="Zahidul/Dribbble."></p><p>© <a href="https://dribbble.com/zahidvector">Zahidul/Dribbble</a>.</p><p>C’est une tendance apparue il y a plusieurs années déjà, mais qui continue sur sa lancée et prend de l’ampleur. On voit de plus en plus de sites qui se modernisent et se donnent une nouvelle image grâce aux illustrations. Jusque dans les années 60, c’était l’illustration qui régnait en maître dans les publicités et autres communications. Puis la photographie a pris le pouvoir. Mais maintenant, on revient de plus en plus vers l’ancienne tendance, ce qui pour moi est une bonne chose, car elles ont un effet rassembleur lorsque qu’elles offrent un certain degré d’abstraction : en ne ressemblant à personne de manière spécifique, elles ressemblent à tout le monde.</p><p>Quelques bibliothèques d’illustrations disponibles en ligne :</p><ul> <li><a href="https://www.humaaans.com">Humaaans</a></li> <li><a href="https://www.ls.graphics/illustrations">Lstore Graphics: Free and Premium Illustrations</a></li> <li><a href="https://s.muz.li/NzA3YjhkNWEy">Open Peeps</a></li> <li><a href="https://icons8.com/illustrations">Icons8 Illustrations</a></li> <li><a href="https://www.getillustrations.com">Get Illustrations</a></li></ul><h2>La 3D et le design isométrique</h2><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/68470694686b311d13f41118_isometric.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="Peter Tarka/Dribbble." title="Peter Tarka/Dribbble."></p><p>© <a href="https://dribbble.com/tarka">Peter Tarka/Dribbble</a>.</p><p>Comme pour les illustrations, on peut difficilement appeler ça une nouvelle tendance, mais la 3D est ici pour rester un bon bout de temps. D’autant plus qu’il devient de plus en plus facile de réaliser des éléments en 3D, même pour des designers d’interfaces utilisateur pour qui ce n’est pas une spécialité, grâce à <a href="https://spline.design">Spline</a> par exemple, un outil qui simplifie la conception 3D (en version bêta pour le moment).</p><p>La 3D permet de passer du concept à un produit apparemment concret, permettant ainsi à l’utilisateur de vraiment se projeter. Les illustrations 3D et autres visuels vont sans doute devenir de plus en plus fréquents, surtout avec l’arrivée en masse de la réalité virtuelle et augmentée.</p><p>On peut maintenant tout faire en 3D sans jamais recourir à de véritables objets, ce qui dans certains cas permet de faire de belles économies, par exemple quand on veut présenter une voiture de luxe ou un bien immobilier inhabituel. Mais surtout, la 3D attire l’attention et rend les sites et interfaces plus attractifs pour l’utilisateur.</p><h2>Des designs riches en couleurs</h2><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/68470697a0418b84dddb3a10_deut-huit-huit.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="Deux Huit Huit." title="Deux Huit Huit."></p><p>Site de <a href="https://deuxhuithuit.com/fr/">Deux Huit Huit</a>.</p><p>Parlant de couleurs, on est passé par bien des modes, certaines étaient dans la sobriété : on a eu la mode du noir et blanc, celle du monochrome, mais maintenant, c’est bien les couleurs vives et pétantes qui sont à l’honneur. Que ce soit dans les créations de <a href="https://carrenoir.com">Carré noir</a>, le site de l’agence créative <a href="https://deuxhuithuit.com/fr/">Deux Huit Huit</a>, ou encore les milliers de références sur <a href="https://dribbble.com">Dribbble</a>, on peut trouver des designs tous plus colorés les uns que les autres. Ne craignez pas les énormes aplats, les effets néons et les couleurs criardes, 2021 est l’année de la couleur.</p><h2>Le minimalisme</h2><p><img src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/6847069a686b311d13f412fe_revolut.webp" style="width: 100%; border-style:solid; border-width:1px;" alt="Revolut." title="Revolut."></p><p>Site de <a href="https://www.revolut.com/">Revolut</a>.</p><p>Mais 2021, c’est aussi l’année du design minimaliste. Des textes bien hiérarchisés, des blancs tournants (marges) maîtrisés, des interfaces lisibles et claires… c’est le choix qu’on fait certains sites comme <a href="https://www.sketch.com">Sketch</a>, <a href="https://theordinary.deciem.com">The Ordinary</a> ou <a href="https://weaintplastic.com">We Ain’t Plastic</a>. Ces sites sont de parfaits exemples du fait qu’il n’est pas nécessaire d’avoir une UI hyper complexe pour provoquer un effet “Wow”.</p><p>Voici un guide sur le design minimaliste : “<a href="https://uxdesign.cc/a-guide-to-minimalist-design-36da72d52431">A guide to minimalist design - The reign of white space</a>.”</p><h2>Les micro-interactions</h2><img data-gifffer="/site/assets/files/6260/shot.gif" style="width: 100%; border-style:solid; border-width:1px;" data-gifffer-alt="Aaron Iker." title="Aaron Iker."><p>© <a href="https://dribbble.com/ai">Aaron Iker</a>.</p><p>Les micros-interactions sont de petites attentions aux détails destinées à ravir l’utilisateur, à créer de brefs moments engageants et accueillants. Il est de plus en plus difficile de surprendre l’utilisateur avec des animations très complexes et élaborées, alors désormais, il est préférable d’utiliser de petits éléments animés pour encourager l’utilisateur, pour montrer des changements d’états d’un bouton cliqué, une transition entre deux pages ou encore les différentes étapes d’un processus. Préparez-vous à voir beaucoup plus d’animations de ce genre en 2021.</p>
<p>En contraste extrême, Python est souvent décrit comme l’un des langages de programmation les plus simples à apprendre et à lire. Il est également très dynamique, permettant de passer facilement n’importe quelle donnée à n’importe quelle fonction. Mieux, les fonctions peuvent être définies et redéfinies à tout moment. Lorsque vous appelez une fonction, vous ne savez jamais d’avance quel code sera <i>vraiment</i> exécuté.</p><p>Alors, pourquoi ne pas combiner les deux ? La complexité et la syntaxe obscure des templates du C++ avec les surprises dynamiques de Python ? Si ce genre de mélange peu ragoûtant vous intéresse, alors vous êtes à la bonne place !</p><h2>Sérieusement…</h2><p>En fait, mon but est de déplacer la résolution des fonctions surchargées (<i>overloaded functions</i> en anglais) à l’exécution du code plutôt qu’au moment de la compilation.</p><p>Je voulais créer un système dynamique d’appel de fonctions qui pourrait appeler une fonction avec n’importe quelle donnée. Je voulais que ce système soit dynamique et extensible, afin de permettre de nouvelles surcharges de la fonction pour de nouveaux types de données. Je voulais également pouvoir appeler ces fonctions avec des données concrètes ou un tas de <code>std::any</code>. Enfin, je voulais aussi que tout cela soit raisonnablement efficace.</p><p>Pour atteindre tous ces objectifs, je me suis tourné vers les templates. Pas de simples templates, mais la version plus complexe des templates variadiques.</p><h2>Syntaxe des templates variadiques</h2><p>Qu’est-ce qu’un template variadique ? Les templates normaux prennent en argument un nombre de types fixe. C’est bien quand on sait à l’avance combien de types on doit utiliser. En revanche, un template variadique prend en argument un nombre variable de types. Il peut en recevoir zéro, un, deux, ou n’importe combien.</p><p>En plus de recevoir ces types, le template doit être capable de s’en servir. Comme vous le savez peut-être déjà, les templates sont entièrement générés à la compilation. Ils doivent donc fonctionner sans modifier aucune donnée. Ainsi, pour manipuler un nombre variable de types, une nouvelle syntaxe a dû être ajoutée au C++. La nouvelle syntaxe pour la réception des types et leur utilisation a été créée avec l’ellipse : <code>…</code>.</p><p>L’astuce de base est que chaque fois que l’ellipse est utilisée, elle indique au compilateur C++ de générer autant de fois que nécessaire le code qui entoure l’ellipse. Par exemple, les types du template sont reçus avec une ellipse. Dans l’exemple suivant, l’argument <code>VARIA</code> du template représente un nombre quelconque de types.</p><pre><code>template <class... VARIA>struct example{ // le code du template serait ici.};</code></pre><p>Par la suite, dans le code du template, les arguments variadiques peuvent être utilisés avec une ellipse. Par exemple, le template variadique présenté ci-haut pourrait avoir une fonction qui recevrait des arguments pour les transmettre à une autre fonction, comme ceci :</p><pre><code>// Réception d'un nombre variable d'arguments...void foo(VARIA... function_arguments){ // ... passation à une autre fonction. other_bar_function(function_arguments...);}</code></pre><p>Ces exemples ne font qu’effleurer la surface de ce qui est possible avec modèles variés, mais ils seront suffisants pour notre objectif dans cet article.</p><h2>Design d’appels dynamiques</h2><p>Avant de nous lancer dans le design de notre système d’appels dynamique, exposons plus concrètement nos exigences. J’ai dit qu’il devrait imiter la surcharge de fonctions du C++. Qu’est-ce que cela signifie concrètement ? Voici nos exigences :</p><ul> <li>La fonction elle-même est déclarée et référencée par son nom, comme une fonction normale.</li> <li>Le nombre d’arguments de cette fonction peut varier.</li> <li>Chaque surcharge de cette fonction peut avoir un type de valeur de retour différent.</li> <li>Cette fonction peut être surchargée pour n’importe quel type.</li> <li>Une nouvelle surcharge de fonction peut être ajoutée dynamiquement, au moment de l’exécution, pour tout type.</li></ul><p>Bien que ces exigences soient suffisantes pour atteindre notre objectif, je désirais quelques ajouts. Le premier ajout est de supporter des arguments de fonction ayant un type fixe. Par exemple, une fonction pour écrire recevrait toujours en argument un <code>std::ostream</code>. Le deuxième est de permettre sélectionner la surcharge d’une fonction sans avoir à passer d’argument à la fonction. Par exemple, ceci permet de spécifier le type de retour de la fonction ou bien d’écrire une fonction ne prenant aucun argument.</p><p>Pour supporter tout cela, nous ajoutons deux exigences à la liste :</p><ul> <li>Tous les arguments n’ont pas à jouer un rôle dans la sélection de la fonction.</li> <li>Des types supplémentaires peuvent jouer un rôle dans la sélection de la fonction sans en être un argument.</li></ul><p>Le résultat doit ressembler à la surcharge de fonctions normale C++. Par exemple, voici à quoi ressemble un appel à la fonction <code>to_text</code> dans le système d’appels dynamique :</p><pre><code>std::wstring resultat = to_text(7);// resultat == "7"std::any seven(7);std::wstring resultat = to_text(seven);// resultat == "7"</code></pre><p>Pour parvenir à une telle similitude comparée à la surcharge de fonction, un grand nombre d'éléments complexes travaillent en arrière-scène.</p><h2>Smooth Operator</h2><p>Avant de montrer comment les appels dynamique fonctionnent, nous allons voir comment ils se présentent du point de vue du programmeur voulant créer une nouvelle opération.</p><p>Pour créer une nouvelle fonction appelée <code>foo</code>, il faut déclarer une classe pour la représenter. Pour notre exemple, nous l'avons nommée <code>foo_op_t</code> et l'avons dérivée de <code>op_t</code>. Cette classe <code>foo_op_t</code> ne sert qu'à identifier notre fonction. Elle peut être entièrement vide! Ensuite, nous pouvons écrire la fonction <code>foo</code> elle-même, le vrai point d'entrée. Cette fonction est très simple: elle ne fait qu'appeler la fonction <code>call<>::op()</code> (pour des valeurs de type concret) ou bien <code>call_any<>::op()</code> (pour des valeurs de type <code>std::any</code>), toutes deux contenue dans <code>foo_op_t</code>, qui fera tout le travail:</p><pre><code>struct foo_op_t : op_t<foo_op_t> { /* empty! */ };inline std::any foo(const std::any& arg_a, const std::any& arg_b){ return foo_op_t::call_any<>::op(arg_a, arg_b);}template<class A, class B, class RET>inline RET foo(const A& arg_a, const A& arg_b){ std::any result = foo_op_t::call<>::op(arg_a, arg_b); // Notez: nous pourrions aussi vérifier que le std::any // contient bien un RET, plutôt qu'en être sûr. return any_cast<RET>(result);}</code></pre><p>Notez que la classe de base de la nouvelle opération prend l’opération elle-même comme paramètre de template. Il s’agit d’une astuce bien connue dans la programmation de templates. Elle est si connue qu’elle a même un nom : curiously recursive template pattern. Dans notre cas, cette astuce est utilisée pour que le <code>op_t</code> puisse se référer à l’opération spécifique utilisée.</p><p>Maintenant, nous pouvons créer des surcharges de la fonction <code>foo</code>. Ceci est fait en appelant <code>make<>::op</code> avec une fonction qui implémente la surcharge. Pour créer une surcharge prenant les types <code>A</code> et <code>B</code> et retournant le type <code>RET</code>, nous appellerions <code>make<>::op<RET, A, B></code>. Cela enregistre la surcharge dans la classe <code>foo_op_t</code>. Par exemple, implémentons notre <code>foo</code> pour les types <code>int</code> et <code>double</code>, retournant un <code>float</code> :</p><pre><code>// Code de la surcharge.float foo_for_int_and_double(int i, double d){ return float(i + d);}// Enregistrement de la surcharge.foo_op_t::make<>::op<float, int, double>(foo_for_int_and_double);</code></pre><p>Bien sûr, nous pourrions raccourcir et simplifier ceci en rédigeant le code dans l’appel à <code>make<>::op</code> lui-même, avec un lambda. C’est même le style que vous je suggère :</p><pre><code>foo_op_t::make<>::op<float, int, double>( [](int i, double d) -> float { return float(i + d); });</code></pre><p>Si vous vous demandez pourquoi les appels <code>call<></code> et <code>make<></code> ont les sigils de templates, c’est bien sûr parce qu’ils sont des templates variadiques. Les arguments optionnels de template sont les types supplémentaires qui permettent de choisir une surcharge de fonction plus précisément, sans que ces types soient passés en argument à la fonction <code>foo</code>. Nous verrons cela plus en détail plus loin.</p><p>Nous sommes maintenant prêts à entrer dans le vif du sujet : le design des appels dynamiques.</p><h2>C’est le sélecteur</h2><p>Le premier problème à résoudre est la manière dont chaque surcharge est identifiée au sein d’une famille de fonction. La solution évidente est de l’identifier par les types des arguments et les types supplémentaires optionnels de sélection. C++ fournit les classes <code>std::type_info</code> et <code>std::type_index</code> pour identifier un type. Ce dont nous avons besoin, c’est d’un <code>tuple</code> de ces <code>type_index</code>. Pour ce faire, nous utilisons deux templates : le convertisseur et le sélecteur.</p><p>Le convertisseur convertit (eh ben…) n’importe quel type vers le type <code>std::type_index</code>. Écrire ainsi une classe pour implémenter une étape d’un algorithme est une astuce très idiomatique des templates. Ceci permet d’exécuter l’algorithme pendant la compilation. Voici donc le convertisseur, qui convertit tout type <code>A</code> vers les types <code>type_index</code> et <code>std::any</code> :</p><pre><code>template <class A>struct type_converter_t{ using type_index = std::type_index; using any = std::any;};</code></pre><p>Le sélecteur de type peut alors être écrit comme un template variadique en appliquant le convertisseur à tous les types donnés comme argument et en déclarant un type <code>tuple</code> nommé <code>selector_t</code> avec le résultat. Ce tuple contient les types des arguments de la fonction que l’on est en train de créer, <code>N_ARY</code>, et les types supplémentaires de sélection, <code>EXTRA_SELECTORS</code>, afin d’avoir un sélecteur complet.</p><pre><code>template <class... EXTRA_SELECTORS>struct op_selector_t{ template <class... N_ARY> struct n_ary_t { // Le type selector_t est un tuple de type_index. using selector_t = std::tuple< typename type_converter_t<EXTRA_SELECTORS>::type_index..., typename type_converter_t<N_ARY>::type_index...>; };};</code></pre><p>Notez comment l’ellipse est appliquée à la ligne :</p><pre><code>typename type_converter_t<EXTRA_SELECTORS>::type_index...</code></pre><p>La façon dont le langage C++ applique l’ellipse est un peu la magie noire des templates variadiques. Parfois, vous devrez faire plusieurs essais pour trouver ce qui fonctionne et ce qui ne fonctionne pas.</p><p>Nous avons maintenant un sélecteur, mais comment l’utiliser ? Pour cela, nous fournissons quelques fonctions. L’objectif est d’avoir une fonction qui construit un sélecteur rempli avec les types concrets. Naturellement, nous appelons notre fonction <code>make</code> :</p><pre><code>template <class... EXTRA_SELECTORS>struct op_selector_t{ template <class... N_ARY> struct n_ary_t { template <class A, class B> static selector_t make() { return selector_t( std::type_index(typeid(EXTRA_SELECTORS))..., std::type_index(typeid(N_ARY))...); } };};</code></pre><p>Puisque je veux supporter les appels de fonction avec <code>std::any</code>, nous devons fournir une fonction <code>make_any</code> prenant des <code>std::any</code> en arguments. (En tant qu’optimisation, une version avec les sélecteurs supplémentaires déjà convertis en <code>type_index</code> est fournie sous le nom <code>make_extra_any</code>, mais elle n’est pas montrée ici.)</p><pre><code>static selector_t make_any(const typename type_converter_t<N_ARY>::any&... args){ return selector_t( std::type_index(typeid(EXTRA_SELECTORS))..., std::type_index(args.type())...);}</code></pre><h2>Plongée mécanique</h2><p>Enfin, nous pouvons nous plonger dans les détails mécaniques de l’enregistrement et de l’appel des fonctions. La classe de base des opérations est déclarée comme un template prenant l’opération elle-même et la liste des arguments immuables supplémentaires, <code>EXTRA_ARGS</code>, qui auront donc des types fixes. (Rappelez-vous notre précédent exemple d’opération d’écriture, qui reçoit toujours un <code>std::ostream</code> en argument.)</p><pre><code>template <class OP, class... EXTRA_ARGS>struct op_t{ // Détails internes décrit ci-bas...};</code></pre><p>Les premiers détails internes que nous verrons sont quelques types utilisés à plusieurs reprises : la classe sélecteur (<code>op_sel_t</code>), le tuple sélecteur (<code>selector_t</code>) et la représentation interne des fonctions (<code>op_func_t</code>).</p><pre><code>using op_sel_t = typename op_selector_t<EXTRA_SELECTORS...>::template n_ary_t<N_ARY...>;using selector_t = typename op_sel_t::selector_t;using op_func_t = std::function<std::any(EXTRA_ARGS ..., typename type_converter_t<N_ARY>::any...)>;</code></pre><p>Ce code montre une partie de la complexité inhérente à la programmation des templates. Il y a plusieurs éléments du code qui seraient normalement totalement superflus. Mais, dans le contexte particulier des templates, ces éléments sont nécessaires. Par exemple, Le <code>typename</code> est nécessaire pour dire au compilateur que ce qui suit vraiment est un type. Cela arrive quand un template fait référence à des éléments d’un autre template. La syntaxe C++ est trop ambiguë pour que le compilateur puisse déduire que nous utilisons un type. Un autre élément très particulier est le mot-clé <code>template</code> se trouve juste avant l’accès à <code>n_ary_t</code>. Cet ajout est nécessaire pour dire au compilateur qu’il s’agit réellement d’un template.</p><p>Nous sommes donc prêts à décrire l’ensemble du système, construit à partir de quelques fonctions :</p><ul> <li>Appeler l’opération : <code>call<>::op</code></li> <li>Créer une nouvelle surcharge : <code>make<>::op</code></li> <li>Rechercher la surcharge correcte : <code>get_ops</code></li></ul><p>Nous nous attaquerons à chacune d’entre elles dans l’ordre inverse, en partant des tréfonds du design jusqu’à notre but final : appeler une surcharge.</p><h2>Gardien des merveilles</h2><p>Le tréfonds du design est la fonction qui détient les surcharges déjà enregistrées. Il y a une raison très importante pour laquelle <code>get_ops</code> doit exister. En effet, les surcharges doivent bien être conservées dans un conteneur, mais que notre classe d’opération est un template. Nous ne pouvons pas garder toutes les surcharges pour toutes les opérations ensemble. Heureusement, en C++, nous avons la garantie qu’une variable statique contenue dans une fonction d’un template est unique pour chaque instanciation du template. Donc, <code>get_ops</code> peut contenir en toute sécurité notre liste de surcharge :</p><pre><code>template <class SELECTOR, class OP_FUNC>static std::map<SELECTOR, OP_FUNC>& get_ops(){ static std::map<SELECTOR, OP_FUNC> ops; return ops;}</code></pre><p>Le fait que <code>get_ops</code> soit un template pour <code>SELECTOR</code> et pour <code>OP_FUNC</code> permet supporter l’enregistrement de surcharges avec un nombre d’arguments différents.</p><h2>Création d’opérations</h2><p>La fonction <code>make<>::op</code> est un template qui prend une surcharge de fonction que vous avez écrite pour des types concrets. Elle enveloppe la surcharge dans la représentation interne de la fonction et l’enregistre. L’enveloppe se charge de convertir les <code>std::any</code> en des types concrets. C’est sans danger, puisque la surcharge pour ces types concrets n’est appelée que lorsque les types correspondent. C’est ici que les types de sélection supplémentaires facultatifs peuvent être fournis en arguments du template, sous le nom <code>EXTRA_SELECTORS</code>.</p><pre><code>template <class... EXTRA_SELECTORS>struct make{ template <class RET, class... N_ARY> static void op( std::function<RET(EXTRA_ARGS... extra_args, N_ARY... args)> a_func) { // Enveloppe gardée sous la forme d’un lambda qui // convertit la représentation interne de la fonction // vers la signature réelle de la surcharge. op_func_t op( [a_func]( EXTRA_ARGS... extra_args, const typename type_converter_t<N_ARY>::any&... args) -> std::any { // Conversion vers les types concrets. return std::any(a_func(extra_args..., *std::any_cast<N_ARY>(&args)...)); } ); // Enregistrement. auto& ops = get_ops<selector_t, op_func_t>(); ops[op_sel_t::make()] = op; }};</code></pre><h2>Appels à la pelle</h2><p>Nous arrivons enfin à la fonction utilisée pour envoyer un appel. Il y a trois versions de la fonction. Les seules différences entre elles sont si les arguments sont déjà convertis en <code>std::any</code> et si les sélecteurs supplémentaires facultatifs sont déjà convertis en <code>std::type_index</code>. Voici ce que la fonction <code>call<>::op</code> doit faire :</p><ul> <li>Créer un sélecteur à partir des types de ses arguments, plus les sélecteurs supplémentaires optionnels.</li> <li>Aller chercher la liste des surcharges disponibles.</li> <li>Trouver la surcharge de fonction à l’aide du sélecteur.</li> <li>Retournez une valeur vide si aucune surcharge ne correspond aux arguments.</li> <li>Appeler la fonction trouvée si une surcharge correspond aux arguments.</li></ul><pre><code>template <class... EXTRA_SELECTORS>struct call{ template <class... N_ARY> static std::any op(EXTRA_ARGS... extra_args, N_ARY... args) { // Les surcharges disponibles. const auto& ops = get_ops<selector_t, op_func_t>(); // Trouver une surcharge correspondante. const auto pos = ops.find(op_sel_t::make()); // Résultat vide si auncune surcharge n'est trouvée. if (pos == ops.end()) return std::any(); // Appel à la bonne surcharge trouvée. return pos->second(extra_args..., args...); }};</code></pre><h2>Point final</h2><p>Cela complète la description du design du système dynamique d’appels surchargés. Le repo du code source contient de multiples exemples d’opérations avec une suite complète de tests.</p><p>Les opérations données en exemple sont :</p><ul> <li><code>compare</code>, une opération binaire pour comparer deux valeurs.</li> <li><code>convert</code>, une opération unaire pour convertir une valeur vers autre type. Cet exemple montre une opération avec un argument de sélection supplémentaire, le type final de la conversion.</li> <li><code>is_compatible</code>, une opération nullaire prenant deux types supplémentaires pour sélectionner la surcharge, et qui vérifie si l’un peut être converti en l’autre.</li> <li><code>size</code>, une opération unitaire retournant le nombre d’éléments dans un conteneur, ou retournant zéro si aucune surcharge n’a été trouvée.</li> <li><code>stream</code>, une opération unaire pour écrire une valeur dans un flux de texte. C’est un exemple d’une opération avec un argument immuable, la destination <code>std::ostream</code>.</li> <li><code>to_text</code>, une opération unitaire de conversion d’une valeur en texte.</li></ul><p>Tout le code se trouve dans la bibliothèque <code>any_op</code> qui fait partie de <a href="https://github.com/pierrebai/dak_utility">mon repo dak_utility</a>.</p>
<h2>Les machines apprennent toutes seules</h2><p>C’est l’impression qu’on peut en avoir. Mais, d’une part, les machines n’en sont pas encore au stade où elles décident d’elles-mêmes de leur champ d’application. Et d’autre part, il y a toujours un considérable travail humain en amont. Des spécialistes aguerris formulent le problème, préparent les modèles, déterminent les ensembles de données d’entraînement appropriés, essayent d’éliminer les biais potentiels induits par ces données, etc. Aussi, ils font évoluer le logiciel en fonction de ses performances. Il y a beaucoup de temps de cerveau humain derrière les modèles d’IA.</p><h2>Les machines font preuve d’objectivité</h2><p>Rien n’est bien sûr plus faux. Le design des machines et l’écriture de leurs logiciels sont des créations humaines. Et dans le cadre de l’apprentissage automatique, l’objectivité repose essentiellement sur la neutralité des données d’entraînement qui sont soumises au modèle en vue de son apprentissage. Le biais cognitif est pratiquement inévitable et toute la difficulté de la préparation des données est de réussir à limiter ce biais au maximum. Il arrive souvent qu’un modèle reproduise un biais de confirmation qu’il a hérité de ses créateurs humains. Si nous introduisons des données biaisées, même involontairement, dans un système, nous obtenons en sortie des résultats biaisés.</p><h2>L’IA est synonyme d’apprentissage automatique</h2><p>Il est vrai que presque toutes les applications actuelles de l’intelligence artificielle relèvent de l’apprentissage automatique. Mais l’apprentissage automatique, basé sur l’idée que les machines peuvent apprendre et s’adapter par l’expérience, n’est qu’un outil de l’intelligence artificielle. Peut-être découvrirons-nous un jour de nouvelles méthodes pour résoudre des problèmes où l’apprentissage automatique échoue, par exemple avec des questions pour lesquelles on n’a pas de grandes quantités de données qualifiées. L’intelligence artificielle quant à elle fait référence à une idée plus générale où les machines peuvent exécuter des tâches de façon “intelligente”, c’est-à-dire d’avoir des fonctionnements se rapprochant de l’intelligence humaine. Cela dit, le concept d’intelligence artificielle n’a pas de définition communément admise et ses limites sont floues. Peut-être devrions-nous parfois parler de traitement complexe de l’information ou d’automatisation cognitive, mais cela serait certainement moins sexy.</p><h2>L’IA va supprimer des emplois</h2><p>Comme pour l’automatisation et la robotisation de ces dernières décennies, il serait plus exact de dire que les technologies de l’intelligence artificielle vont remplacer certains postes et en faire évoluer d’autres. C’est-à-dire qu’elles vont modifier profondément le paysage du travail, comme ce fut le cas des révolutions industrielles précédentes, mais probablement pas réduire le nombre d’emplois. Autant la robotisation a permis d’éliminer des tâches manuelles répétitives, autant l’intelligence artificielle permet d’éliminer des tâches intellectuelles répétitives, de travailler d’une façon nouvelle et plus intelligente. Et comme avec la robotisation, l’intelligence artificielle peut être plus efficace dans certaines tâches que n’importe quel humain. Prenons comme exemple une application, basée sur l’IA, d’examen de radiographies pulmonaires qui peut détecter des maladies bien plus rapidement, avec un taux d’erreurs moindre que les radiologues.</p><h2>L’IA, pas utile dans mon entreprise</h2><p>Et pourquoi cela ? L’intelligence artificielle peut d’ores et déjà améliorer les interactions avec les clients, analyser les données plus rapidement, aider à la prise de décision, générer des alertes précoces sur des perturbations à venir, etc. Pourquoi s’en priver ? Elle a aussi quantité d’applications utiles en milieu industriel, notamment avec la vision/reconnaissance par ordinateur qui permet par exemple de détecter une pièce défectueuse avec bien plus d’efficacité et de rapidité qu’un opérateur humain. Refuser l’IA est du même ordre que renoncer aux bénéfices de l’automatisation, au risque de mettre l’entreprise dans une position concurrentielle défavorable. Il faut bien comprendre que l’IA est le prolongement logique de la révolution industrielle de l’automatisation/robotisation.</p><h2>Des machines super-intelligentes vont dépasser les humains</h2><p>Les applications actuelles de l’IA sont toujours très spécifiques, c’est-à-dire qu’elles répondent à un problème bien défini. En revanche, les intelligences généralisées, qui sont capables de s’atteler à un certain nombre de tâches différentes, tout comme le font les intelligences humaines ou naturelles, ne sont pas encore à l’ordre du jour et appartiennent au registre de la science-fiction. Mais en 1865, le voyage de la Terre à la Lune appartenait au registre de la science-fiction et l’on sait ce qu’il en est aujourd’hui. Alors, on ne saurait faire une prédiction définitive sur ce point et affrimer que cette idée est tout à fait fausse. Il paraît cependant sage de penser que nous ne connaîtrons pas de notre vivant les super-robots qui seront capables de dépasser les humains en tout.</p>
<p>J’ai eu l’occasion de travailler ces derniers mois sur deux applications souffrant de lourdes dettes techniques, et pour l’une d’elles, il a fallu mettre en place une stratégie de paiement progressif, plus aisée à appliquer pour la plupart des <a href="https://www.spiria.com/fr/services/developpement-axe-performance/developpement-logiciel-sur-mesure/">projets de développement</a>.</p><p>À première vue, évaluer le coût d’une dette technique est très complexe. Il faut également se rappeler que si on n’y consacre qu’un minimum d’efforts, on finira toujours par ne payer que les intérêts. Chaque nouvelle fonctionnalité ne sera alors qu’un nouveau prêt, s’ajoutant dans le meilleur des cas au principal. Afin de rattraper le retard et de poursuivre en payant ses mensualités à temps, il faut évaluer la dette actuelle et revoir certaines définitions et pratiques au sein de l’équipe.</p><p>Est-ce que des librairies pas à jour ou l’utilisation d’une API obsolète constituent une dette technique ? Qu’en est-il des bogues connus ? Et des problèmes de performance qui découlent de ces précédents points ? Et de chaque raccourci pris dans le passé simplement pour pouvoir livrer à temps sans réellement régler le problème ? Oui, ils sont tous constitutifs de la dette globale. Est-ce qu’une architecture non optimale fait partie de cette dette technique ? Probablement non, et le changement d’architecture serait un thème à traiter à part. Et le déficit en tests unitaires ? Ce n’est pas non plus une dette en soi, mais l’implémentation de ceux-ci fera sans doute partie des bonnes habitudes qui permettront de ne pas faire croître la dette à nouveau.</p><p>La première étape inévitable est de disséquer l’application et de regrouper par thème chaque élément de la dette. Le temps nécessaire à cette analyse dépendra directement de l’implication antérieure de l’équipe dans le projet. Une fois une vue d’ensemble acquise, les différentes parties de la dette peuvent être regroupées en trois catégories principales :</p><ul> <li>celles qui sont d’ordre général, qui peuvent être payées sans conséquences directes pour les fonctionnalités, et qui sont donc chacune candidates à leur propre récit (par exemple : “mettre à jour React à la dernière version”) ;</li> <li>celles qui sont d’ordre général, mais qui devront être répétées pour de multiples fonctionnalités, et qui sont donc candidates à devenir des sous-tâches (par exemple, effectuer la transition de classes à des composantes fonctionnelles) ;</li> <li>celles qui sont spécifiques à un récit et qui ne seront pas dupliquées (typiquement, un bogue concret).</li></ul><p>Avant de commencer l’évaluation des efforts à attribuer à chaque élément de la dette, il est important de redéfinir la notion d’achèvement. À quel moment considère-t-on un récit comme “fini”, que la “story” peut être fermée ? Avant que les tests d’assurance qualité ne débutent ? Après les tests d’AQ et le “merge” dans la branche de développement principale, mais avant les tests de contrôle qualité (CQ) ? Ou encore, seulement au moment où la fonctionnalité est livrée aux utilisateurs ?</p><p>Dans le cas de mon équipe, l’approche qui s’est avérée la plus bénéfique fut de considérer un récit comme “fini” une fois que les tests d’AQ étaient réussis et que notre code se trouvait dans notre branche de développement. Toutefois, notre entente stipulait que cette branche devrait être livrable à tout moment. En conséquence, aucun “merge” incomplet ou problématique n’était en aucun cas permis.</p><p>Lors du premier “grooming”, nous avons dû mettre en place une méthodologie permettant une réduction systématique de la dette. Cette méthodologie se révéla en fin de compte assez simple, tout en étant méticuleuse et très efficace.</p><p>Il a été convenu qu’un maximum de 20 % du sprint serait consacré au paiement de la partie de la dette d’ordre général (la première catégorie de notre liste plus haut), celle qui ne nous créerait pas de bogues affectant les fonctionnalités individuelles.</p><p>Pour la deuxième catégorie de dette, c’est-à-dire les parties qui sont globales, mais qui doivent être répétées, il a été décidé d’y aller progressivement. Chaque fois que l’on toucherait à une fonctionnalité existante, deux sous-tâches seraient créées automatiquement pour le récit, l’une pour la refactorisation du code et l’autre pour les tests unitaires. Ces deux sous-tâches représentaient dans la majorité des cas de 15 à 30 % des points du récit (“Story Points”).</p><p>La troisième catégorie de la dette, qui porte sur des bogues spécifiques à une fonctionnalité, serait payée selon les priorités établies dans le carnet de produit (“Backlog”).</p><p>Au bout de trois sprints avec cette approche, le résultat fut sans appel. Chaque refactorisation du code nous a permis d’éliminer beaucoup de raccourcis qui avaient été pris auparavant, d’améliorer la modularité et de tester chaque ligne de code, ce qui a eu un impact direct sur la facilité de maintenance et d’implémentation, la fiabilité et les performances. Les bénéfices étaient palpables des deux côtés de l’application : pour les développeurs et, tout aussi important, pour les utilisateurs. Au final, la taille du paquet de l’application a été réduite de 70 % (de 12 à 3,5 Mo), le temps de chargement de 80 % et la couverture en tests unitaires est passée de 2 à 60 % (ce qui représente près de 380 nouveaux tests).</p><p>Même si au départ tout ce travail semblait représenter un effort relativement flou et un coût supplémentaire inconnu, il s’est avéré finalement que les bénéfices obtenus justifiaient largement l’investissement, et ce sur long terme. Il n’est parfois pas aisé d’être complètement transparent sur la dette existante, et d’exposer celle-ci à toute l’équipe par le biais du carnet de produit, mais en mettant en place des métriques pour chaque objectif, l’avancée est plus que gratifiante pour toutes les personnes impliquées.</p>
<p>Ainsi, qu’est-ce qu’un bon chef de développement logiciel? Avant de répondre à cette question, demandons-nous <i>pourquoi</i> il faut un chef de développement logiciel.</p><h2>Un chef est-il vraiment nécessaire?</h2><p>S’il est admis que les bons logiciels sont développés par les les bonnes équipes, alors, il est logique d’avoir un bon chef d’équipe.</p><p>D’abord, un bon chef d’équipe <b>écarte les problèmes</b> qui empêchent l’équipe de travailler libre de distractions ou d’obstacles. Une équipe qui doit s’écarter du développement pour régler des problèmes accessoires est une équipe dont la performance en développement est diminuée.</p><p>Ensuite, un bon chef d’équipe <b>motive et rallie l’équipe</b> tout au long du cycle de vie du projet, surtout lorsque les choses vont mal. Une équipe de développement logiciel se compose (normalement) d’êtres humains ; or, il faut comprendre ce qui anime chacun de ses membres pour guider l’équipe vers son but commun.</p><p>Enfin, un bon chef d’équipe <b>cerne et gère les lignes de fracture</b>. Katerina Bezrukova, professeure adjointe de dynamique de groupe à l’Université de Santa Clara, a étudié des entreprises de haute technologie de la Silicon Valley pour déterminer si la « chimie d’équipe » peut être prédite, et si celle-ci est essentielle au succès. Les « lignes de fracture » sont les caractéristiques personnelles qui peuvent produire des clivages au sein d’un groupe, par exemple l’âge, le sexe, l’ethnicité, les motivations professionnelles, les passe-temps ou les intérêts personnels. Ces clivages permettent de comprendre les effets de la composition d’un groupe, soit la « chimie d’équipe ». La diversité peut creuser les lignes de fracture, mais si celles-ci peuvent être comblées, par exemple grâce aux intérêts chevauchants des membres d’un groupe, les conflits éventuels peuvent être surmontés.</p><p>Il existe d’autres excellentes raisons pour lesquelles un bon chef d’équipe vaut son pesant d’or, mais nous nous fonderons sur les trois raisons ci-dessus pour examiner les qualités qui vous permettront d’en être un.</p><h2>Empathie</h2><p>Les bonnes équipes ne se créent pas par hasard : il faut comprendre chaque membre de l’équipe et, surtout, comprendre que leur vision du monde et perspective sont différentes des vôtres. Un bon chef d’équipe observe et retient les comportements et réactions des membres de l’équipe. Ceci dit, rien ne remplace <b>l’écoute active</b>. Souvent, les chefs d’équipe frustrés ne comprennent pas pourquoi leur technique de ralliement ne fonctionne pas (par exemple, but commun et valeurs culturelles partagées, ennemi commun, système de récompense). Ces chefs d’équipe ont tendance à se rabattre sur la technique « c’est moi qui commande ».</p><p>Le manque d’empathie pour les membres de l’équipe peut creuser les lignes de fracture et créer des clivages au sein de l’équipe.</p><h2>Anticipation</h2><p>Les clients comme les développeurs de logiciels sont unanimes : tous apprécient les chefs d’équipe qui <b>anticipent les réactions et les résultats</b>. Ces chefs d’équipe ont pris le temps de prévoir la façon par laquelle un résultat pourrait infléchir divers comportements et ont <b>fait un plan</b>. Ce genre de leadership valorise les membres de l’équipe dans toute leur diversité de personnalités et d’expériences, sans essayer de les homogénéiser et d’en faire une armée de robots commandés.</p><p>La capacité d’un chef d’équipe à prévoir et anticiper conforte le sentiment qu’il <b>s’occupe de tout et de tous</b> – équipe et client – pour que les membres de l’équipe puissent travailler l’esprit tranquille et se concentrer sur leur mission.</p><h2>Communication</h2><p>L’on n’insiste jamais assez sur l’importance de <b>l’écoute active</b> comme élément clé de la communication. Un bon chef d’équipe doté d’une bonne écoute active mène par l’exemple, ce qui encourage les membres de l’équipe de faire preuve à leur tour d’écoute active en cas de conflits entre eux. Mais l’inverse est vrai aussi : un chef d’équipe qui n’est pas à l’écoute sème la zizanie entre les membres de l’équipe, qui se concurrencent pour se faire entendre.</p><p>Les équipes fonctionnent le mieux lorsque la confiance règne. Pour faire régner la confiance, un bon chef d’équipe doit savoir quel message transmettre (« le <b>quoi</b> »), <b>quand</b> le transmettre (le choix du moment) et <b>comment</b> (le ton). Le bon choix du « quoi », « quand » et « comment » s’appuie sur l’empathie et l’anticipation qui nous ont aidé à bien comprendre la dynamique du groupe.</p><p>Toujours dans l’optique de « connaître son auditoire », attention aux effets pervers de la sur-communication. Parfois, il vaut mieux éviter la précipitation afin de ne pas semer la panique ou aggraver l’anxiété au sein de l’équipe. Accordez à votre équipe le temps, et la confiance, de régler les problèmes.</p><h2>Prise de décision</h2><p>Peu importe que vous soyiez le genre de chef d’équipe qui mène de l’arrière ou de l’avant : votre équipe s’attend à ce que vous fassiez preuve de décision et de responsabilité. Ce « courage décisionnel » ne signifie pas la prise de risques, mais plutôt la fermeté, la conviction et la prise de responsabilité. Si votre compétence et votre titre appuient votre crédibilité, le manque de courage la saborde.</p><h2>Compétence</h2><p>Enfin, la compétence est toujours importante pour un chef d’équipe. Il ne s’agit pas de savoir coder ou de connaître votre clients ou leur secteur mieux que quiconque, mais plutôt, de posséder les qualités déclinées plus haut : compétence au niveau de l’empathie, de l’anticipation et de la planification, de la communication et de la prise de décision.</p><p>Soyez honnête avec votre équipe quant à votre niveau de compétence : cela lui permettra d’ajuster tout naturellement sa dynamique jusqu’à ce que tout le monde atteigne son plein potentiel dans chaque domaine.</p><p>D’aucuns diront que les développeurs de logiciels n’ont pas besoin de chef d’équipe. Certes, une équipe peut toujours se débrouiller tant bien que mal en l’absence de direction ; mais les problèmes sous-jacents ne se résolvent pas tout seuls. En fait, ils empêchent les développeurs de se concentrer sur leur mission, puisque ceux-ci doivent maintenant s’attarder sur la dynamique du groupe et les autres problèmes inhérents à la collaboration humaine. Les mauvais chefs d’équipe, en revanche, imposent leur volonté à l’équipe et divisent le groupe pour se rendre indispensables au progrès.</p><p>C’est pourquoi il est de l’intérêt de toute l’équipe – client et développeurs – d’avoir un bon chef d’équipe pour guider votre projet de développement logiciel. Si la valeur d’un bon chef d’équipe est difficile à isoler et à quantifier, elle éclate au grand jour dès que l’on constate l’amélioration de la performance des membres de son équipe.</p>
<p>Bien entendu, le facteur coût est important, mais ce coût ne dicte pas la <b>valeur</b> de votre idée. Dans cet article, nous examinerons la façon d’établir un budget qui appuiera votre idée, qui lui donnera un élan plutôt que de la freiner, pour que rien n’entrave vos rêves et votre inspiration.</p><h2>Faites vos devoirs</h2><p>Eh oui, les mots « rêves » et « inspiration » sont suivis du mot « devoirs ». En effet, préparation et information sont les prérequis d’une décision budgétaire éclairée.</p><h3>Préparation : comparaisons et leçons</h3><p>Les devoirs se composent de deux éléments complémentaires : préparation et philosophie. La <b>préparation</b> vous permet de faire des <b>comparaisons</b> pour vous situer. Idéalement, vous comparez des pommes avec des pommes, mais parfois, il n’existe pas deux pommes comparables. Gardez cela à l’esprit lorsque vous comparerez des « projets semblables », et évitez de présumer qu’un résultat en garantit un autre. De plus, demandez-vous <b>comment votre entreprise maintiendra ses activités malgré la surcharge d’un projet additionnel</b> (logiciel ou autre), puisqu’un projet de développement logiciel pourrait perturber vos activités quotidiennes. Les projets réussis reposent sur une robuste collaboration — ce qui, à son tour, exigera du temps de votre personnel. Enfin, tirez des leçons de vos projets antérieurs (et ceux des autres), et gardez-les toujours à l’esprit.</p><h3>Philosophie : assurance-coût et assurance-valeur</h3><p>Le second élément complétant vos devoirs consiste à définir votre <b>philosophie</b> budgétaire. L’on croit à tort que l’existence d’un budget représente une <b>assurance-coût</b> (sans parler de son incidence sur la certitude des délais !) Demandez-vous si vous recherchez réellement une assurance-coût, ou plutôt une <b>assurance-valeur</b>. La valeur est cultivée et affinée dans le temps — voulez-vous positionner votre équipe pour rechercher la valeur ? L’orientation valeur ne signifie pas un chèque en blanc ; elle signifie tout simplement que vous êtes disposé à revoir vos priorités et à affiner votre budget au fur et à mesure de l’évolution de la situation.</p><p>Un bon plan de <a href="https://www.spiria.com/fr/services/developpement-axe-performance/developpement-logiciel-sur-mesure/">développement logiciel</a> ne suit jamais le plan. Par contre, si vous faites bien vos devoirs, vous pourrez prendre des décisions en cours de projet et apporter des ajustements en fonction des faits nouveaux, d’un raisonnement sûr et de nouveaux objectifs, plutôt que de constamment éteindre des feux.</p><h2>Liste de vérification</h2><p>Cernons maintenant les éléments d’un projet de développement logiciel :</p><p><b>Travail, dont frais de déplacement</b> : Ceci inclut le travail de votre entreprise ou celui d’un consultant externe. N’oubliez pas que vous payez pour de l’expertise en plus de codage : les conseils techniques ne sont pas gratuits, et représentent en fait la plus importante partie de votre projet.</p><p><b>Équipement, licences de logiciels</b> : Tant d’options s’offrent à vous que vous devez clairement définir les objectifs et comprendre le contexte. Par exemple, un logiciel gratuit open source peut convenir dans certains cas, mais pas dans d’autres.</p><p><b>Impact sur les activités quotidiennes</b> : Votre projet enlèvera votre personnel de vos activités quotidiennes. Et, une fois le projet livré, vous devrez prévoir des coûts de formation et d’adoption du nouveau système jusqu’à ce qu’il soit pleinement intégré à vos activités.</p><p><b>Coûts de maintenance récurrents</b> : Vous devriez prévoir entre 10 % et 30 % du coût de projet initial par année. Au moment de dresser votre liste de vérification, évaluez les nombreux facteurs qui infléchiront le budget que vous devrez allouer à chacun des éléments suivants :</p><ul> <li>Coûts directs et indirects</li> <li>Coûts fixes et variables</li> <li>Coûts uniques et récurrents</li> <li>Désirs et nécessités</li></ul><p>Dresser une liste de vérification est facile ; l’assortir d’estimations réalistes est difficile. C’est là que votre préparation paie des dividendes : vos comparaisons vous aideront à estimer la dépense initiale, et les leçons vous aideront à faire votre évaluation de risques et d’imprévus. Enfin, le plus important reste l’acceptation, par tous, du budget et de la valeur intrinsèque du projet. Il faudra faire front commun au moment de recueillir des devis.</p><h2>Recueillir des devis</h2><p>Maintenant que vous avez un budget préliminaire pour votre projet de développement logiciel — pour votre idée ! — vous pouvez commencer à recueillir des devis des entreprises et fournisseurs.</p><p>Vous recevrez toute une fourchette de devis, dont quelques surprises. Examinez ces surprises : certaines témoignent peut-être d’éléments que vous avez oubliés ou sous-estimés, tandis que d’autres ne sont rien de plus que des « surclassements » ou des arnaques. Ou bien, elles pourraient indiquer que ces entreprises ou fournisseurs ont mal compris votre philosophie, ou qu’ils fondent leur devis non pas sur vos besoins, mais bien les leurs. Profitez-en pour revoir votre philosophie.</p><p>Enfin, n’oubliez pas que ces devis ne représentent qu’une partie du budget total — c’est pourquoi nous avons calculé les coûts indirects et l’impact de votre projet sur vos activités quotidiennes.</p><h2>Continuez à rêver !</h2><p>Nous avons commencé cet article en parlant de votre grande idée. L’énergie et la passion que vous avez ressenties au moment de la visite de votre idée doivent être mises en bouteille et partagées tout au long du projet. Le train-train quotidien de la gestion d’un budget risque d’obscurcir votre vision originale, ce qui pourrait la distordre.</p><p>Il existe un juste équilibre entre, d’une part, respecter le budget — même s’il est coulé dans le béton — et, d’autre part, donner vie à votre idée ; votre état de préparation et votre leadership vous aideront à réussir ce tour de funambule.</p><p>Et <a href="https://www.spiria.com/fr/">Spiria</a> est là pour vous aider. 😊</p>
<p>La préparation des données consiste à effectuer la récolte de données, la réconciliation <em>(Data Wrangling)</em> et même l’enrichissement si nécessaire.</p> <h2>Récolte des données</h2> <p>Premièrement, rassemblez les données dont vous aurez besoin pour l’apprentissage automatique. Veillez à les rassembler sous une forme consolidée, afin qu’elles soient toutes contenues dans une seule table <em>(Flat Table</em>).</p> <p>Pour cela, vous pouvez utiliser l’outil qui vous convient le plus. Par exemple :</p> <ul> <li>Outils de bases de données relationnelles (SQL)</li> <li>Jupiter notebook</li> <li>Excel</li> <li>Azure ML</li> <li>R Studio</li> </ul> <h2>Réconciliation <em>(Data Wrangling)</em></h2> <p>il s’agit de préparer les données pour les rendre utilisables par des algorithmes d’apprentissage automatique. <em>(Nettoyage des données, décomposition, agrégation, mise en forme et transformation, mise à l’échelle “Data Scaling”.)</em></p> <h3><em>Nettoyage des données</em></h3> <p>Il s’agit de trouver les valeurs “Null”, les valeurs manquantes et les données dupliquées.</p> <p>Exemple de valeurs manquantes :</p> <ul> <li>blanks</li> <li>NULL</li> <li>?</li> <li>N/A, NaN, NA</li> <li>9999999</li> <li>Unknown</li> </ul> <p> </p> <div> <table cellspacing="0" cellpadding="0"> <tbody> <tr> <th>#Row</th> <th>Title</th> <th>Type</th> <th>Format</th> <th>Price</th> <th>Pages</th> <th>NumberSales</th> </tr> <tr> <td>1</td> <td>Kids learning book</td> <td>Series – Learning – Kids -</td> <td>Big</td> <td>16</td> <td>100</td> <td>10</td> </tr> <tr> <td>2</td> <td>Guts</td> <td>One Book – Story - Kids</td> <td>Big</td> <td> </td> <td> </td> <td>3</td> </tr> <tr> <td>3</td> <td>Writing book</td> <td>Adults – learning- Series</td> <td> </td> <td>10</td> <td>120</td> <td>8</td> </tr> <tr> <td>5</td> <td>Dictation</td> <td>Series - Teenagers</td> <td>Small</td> <td>13</td> <td>85</td> <td>22</td> </tr> </tbody> </table> </div> <p><code>data_frame</code> dans ce code est notre ensemble de données sur “pandas”.</p> <pre><code>#Count the number of missing values in each row in Pandas dataframedata_frame.isnull().sum()</code></pre> <pre><code>#Row 0Title 0Type 0Price 1Format 1Pages 1NumberSales 0</code></pre> <p>Si certaines lignes ont des données manquantes dans plusieurs colonnes importantes, nous pouvons envisager de les supprimer. En utilisant la requête <code>DELETE</code> en SQL ou <code>pandas.drop()</code> en Python.</p> <p>On peut parfois remplacer la valeur manquante par zéro, la valeur la plus fréquente ou par la moyenne, cela dépend des valeurs et du type de colonne. En utilisant la requête de mise à jour <code>UPDATE</code> en SQL ou en utilisant <code>pandas.fillna()</code> en Python.</p> <p>Dans le code suivant, nous remplaçons les valeurs manquantes de “Pages” par la moyenne :</p> <pre><code>global_mean = data_frame.mean()data_frame['Pages'] = data_frame['Pages'].fillna(global_mean['Pages'])data_frame.isnull().sum()</code></pre> <pre><code>#Row 0Title 0Type 0Price 1Format 1Pages 0NumberSales 0</code></pre> <p>Et la valeur la plus fréquente, pour les valeurs manquantes de “Format” :</p> <pre><code>#Counts of unique valuesdata_frame["Format"].value_counts()</code></pre> <pre><code>Big 2Small 1Name: Format, dtype: int64</code></pre> <p>Comme “Big” est la valeur la plus fréquente dans notre cas, nous allons donc remplacer toutes les valeurs manquantes par “Big”.</p> <pre><code># Replace missing "Format" value with the most common value “Big”data_frame["Format"] = data_frame['Format'].fillna("Big")data_frame["Format"].value_counts()</code></pre> <pre><code>Big 3Small 1</code></pre> <p>Le <code>data_frame</code> résultant serait :</p> <div> <table cellspacing="0" cellpadding="0"> <tbody> <tr> <th>#Row</th> <th>Title</th> <th>Type</th> <th>Format</th> <th>Price</th> <th>Pages</th> <th>NumberSales</th> </tr> <tr> <td>1</td> <td>Kids learning book</td> <td>Series – Learning – Kids -</td> <td>Big</td> <td>16</td> <td>100</td> <td>10</td> </tr> <tr> <td>2</td> <td>Guts</td> <td>One Book – Story - Kids</td> <td>Big</td> <td>13</td> <td>100</td> <td>3</td> </tr> <tr> <td>3</td> <td>Writing book</td> <td>Adults – learning- Series</td> <td>Big</td> <td>10</td> <td>120</td> <td>8</td> </tr> <tr> <td>4</td> <td>Dictation</td> <td>Series - Teenagers</td> <td>Small</td> <td>13</td> <td>85</td> <td>22</td> </tr> </tbody> </table> </div> <p>Assurez-vous de ne pas avoir de doublons. S’il y en a, supprimez les lignes dupliquées (<code>DELETE</code> en SQL et <code>pandas.drop()</code> en Python).</p> <h3><em>Décomposition des données</em></h3> <p>Les colonnes de texte contiennent parfois plusieurs informations ; séparez-les en autant de colonnes dédiées que nécessaire. Si certaines colonnes représentent des catégories, convertissez-les en colonnes de catégories dédiées.</p> <p>Dans notre exemple, la colonne “Type” contient plus d’une information, nous pouvons clairement la diviser en 3 colonnes comme ci-dessous <em>(Style, Type et Readers)</em>, et faire ensuite le même processus pour les valeurs manquantes s’il y a lieu.</p> <div> <table cellspacing="0" cellpadding="0"> <tbody> <tr> <th>#Row</th> <th>Title</th> <th>Style</th> <th>Kind</th> <th>Readers</th> <th>Format</th> <th>Price</th> <th>Pages</th> <th>SalesMonth</th> <th>SalesYear</th> <th>NumberSales</th> </tr> <tr> <td>1</td> <td>Kids learning book</td> <td>Series</td> <td>Learning</td> <td>Kids</td> <td>Big</td> <td>16</td> <td>100</td> <td>11</td> <td>2019</td> <td>10</td> </tr> <tr> <td>2</td> <td>Guts</td> <td>One Book</td> <td>Story</td> <td>Kids</td> <td>Big</td> <td>13</td> <td>100</td> <td>12</td> <td>2019</td> <td>3</td> </tr> <tr> <td>3</td> <td>Writing book</td> <td>Series</td> <td>learning</td> <td>Adults</td> <td>Big</td> <td>10</td> <td>120</td> <td>10</td> <td>2019</td> <td>8</td> </tr> <tr> <td>4</td> <td>Writing book</td> <td>Series</td> <td>learning</td> <td>Adults</td> <td>Big</td> <td>10</td> <td>120</td> <td>11</td> <td>2019</td> <td>13</td> </tr> <tr> <td>5</td> <td>Dictation</td> <td>Series</td> <td>learning</td> <td>Teenagers</td> <td>Small</td> <td>13</td> <td>85</td> <td>9</td> <td>2019</td> <td>17</td> </tr> <tr> <td>6</td> <td>Dictation</td> <td>Series</td> <td>learning</td> <td>Teenagers</td> <td>Small</td> <td>13</td> <td>85</td> <td>10</td> <td>2019</td> <td>22</td> </tr> </tbody> </table> </div> <h3><em>Agrégation des données</em></h3> <p>Il s’agit du regroupement des données, si c’est pertinent.</p> <p>Dans notre exemple, le nombre de ventes est en fait déjà une agrégation de données. Au départ, la base de données présentait des lignes de transactions, que nous avons agrégées pour obtenir le nombre de livres vendus par mois.</p> <h3><em>Mise en forme et transformation des données</em></h3> <p>Cela implique la conversion de données catégorielles en données numériques, puisque les algorithmes ne peuvent utiliser que des valeurs numériques.</p> <p>Les colonnes Style, Kind, Readers et Format sont clairement des colonnes de type catégorie. Nous allons voir deux façons de faire la transformation.</p> <p><strong>1. <em>Premièrement méthode, convertir toutes les valeurs de type catégorie en valeurs numériques</em></strong> en remplaçant toutes les valeurs uniques par des nombres séquentiels.</p> <p>Exemple de la façon de le faire en Python :</p> <pre><code>cleanup_nums = {"Format": {"Big": 1, "Small": 2}, "Style": {"Serie": 1, "One Book": 2}, "Kind": {"Learning": 1, "Story": 2}, "Readers": {"Adults": 1, "Teenagers": 2, "Kids": 3} }data_frame.replace(cleanup_nums, inplace=True)data_frame.head()</code></pre> <p>Ce qui nous donne :</p> <div> <table cellspacing="0" cellpadding="0"> <tbody> <tr> <th>#Row</th> <th>Title</th> <th>Style</th> <th>Kind</th> <th>Readers</th> <th>Format</th> <th>Price</th> <th>Pages</th> <th>SalesMonth</th> <th>SalesYear</th> <th>NumberSales</th> </tr> <tr> <td>1</td> <td>Kids learning book</td> <td>1</td> <td>1</td> <td>3</td> <td>1</td> <td>16 $</td> <td>100</td> <td>11</td> <td>2019</td> <td>10</td> </tr> <tr> <td>2</td> <td>Guts</td> <td>2</td> <td>2</td> <td>3</td> <td>1</td> <td>13 $</td> <td>100</td> <td>12</td> <td>2019</td> <td>3</td> </tr> <tr> <td>3</td> <td>Writing book</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>10 $</td> <td>120</td> <td>10</td> <td>2019</td> <td>8</td> </tr> <tr> <td>3</td> <td>Writing book</td> <td>1</td> <td>1</td> <td>1</td> <td>1</td> <td>10 $</td> <td>120</td> <td>11</td> <td>2019</td> <td>13</td> </tr> <tr> <td>4</td> <td>Dictation</td> <td>1</td> <td>1</td> <td>2</td> <td>2</td> <td>13</td> <td>85</td> <td>9</td> <td>2019</td> <td>17</td> </tr> <tr> <td>4</td> <td>Dictation</td> <td>1</td> <td>1</td> <td>2</td> <td>2</td> <td>13</td> <td>85</td> <td>10</td> <td>2019</td> <td>22</td> </tr> </tbody> </table> </div> <p><img title=" " src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/684705bb086a552384a7a899_output_1.png" alt=" " /></p> <p><strong>2. <em>La deuxième méthode est celle des “dummies” :</em></strong> elle consiste à créer une colonne séparée pour chacune des valeurs uniques des colonnes de catégories. Comme la valeur de chaque colonne est binaire (0/1), vous ne pouvez donc avoir qu’une seule valeur 1 dans les colonnes nouvellement générées.</p> <p>Faire cette conversion en Python :</p> <pre><code># Convert category to dummydata_frame = pd.get_dummies(data_frame, columns=["Format"])data_frame = pd.get_dummies(data_frame, columns=["Style"])data_frame = pd.get_dummies(data_frame, columns=["Kind"])data_frame = pd.get_dummies(data_frame, columns=["Readers"])data_frame.head()</code></pre> <p>Vous remarquerez ci-dessous que “Format” a généré deux colonnes, “Format_Big” et “Format_Small”, parce que la colonne a deux valeurs distinctes, “Big” et “Small”. Cependant, “Readers” génère trois colonnes différentes parce qu’elle a trois valeurs distinctes “Adults”, “Teenagers” et “Kids”.</p> <div> <table cellspacing="0" cellpadding="0"> <tbody> <tr> <th>Id</th> <th>Title</th> <th>Style_Series</th> <th>Style_OneBook</th> <th>Kind_Learning</th> <th>Kind_Story</th> <th>Readers_Adults</th> <th>Readers_Teenagers</th> <th>Readers_Kids</th> <th>Format_Big</th> <th>Format_Small</th> <th>Price</th> <th>Pages</th> <th>SalesMonth</th> <th>SalesYear</th> <th>NumberSales</th> </tr> <tr> <td>1</td> <td>Kids learning book</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>0</td> <td>1</td> <td>1</td> <td>0</td> <td>16</td> <td>100</td> <td>11</td> <td>2019</td> <td>10</td> </tr> <tr> <td>2</td> <td>Guts</td> <td>0</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>1</td> <td>0</td> <td>13</td> <td>100</td> <td>12</td> <td>2019</td> <td>3</td> </tr> <tr> <td>3</td> <td>Writing book</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>0</td> <td>10</td> <td>120</td> <td>10</td> <td>2019</td> <td>8</td> </tr> <tr> <td>3</td> <td>Writing book</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>0</td> <td>10</td> <td>120</td> <td>11</td> <td>2019</td> <td>8</td> </tr> <tr> <td>4</td> <td>Dictation</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>13</td> <td>85</td> <td>9</td> <td>2019</td> <td>22</td> </tr> <tr> <td>4</td> <td>Dictation</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>13</td> <td>85</td> <td>10</td> <td>2019</td> <td>22</td> </tr> </tbody> </table> </div> <p><img title=" " src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/684705be378cc0db15094f51_output_2.png" alt=" " /></p> <p>* Les colonnes “Id” et “Title” ne seront pas utilisées pendant notre processus de ML.</p> <p>L’avantage de la méthode des <em>dummies</em> est que toutes les valeurs ont le même poids. Cependant, comme elle ajoute autant de nouvelles colonnes que le nombre de catégories distinctes de chaque colonne initiale, soyez prudent avec l’utilisation de cette méthode si vous avez déjà de nombreuses colonnes à prendre en compte dans votre processus de ML.</p> <p>D’autre part, si vous procédez en remplaçant les valeurs de catégories par des valeurs numériques séquentielles, cela peut avantager certaines catégories si leur nombre est plus élevé. Pour “Readers”, par exemple, la catégorie 3 aura un impact trois fois plus important que la catégorie 1. Imaginez le cas où vous aurez une colonne avec un très grand nombre de catégories.</p> <h3><em>Mise à l’échelle (Data Scaling)</em></h3> <p>Cela permet d’obtenir des données numériques à une échelle commune, si ce n’est déjà le cas. La mise à l’échelle des données ne s’applique pas au <em>label</em> ou aux <em>colonnes de catégories</em>. Elle est nécessaire lorsqu’il y a une grande variation entre les plages de valeurs des différentes colonnes.</p> <p>Nous devons procéder à une mise à l’échelle, pour donner encore une fois le même poids à toutes les colonnes.</p> <p>Dans notre exemple, nous allons mettre à l’échelle les colonnes “Price” et “Pages” :</p> <ol> <li>Price [10, 16]</li> <li>Pages [85, 120]</li> </ol> <p>Nous devons mettre ces deux colonnes à l’échelle, sinon la colonne “Pages” aura plus de poids sur le résultat que la colonne “Price”.</p> <p>Il existe de nombreuses méthodes de mise à l’échelle. Pour notre exemple, nous utiliserons le <code>MinMaxScaler</code> de 0 à 1.</p> <pre><code>#scale the columnsscaler = MinMaxScaler()rescaledX = scaler.fit_transform(X[:,0:2])#put the scaled columns in dataframecolnames = [ 'Price', 'Pages']df_scaled = pd.DataFrame(rescaledX, columns=colnames)# Replace the original columns with the new scaleddata_frame_scalled = data_framedata_frame_scalled[colnames] = df_scaled[colnames]data_frame_scalled.head()</code></pre> <p>Le résultat est le suivant :</p> <div> <table cellspacing="0" cellpadding="0"> <tbody> <tr> <th>Id</th> <th>Title</th> <th>Style_1</th> <th>Style_2</th> <th>Kind_1</th> <th>Kind_2</th> <th>Readers_1</th> <th>Readers_2</th> <th>Readers_3</th> <th>Format_1</th> <th>Format_2</th> <th>Price</th> <th>Pages</th> <th>NumberSales</th> </tr> <tr> <td>1</td> <td>Kids learning book</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>0</td> <td>1</td> <td>1</td> <td>0</td> <td>1</td> <td>0.42857143</td> <td>10</td> </tr> <tr> <td>2</td> <td>Guts</td> <td>0</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>1</td> <td>0</td> <td>0.5</td> <td>0.42857143</td> <td>3</td> </tr> <tr> <td>3</td> <td>Writing book</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>8</td> </tr> <tr> <td>4</td> <td>Dictation</td> <td>1</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>0</td> <td>0</td> <td>1</td> <td>0.5</td> <td>0</td> <td>22</td> </tr> </tbody> </table> </div> <p><img title=" " src="https://cdn.prod.website-files.com/67c06f07cfba9b0adb43e16c/684705c1a0ed67c8acd0bf06_output_3.png" alt=" " /></p> <p>Comme indiqué, il existe de nombreuses autres méthodes de mise à l’échelle ; la manière et le moment opportun d’utiliser chacune d’entre elles feront l’objet d’un prochain article.</p>
Développement sur mesure
5 min de lecture
Le PMV, la stratégie gagnante pour être rapidement sur le marché !
<p>Le PMV, le « Produit Minimum Viable », est essentiellement une version du logiciel présentant juste assez de fonctionnalités pour satisfaire les premiers clients et obtenir d’utiles retours d’information pour les futurs développements du produit. Mais à qui est-ce de déterminer ce qui compose le PMV ? La réponse est « à tous les acteurs ». Le créateur lui-même, mais aussi et surtout l’utilisateur final, ainsi que le développeur, qui pourra aider à trancher entre finalité et éventuelle difficulté technique.</p><p>Il est important de se rappeler qu’on pourra éventuellement en faire des itérations (des versions 2, 3, etc.), et qu’il sera possible d’ajouter de nouveaux incréments à chaque sprint. L’itération et le perfectionnement continu sont un modèle éprouvé depuis plusieurs années pour de nombreux types de produits, et c’est lui qui permet de sortir la création le plus rapidement possible !</p><p>La nature même du <a href="https://www.spiria.com/fr/services/developpement-axe-performance/developpement-logiciel-sur-mesure/">développement logiciel</a> s’y prête parfaitement avec des méthodologies adaptées. L’objectif est de recevoir des commentaires et opinions le plus rapidement possible et ainsi d’encore mieux cerner les désirs des utilisateurs et ce qui fera pencher la balance dans les options constitutives du produit. Conséquemment, ces retours fourniront également la « liste de souhaits » des versions 2, 3, et ainsi de suite.</p><p>Plus concrètement, c’est avec des maquettes, des démonstrations de faisabilité (<em>Proof of Concept</em>), des “spikes” et finalement un <a href="https://www.spiria.com/fr/services/design-centre-sur-utilisateur/prototypage-produit-logiciel/">prototype</a> fonctionnel, créés conjointement par des <a href="https://www.spiria.com/fr/services/design-centre-sur-utilisateur/design-experience-utilisateur/">designers/experts UX</a> et des développeurs, qu’on peut arriver au meilleur PMV possible. Une boucle de rétroaction fréquente (par exemple à chaque démo de sprint, ou encore à chaque étape importante que l’on s’est donnée) est la clé du succès.</p><p>En développement logiciel, il est possible que certaines fonctionnalités soient plus complexes à réaliser ou encore contiennent plusieurs incertitudes techniques. On doit se demander — et confirmer l’hypothèse — si cette fonctionnalité est essentielle au lancement initial, sinon elle ne fera que retarder la mise en marché.</p><p>La première impression d’un nouveau produit est cruciale, et on se doit d’avoir potentiellement au moins une fonctionnalité qui le différencie de la concurrence, qui innove ou atteint particulièrement bien son but. On recommande d’en sélectionner quelques-unes et de laisser les autres pour les versions à suivre. Le juste milieu, le PMV créant un engouement tout en n’offrant pas tout d’un coup, est un mélange magique qu’on peut atteindre à l’aide d’une équipe méticuleuse épaulée d’expert logiciel qui pourra calculer la faisabilité/complexité, la mesurer aux autres fonctionnalités et émettre des recommandations techniques.</p><p>Généralement, un produit minimum viable contient chaque strate d’un produit, mais en version incomplète (par exemple, on veut qu’il y ait un forum ou un blogue, mais au début, il n’y aura pas la possibilité de faire de recherche dans ledit forum/blog). Parfois, une fonctionnalité entière n’est pas vraiment essentielle et fera un ajout très excitant pour l’utilisateur lorsqu’elle sera proposée.</p><p>Attention, incomplet ne signifie pas « qui contient encore des anomalies ! » Si l’utilisateur expérimente trop d’anicroches, il peut vivre une telle frustration qu’il ne reviendra pas pour les versions subséquentes.</p>
<p>De nombreux langages de programmation modernes vous permettent de parvenir à cette accélération grâce au code asynchrone et aux valeurs futures. Le principe de base de l’asynchronisation et des valeurs futures est que les fonctions appelées sont exécutées sur un autre fil, et les valeurs de retour sont converties en ce que l’on appelle une valeur future. De telles valeurs futures ne possèdent pas de valeur réelle tant que la fonction asynchrone n’est pas terminée. La fonction s’exécute simultanément et, lorsqu’elle finit par retourner une valeur, la variable de la valeur future est mise à jour en arrière-plan pour conserver cette valeur de retour. Il n’est pas nécessaire d’utiliser des mutex ou un système de messages inter-fil (“interthread”) explicite : toute la synchronisation entre l’appel initial et le deuxième fil d’exécution est effectuée en arrière-plan. Lorsque le fil initial accède à la valeur future, il est automatiquement mis en pause jusqu’à ce que la valeur soit prête.</p><p>Le principal avantage de ce système est qu’il est très facile d’avoir un fonctionnement asynchrone. Bien sûr, le programmeur doit s’assurer que la fonction peut réellement être exécutée dans un autre fil en toute sécurité, qu’il n’y a pas de course aux données avec d’autres fils d’exécution. Les fonctions asynchrones et les valeurs futures ne font que créer des fils d’exécution pour générer un résultat.</p><h2>Les problèmes écartés</h2><p>Mon but ici n’est pas de discuter de la manière de concevoir des algorithmes sans course entre fils, ni de la manière d’arranger des données pour faciliter l’exécution d’algorithmes multi-fils. Je me contenterai de mentionner qu’un moyen possible d’y parvenir consiste à éviter toute valeur globale et à transmettre toutes les données à la fonction asynchrone par valeur. De cette façon, rien n’est partagé entre les fils et donc aucune concurrence ne peut se produire.</p><h2>Les problèmes abordés</h2><p>Si l’asynchronisme et les valeurs futures permettent de transformer facilement une fonction en un fil conducteur, c’est cette simplicité même qui pose problème. Elle signifie une absence totale de contrôle. Plus précisément, vous n’avez aucun contrôle sur :</p><ul> <li>combien de fonctions asynchrones sont exécutées,</li> <li>combien de fils sont créés pour exécuter ces fonctions,</li> <li>combien de fils attendent des résultats.</li></ul><p>Cela nécessite un équilibre délicat entre le désir de maximisation de l’utilisation du processeur et le maintien d’un certain contrôle. D’une part, vous voulez qu’un maximum de fonctions asynchrones soit exécuté, afin que le processeur soit toujours entièrement occupé ; d’autre part, vous ne voulez pas arriver à une surcharge du processeur avec trop de fils.</p><h2>La solution</h2><p>La meilleure solution à ce problème est d’introduire une certaine complexité. La complexité ajoutée vous permet de reprendre le contrôle de tous les éléments énumérés ci-dessus.</p><h3>Première étape : le pool de fils</h3><p>La première étape consiste à renoncer aux fonctions asynchrones et aux valeurs futures pour maximiser l’utilisation du processeur. Ceux-ci peuvent toujours être utilisés pour exécuter des algorithmes parallèles, mais pas pour créer des fils multiples. Il est préférable d’utiliser un pool de fils (“thread pool”).</p><p>Un pool de fils vous permet de contrôler le nombre de fils créés pour exécuter des algorithmes parallèles parallèles. Vous pouvez créer exactement autant de fils qu’il y a de cœurs dans le processeur, ce qui garantit un débit maximal exact sans surcharge du processeur.</p><h3>Deuxième étape : la file d’attente des travaux</h3><p>Si le pool de fils contrôle le nombre de fils utilisés, il ne contrôle pas la manière dont les fonctions sont gérées par ces fils. C’est le travail de la file d’attente des travaux. Les fonctions à exécuter de manière asynchrone sont ajoutées à la file, et le pool prend des fonctions de cette file pour les exécuter et produire des résultats.</p><h3>Troisième étape : les résultats</h3><p>Alors que la file d’attente des travaux s’occupe de l’entrée des algorithmes parallèles, nous avons besoin d’une autre fonction pour gérer l’attente des résultats. Alors qu’on aurait pu utiliser une file d’attente de résultats, nous avons une meilleure option : les valeurs futures ! La synchronisation entre le producteur d’un résultat et son consommateur est exactement l’utilité des valeurs futures. La principale différence par rapport au problème initial est que les résultats sont ici créés par le pool de fils.</p><h3>Quatrième étape : le vol de fils</h3><p>Un problème est alors de savoir ce qui se passe si l’algorithme parallèle soumet des sous-algorithmes à la file d’attente des travaux et attend leurs résultats. Nous pourrions nous retrouver à court de fils ! Chaque fil pourrait attendre que les résultats soient produits alors qu’aucun fil n’est disponible pour produire ces résultats.</p><p>La solution à ce problème est le concept de vol de fil en attente d’un résultat. Essentiellement, vous fournissez une fonction qui peut travailler sur la file d’attente des travaux tant qu’un résultat précis n’est pas disponible. Nous évitons donc d’accéder directement aux valeurs futures produites, car accéder à une valeur future non disponible bloquerait le fil. Au lieu de cela, nous passons la valeur future à la file d’attente, qui peut alors exécuter des travaux en attendant que la valeur soit prête.</p><h2>Exemple de code concret</h2><p>J’ai mis en place un tel système à plusieurs reprises dans le passé. Je l’ai récemment réimplémenté dans une application open-source, écrite en C++. Cette application s’appelle <i>Tantrix Solver</i> et comme son nome l’indique, elle solutionne les puzzles Tantrix. Le code de l’application est disponible sur GitHub et contient plusieurs branches Git :</p><ul> <li>Une branche utilise des fonctions asynchrones et les valeurs futures.</li> <li>Une autre branche montre le même algorithme en utilisant le design suggéré.</li></ul><p>Le repo sur GitHub est disponible <a href="https://github.com/pierrebai/Tantrix">ici</a>.</p><h3>Version asynchrone avec valeurs futures</h3><p>La branche Git contenant la version asynchrone avec valeurs futures est appelée “thread-by-futures”.</p><p>Le design du code dans cette branche est simple. C’est son principal avantage. Il utilise la fonction C++ <code>std::async</code> avec le mode <code>std::launch::async</code> pour créer des fils. Cependant, les problèmes que nous avons mentionnés se manifestent comme prévu, avec un nombre incontrôlé de fils. Un puzzle Tantrix simple peut engendrer la création de quelques dizaines de fils, ce qui est probablement trop, mais reste encore gérable. En revanche, un puzzle Tantrix complexe peut créer plusieurs <b>centaines</b> de fils, ce qui peut saturer la plupart des ordinateurs.</p><h3>Pool de fils et file d’attente des travaux</h3><p>La branche Git contenant la version avec pool de fils et file d’attente est appelée “thread-pool”. Je vais décrire plus en détail la conception du code, car elle est plus complexe, bien que j’aie essayé de la garder aussi simple que possible.</p><h3>Conception du code : les parties faciles</h3><p>Dans cette section, je présenterai les éléments les plus simples du design.</p><p>Le premier élément du design est la classe du pool de fils. Il suffit de lui donner un “fournisseur de travail” et le nombre de fils à créer :</p><pre><code style="white-space: pre;"> // A pool of threads of execution. struct thread_pool_t { // Create a thread pool with a given number of threads // that will take its work from the given work provider. thread_pool_t(work_provider_t& a_work_provider, size_t a_thread_count = 0); // Wait for all threads to end. ~thread_pool_t(); private: // The internal function that execute queued functions in a loop. static void execution_loop(thread_pool_t* self); };</code></pre><p>Le fournisseur de travail (“work provider”) indique aux fils ce qu’ils doivent faire. Il contrôle l’arrêt et l’exécution des algorithmes. Il dispose d’une fonction d’attente et d’exécution qui encapsule entièrement l’exécution d’un travail ou l’attente d’une fonction à exécuter. Les détails de la cette fonction seront présentés ultérieurement, dans son implémentation concrète. Pour l’instant, voici la conception du fournisseur :</p><pre><code style="white-space: pre;"> // The provider of work for the pool. struct work_provider_t { // Request that the threads stop. virtual void stop() = 0; // Check if stop was requested. virtual bool is_stopped() const = 0; // The wait-or-execute implementation, called in a loop // by the threads in the thread =s pool. virtual void wait_or_execute() = 0; };</code></pre><p>Ces deux classes précédentes sont cachées dans la file d’attente de travail. Pour cette raison, elles peuvent en fait être totalement ignorées par les utilisateurs de la conception. C’est pourquoi nous ne les aborderons pas plus avant.</p><h3>Conception du code : les parties intermédiaires</h3><p>La queue de travail est la pièce la plus complexe. Sa mise en œuvre est un template C++ pour faciliter l’utilisation d’un algorithme donné qui produit un type spécifique de résultats.</p><p>Comme il s’agit de la partie centrale du design, je la présenterai dans son intégralité. Je diviserai la présentation de la classe en plusieurs parties pour la rendre plus facile à comprendre.</p><p>La première partie de la conception est constituée des paramètres du template C++ :</p><pre><code style="white-space: pre;"> template <class WORK_ITEM, class RESULT> struct threaded_work_t : work_provider_t { using result_t = typename RESULT; using work_item_t = typename WORK_ITEM; using function_t = typename std::function<result_t(work_item_t, size_t)>;</code></pre><p>Le <code>work_item_t</code> (WORK_ITEM) est la donnée d’entrée de l’algorithme. Le <code>result_t</code> (RESULT) est la sortie de l’algorithme. La <code>function_t</code> est l’algorithme. En utilisant une fonction, nous pouvons prendre en charge une <i>famille</i> d’algorithmes ayant les mêmes types en entrée et en sortie. Lorsqu’un <code>work_item_t</code> est ajouté, l’appelant fournit également la fonction à appliquer.</p><p>La deuxième partie de la conception de la file d’attente des travaux est l’ensemble des types de données de mise en œuvre et les variables membres. Les voici :</p><pre><code style="white-space: pre;"> using task_t = std::packaged_task<result_t(work_item_t, size_t)>; // How the function, work item and recursion depth is kept internally. struct work_t { task_t task; work_item_t item; }; std::mutex my_mutex; std::condition_variable my_cond; std::atomic<bool> my_stop = false; std::vector<work_t> my_work_items; const size_t my_max_recursion; // Note: the thread pool must be the last variable so that it gets // destroyed first while the mutex, etc are still valid. thread_pool_t my_thread_pool;</code></pre><p>Le type <code>task_t</code> contient la fonction dans un type C++ spécial qui peut l’appeler tout en produisant un <code>std::future</code> en C++. C’est ainsi que les valeurs futures sont créées. Le <code>work_t</code> est l’unité de travail qui peut être exécutée par un fil.</p><p>Les deux premières variables membres de la file d’attente sont le mutex et la variable de condition qui servent toutes deux à protéger les données partagées entre les fils d’exécution et le fil principal.</p><p>La variable atomique <code>my_stop</code> est utilisée pour signaler que toute exécution doit s’arrêter (quelle surprise !). Le vecteur de <code>work_t</code> contient les unités de travail à exécuter. Il s’agit donc de la file d’attente elle-même. La variable <code>max_recursion</code> est un détail de l’implémentation utilisé pour éviter la récursion trop profonde due au vol de fils. Ce sera expliqué plus en détail plus tard. Le <code>thread_pool</code> est évidemment l’endroit où les fils d’exécution sont gardés.</p><p>La troisième partie du design est la création de la file d’attente des travaux et l’implémentation de l’interface <code>work_provider_t</code>. Tout cela est relativement simple. Nous créons le pool de fils internes avec le nombre exact de cœurs du processeur. Nous faisons également passer la file d’attente de travail elle-même en tant que “fournisseur de travail” (<code>work_provider_t</code>) du pool de fils.</p><pre><code style="white-space: pre;"> // Create a threaded work using the given thread pool. threaded_work_t(size_t a_max_recursion = 3) : my_max_recursion(a_max_recursion), my_thread_pool(*this, std::thread::hardware_concurrency()) {} ~threaded_work_t() { stop(); } // Stop all waiters. void stop() override { my_stop = true; my_cond.notify_all(); } // Check if it is stopped. bool is_stopped() const override { return my_stop; } // Wait for something to execute or execute something already in queue. void wait_or_execute() override { std::unique_lock lock(my_mutex); return internal_wait_or_execute(lock, 0); }</code></pre><p>La mise en œuvre des fonctions de destruction et d’arrêt (<code>stop</code>) utilise simplement le drapeau d’arrêt (<code>my_stop</code>) et la variable de condition pour signaler l’arrêt à tous les fils d’exécution. La fonction <code>wait_or_execute</code> appelle simplement une autre fonction interne, plus complexe, qui sera décrite dans la section suivante.</p><h3>Conception du code : les parties complexes</h3><p>Dans cette section, nous arrivons enfin au cœur du design, les détails les plus complexes du code.</p><p>Tout d’abord, la fonction d’attente d’un résultat donné. Cette partie est encore facile à comprendre. Tant que la valeur future n’est pas prête, nous continuons à attendre l’arrivée de nouveaux résultats ou de nouveaux travaux à exécuter. C’est ici que nous travaillons pour d’autres algorithmes en file d’attente au lieu de dormir bêtement et de perdre un fil d’exécution. Si nous recevons le signal de tout arrêter, nous sortons rapidement avec un résultat vide.</p><pre><code style="white-space: pre;"> // Wait for a particular result, execute work while waiting. result_t wait_for(std::future<result_t>& a_token, size_t a_recusion_depth) { while (!is_stopped()) { std::unique_lock lock(my_mutex); if (a_token.wait_for(std::chrono::seconds(0)) == std::future_status::ready) return a_token.get(); internal_wait_or_execute(lock, a_recusion_depth); } return {}; }</code></pre><p>Deuxièmement, la fonction qui exécute réellement l’unité de travail. Elle attend quand il n’y a rien à exécuter. En revanche, lorsqu’il <i>y a</i> au moins une unité de travail en file d’attente, elle exécute son algorithme, qui produira un nouveau résultat.</p><pre><code style="white-space: pre;"> private: // Wait for something to execute or execute something already in queue. void internal_wait_or_execute(std::unique_lock<std::mutex>& a_lock, size_t a_recursion_depth) { if (my_stop) return; if (my_work_items.size() <= 0) { my_cond.wait(a_lock); return; } work_t work = std::move(my_work_items.back()); my_work_items.pop_back(); a_lock.unlock(); work.task(work.item, a_recursion_depth + 1); my_cond.notify_all(); }</code></pre><p>La seule chose subtile à observer, c’est que si la fonction était en attente et est réveillée, alors elle retourne immédiatement au lieu d’essayer d’exécuter un travail quelconque. Il y a une bonne raison pour un retour immédiat : le réveil peut être dû à un résultat devenant disponible ou à une unité de travail ajoutée. Comme nous ne savons pas de quel cas il s’agit et comme l’appelant pourrait être intéressé par ces nouveaux résultats, nous revenons à l’appelant pour qu’il puisse vérifier. Peut-être que la valeur future qu’il attendait est prête !</p><p>Enfin, voici la fonction de soumission des travaux à exécuter :</p><pre><code style="white-space: pre;"> // Queue the the given function and work item to be executed in a thread. std::future<result_t> add_work(work_item_t a_work_item, size_t a_recusion_depth, function_t a_function) { if (my_stop) return {}; // Only queue the work item if we've recursed into the threaded work only a few times. // Otherwise, we can end-up with too-deep stack recursion and crash. if (a_recusion_depth < my_max_recursion) { // Shallow: queue the function to be called by any thread. work_t work; work.task = std::move(task_t(a_function)); work.item = std::move(a_work_item); auto result = work.task.get_future(); { std::unique_lock lock(my_mutex); my_work_items.emplace_back(std::move(work)); } my_cond.notify_all(); return result; } else { // Too deep: call the function directly instead. std::promise<result_t> result; result.set_value(a_function(a_work_item, a_recusion_depth + 1)); return result.get_future(); } }</code></pre><p>La principale chose inattendue à remarquer est la vérification de la profondeur de récursivité. Le problème subtil que ceci vise à éviter est lié à la mise en œuvre des fonctions <code>wait_for()</code> et <code>wait_or_execute()</code>. Comme l’attente peut provoquer l’exécution d’une autre unité de travail et cette unité de travail pourrait à son tour finir par attendre et exécuter une autre unité de travail, cela pourrait faire boule de neige et devenir une récursion très profonde.</p><p>Malheureusement, nous ne pouvons pas refuser d’exécuter un travail, car cela pourrait signifier que tous les fils cesseraient d’exécuter du travail. Le système cesserait de travailler et s’arrêterait ! Ainsi, au lieu de cela, lorsque le maximum de profondeur est atteint à l’intérieur d’un fils d’exécution, tout travail supplémentaire mis en file d’attente par ce fils est exécuté immédiatement.</p><p>Bien que cela semble équivalent à mettre en file d’attente le travail à accomplir, ce n’est pas le cas. Comme vous voyez, la quantité de travail nécessaire pour évaluer une branche d’un algorithme est limitée. En contraste, le nombre d’unités de travail qui peuvent être mises en attente en raison de <b>toutes</b> les branches des algorithmes peut être extrêmement grand. En effet, on peut supposer que l’algorithme a été conçu de manière à ce <b>qu’une</b> branche ne cause pas de récursion excessive. Nous ne pouvons pas supposer la même chose sur le total de tous les travaux mis en attente par plusieurs branches indépendantes de l’algorithme.</p><p>Pour cette raison, il est également judicieux de vérifier la profondeur de récurrence dans l’algorithme lui-même et ne pas même mettre en file d’attente ces éléments de travail, une fois la profondeur maximale de récursion atteinte. Il devrait plutôt appeler leur fonction directement dans l’algorithme, plutôt que passer par la file d’attente, pour rendre le tout plus efficace.</p><p>En dehors de cette subtilité, le reste du code ne fait que mettre en file d’attente l’unité de travail et réveiller tout fil qui voulait exécuter un travail.</p><h2>Conclusion</h2><p>Comme on le voit, cette mise en place d’une file d’attente de travail remplace les fonctions asynchrones et les valeurs futures par une réserve de fils. Pour l’appelant, il suffit de connaître deux fonctions : <code>add_work()</code> et <code>wait_for()</code>. L’interface est donc assez simple à utiliser, mais il donne qu’un contrôle supplémentaire sur le multithreading pour éviter de surcharger le processeur.</p><p>J’espère qu’un jour, le standard C++ sera doté d’un design intégré pour les files d’attente et les pools de fils, afin de ne pas avoir à les faire à la main comme ici. D’ici là, n’hésitez pas à réutiliser mon design.</p>
<h2>La duplication</h2><p>Cette première approche est la plus simple. Il suffit de dupliquer les données pour chaque thread d’exécution. Pour que cela fonctionne, les données doivent répondre à quelques critères :</p><ul> <li>être faciles à identifier,</li> <li>ne pas avoir de parties cachées,</li> <li>être faciles à reproduire,</li> <li>ne pas avoir d’exigences essentielles pour être partagées en continu.</li></ul><p>Si les données répondent à tous ces critères, la duplication est alors la solution la plus rapide et la plus sûre. Habituellement, les données qui peuvent être utilisées de cette manière sont essentiellement un groupe de valeurs, comme une structure pure en C++, contenant des valeurs simples.</p><h2>L’emballage</h2><p>Si vos données ne répondent pas au critère de duplication, l’approche de l’emballage des données peut être utilisée. Un cas courant est celui où on a une interface qui devrait être partagée entre plusieurs threads d’exécution. Voici les étapes de la création d’un emballage :</p><ul> <li>Identifier l’interface qui doit être isolée.</li> <li>Écrire un simple protecteur multithread sur l’interface.</li> <li>Écrire une implémentation simple de l’interface pour chaque thread.</li></ul><p>Pour illustrer la technique, je vais vous montrer un exemple d’emballage que j’ai récemment fait en C++. Le code fait partie de l’application <i>Tantrix Solver</i> que j’ai écrite. L’élément particulier que je devais convertir pour une utilisation multithread était l’interface du rapport d’avancement.</p><p>Le code pour cette application est <a href="https://github.com/pierrebai/Tantrix">disponible sur GitHub</a>.</p><h3>Identifier l’interface</h3><p>La première étape consiste à identifier ce qui sera utilisé par les threads. Cela peut nécessiter un certain remaniement dans le cas d’un groupe disparate d’éléments sans cohésion. Dans l’exemple de code, il s’agissait déjà d’une interface appelée <code>progress_t</code>. À noter qu’il n’y a qu’une seule fonction virtuelle qui doit être protégée : <code>update_progress()</code>.</p><pre><code> // Report progress of work. // // Not thread safe. Wrap in a multi_thread_progress_t if needed. struct progress_t { // Create a progress reporter. progress_t() = default; // Force to report the progress tally. void flush_progress(); // Clear the progress. void clear_progress(); // Update the progress with an additional count. void progress(size_t a_done_count); size_t total_count_so_far() const; protected: // Update the total progress so far to the actual implementation. virtual void update_progress(size_t a_total_count_so_far) = 0; };</code></pre><h3>Protecteur multithread</h3><p>La deuxième étape consiste à créer un protecteur multithread. La conception de tous les protecteurs est toujours la même :</p><ul> <li>Ne <b>pas</b> dériver de l’interface à protéger.</li> <li>Conserver l’implémentation originale de l’interface non protégée.</li> <li>Fournir une protection multithread, généralement avec un mutex.</li> <li>Fournir un accès interne à l’implémentation par thread.</li></ul><p>La raison de ne pas implémenter l’interface souhaitée est que le protecteur multithread n’est pas destiné à être utilisé directement. En le rendant non compatible, il ne peut pas être utilisé accidentellement.</p><p>Sa mise en œuvre imitera toujours très fidèlement l’interface. La différence est que chaque fonction correspondante verrouillera le mutex et appellera l’interface originale, non sécurisée pour les threads. C’est ainsi qu’il est protégé contre le multithread.</p><p>Voici un exemple pour l’interface <code>progress_t</code> :</p><pre><code> // Wrap a non-thread-safe progress in a multi-thread-safe progress. // // The progress can only be reported by a per-thread-progress referencing // this multi-thread progress. struct multi_thread_progress_t { // Wrap a non-threas safe progress. multi_thread_progress_t() = default; multi_thread_progress_t(progress_t& a_non_thread_safe_progress) : my_non_thread_safe_progress(&a_non_thread_safe_progress), my_report_every(a_non_thread_safe_progress.my_report_every) {} // Report the final progress tally when destroyed. ~multi_thread_progress_t(); // Force to report the progress tally. void flush_progress() { report_to_non_thread_safe_progress(my_total_count_so_far); } // Clear the progress. void clear_progress() { my_total_count_so_far = 0; } protected: // Receive progress from a per-thread progress. (see below) void update_progress_from_thread(size_t a_count_from_thread); // Propagate the progress to the non-thread-safe progress. void report_to_non_thread_safe_progress(size_t a_count); private: progress_t* my_non_thread_safe_progress = nullptr; size_t my_report_every = 100 * 1000; std::atomic<size_t> my_total_count_so_far = 0; std::mutex my_mutex; friend struct per_thread_progress_t; };</size_t></code></pre><p>Les fonctions importantes sont : <code>update_progress_from_thread()</code> et <code>report_to_non_thread_safe_progress()</code>. La première reçoit les progrès de chaque thread, dont le code sera présenté plus tard. La fonction accumule le total dans une variable multithread-safe et ne l’envoie que lorsque ce total franchit un seuil donné. La deuxième fonction envoie les progrès réalisés à la version originale de l’interface, sous la protection d’un mutex. Voici le code pour les deux fonctions :</p><pre><code> void multi_thread_progress_t::update_progress_from_thread(size_t a_count_from_thread) { if (!my_non_thread_safe_progress) return; const size_t pre_count = my_total_count_so_far.fetch_add(a_count_from_thread); const size_t post_count = pre_count + a_count_from_thread; if ((pre_count / my_report_every) != (post_count / my_report_every)) { report_to_non_thread_safe_progress(post_count); } } void multi_thread_progress_t::report_to_non_thread_safe_progress(size_t a_count) { std::lock_guard lock(my_mutex); my_non_thread_safe_progress->update_progress(a_count); }</code></pre><h3>Code pour chaque thread</h3><p>La dernière partie du modèle est le code de l’interface originale pour chaque thread. Dans ce cas, nous voulons dériver de l’interface. Cela permettra de remplacer la version originale de l’interface qui ne supportait pas le multithreading ! Ce code par thread est destiné à être utilisé par un seul thread. La protection multithread se fait dans le protecteur multithread que nous avons montré auparavant.</p><p>Cette division du travail entre le protecteur et les parties par thread simplifie grandement le raisonnement sur le code et simplifie le code lui-même.</p><p>Voici le code de <code>progress_t</code> par thread de notre exemple :</p><pre><code> // Report the progress of work from one thread to a multi-thread progress. // // Create one instance in each thread. It caches the thread progress and // only report from time to time to the multi-thread progress to avoid // accessing the shared atomic variable too often. struct per_thread_progress_t : progress_t { // Create a per-thread progress that report to the given multi-thread progress. per_thread_progress_t() = default; per_thread_progress_t(multi_thread_progress_t& a_mt_progress) : progress_t(a_mt_progress.my_report_every / 10), my_mt_progress(&a_mt_progress) {} per_thread_progress_t(const per_thread_progress_t& an_other) : progress_t(an_other), my_mt_progress(an_other.my_mt_progress) { clear_progress(); } per_thread_progress_t& operator=(const per_thread_progress_t& an_other) { progress_t::operator=(an_other); // Avoid copying the per-thread progress accumulated. clear_progress(); return *this; } // Report the final progress tally when destroyed. ~per_thread_progress_t(); protected: // Propagate the progress to the multi-thread progress. void update_progress(size_t a_total_count_so_far) override { if (!my_mt_progress) return; my_mt_progress->update_progress_from_thread(a_total_count_so_far); clear_progress(); } private: multi_thread_progress_t* my_mt_progress = nullptr; };</code></pre><h2>Conclusion</h2><p>J’ai utilisé ce design pour résoudre plusieurs fois des problèmes multithread. Cela m’a toujours été utile. N’hésitez pas à réutiliser ce design là où vous en avez besoin !</p><p>L’exemple particulier utilisé ici se trouve dans la bibliothèque “utility” du projet <i>Tantrix Solver</i> <a href="https://github.com/pierrebai/Tantrix">disponible sur GitHub</a>.</p>
<p>Je suis développeur back-end Python depuis plus de 7 ans. La majorité de mon expérience professionnelle s’est faite avec le cadre de développement web Flask qui m’a toujours satisfait. Autant que je sache, les deux options concurrentes en matière de frameworks web en Python sont <a href="https://www.djangoproject.com/">Django</a> et <a href="https://flask.palletsprojects.com/en/1.1.x/">Flask</a> (à dire ça, je vais sans doute recevoir des messages hargneux de la part des communautés Pyramid, Bottle et autres !). Dans ma tête, j’ai toujours pensé que Flask était objectivement supérieur, car il permet une plus grande liberté de choix, est plus récent et est moins surchargé. J’avais l’idée erronée que les gens choisissaient Django simplement à cause de sa popularité. Puis, après avoir débuté dans un nouvel emploi chez <a href="https://www.spiria.com/">Spiria</a>, on m’a confié quelques projets qui avaient été développés avec Django. L’expérience m’a ouvert les yeux, et je me suis mis de façon inattendue à apprécier ce framework. Ci-après, je vais présenter mon évaluation de Django, du point de vue d’un développeur Flask aguerri. Vous lisez peut-être cet article en vous posant la question “Quel framework dois-je utiliser ?”. Comme souvent, la réponse à cette question est… cela dépend !</p><h3>Structure de projet</h3><p>Le framework Django impose une structure de projet spécifique. Vos modèles, vues et routages sont tous placés dans des endroits prévus. Les projets Django dont j’ai hérité à Spiria ont tous été initialement créés par des développeurs Rails et, bien que la base de code ait été parsemée de particularismes, je me suis quand même senti chez moi en tant que développeur Python.</p><p>Je pense que si les développeurs avaient commencé un projet avec Flask, j’aurais eu beaucoup de difficultés. Je me souviens d’une conversation que j’ai eue avec un développeur Django il y a quelques années, au sujet de l’expérience de son équipe avec un projet Flask. Ils n’arrivaient pas comprendre comment quelqu’un peut arriver à réaliser un projet d’envergure avec Flask. Après avoir approfondi la question, il m’est apparu clairement que leur application Flask contenait absolument tout le code dans un seul fichier. Dix mille lignes dans toute leur gloire. Pas étonnant qu’ils aient trouvé ça impossible à maintenir ! Flask n’impose aucune structure inhérente, ce qui peut être une arme à double tranchant. Au mieux, vous adoptez et appliquez une structure stricte dès le début de votre projet. Au pire, aucune structure n’est imposée, et vous héritez d’une très grosse application à fichier unique, ce qui donne un tout nouveau sens au concept d’application web monopage ! Cette liberté veut dire que chaque application Flask que vous rencontrerez sera probablement structurée de manière différente.</p><h3>L’ORM de Django face à SQLAlchemy</h3><p>Ceux d’entre vous qui sont des habitués de Flask auront très probablement utilisé <a href="https://www.sqlalchemy.org/">SQLAlchemy</a> pour leurs besoins en matière d’ORM (mapping objet-relationnel). SQLAlchemy est un cadre extrêmement puissant qui vous donne tout le contrôle dont vous avez besoin sur votre base de données. L’un des avantages de SQLAlchemy est que vous pouvez vous approcher du bas niveau dans les domaines où vous avez vraiment besoin d’un contrôle très fin (par exemple, dans les domaines à faible performance) en allant directement dans le <a href="https://docs.sqlalchemy.org/en/13/core/">noyau SQLAlchemy</a> et en écrivant des requêtes dans le langage d’expressions SQL. Toutefois, cette flexibilité a un coût ; comme on dit, un grand pouvoir s’accompagne de grandes responsabilités. Un peu plus bas, je vous parlerai de certaines fonctionnalités vraiment cool de l’ORM de Django qui sont probablement beaucoup plus difficiles à atteindre avec SQLAlchemy en raison de cette flexibilité.</p><p>En regard, l’ORM de Django est beaucoup plus proche de Rail, probablement parce que l’ORM de Django utilise une implémentation d’enregistrement actif (“active record”). Cela signifie essentiellement que les objets de votre modèle Django ressembleront beaucoup aux lignes SQL dont ils sont dérivés. Cela présente des avantages aussi bien que des inconvénients : du côté positif, la simplicité et l’élégance du système de migration de Django (voir plus bas), sans avoir à gérer la session de base de données ; du côté négatif, le fait de ne pas pouvoir déclarer automatiquement les jointures sur les relations directement sur le modèle lui-même. Dans SQLAlchemy, cela se fait facilement grâce aux techniques de chargement des relations (“Relationship Loading Techniques”) de SQLAlchemy (lazy loading, eager loading, etc.). Pour éviter le <a href="https://thenewstack.io/finding-and-fixing-django-n1-problems/">problème du N+1</a>, vous devrez appeler <code>select_related()</code> ou <code>prefetch_related()</code> partout où vous faites une requête pour ce modèle. Cela peut devenir fastidieux, surtout lorsque vous avez deux modèles qui sont presque toujours utilisés ensemble.</p><pre><code>‘‘‘Example of eagerloading taken from the SQLAlchemy docs’’’class Parent(Base): __tablename__ = ‘parent’ id = Column(Integer, primary_key=True) children = relationship("Child", lazy=‘joined’)</code></pre><p>Comme vous pouvez voir dans l'exemple ci-dessus, vous pouvez faire vos jointures directement sur les classes modèles de votre base de données, au lieu d'ajouter la jointure à la requête chaque fois que vous voulez optimiser votre requête.</p><pre><code>parents_and_children = Parent.objects.select_related(‘children’).all()</code></pre><h3>Django Migrations face à Alembic</h3><p>La migration des bases de données est un domaine où Django surpasse vraiment Flask — le système de migration de base de données intégré à Django est un pur plaisir à utiliser. J’ai été particulièrement impressionné lorsqu’un coéquipier et moi-même avons simultanément engagé des migrations de bases de données dans le projet : Django a immédiatement identifié le problème, et il a été simple de lancer une fusion par le biais de la commande Python <code>manage.py makemigrations --merge</code>. Techniquement, Alembic dispose de la <a href="https://alembic.sqlalchemy.org/en/latest/branches.html">fonction branches</a> pour traiter ce problème, mais elle est en “bêta” depuis des années, ce qui n’inspire pas confiance pour une utilisation en production.</p><h3>Django Admin face à Flask-Admin</h3><p>Django Admin est un avantage majeur de Django. Dès la première utilisation, vous obtenez une interface web <a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD</a> instantanée pour tous vos modèles de base de données, protégée derrière un écran de connexion utilisateur. Le panneau d’administration de Django est très performant, et un certain nombre de bibliothèques tierces facilitent la vie avec des fonctionnalités supplémentaires.</p><p>Une bibliothèque tierce pour Flask, la bien nommée <a href="https://flask-admin.readthedocs.io/en/latest/">Flask-Admin</a>, occupe la même place que Django Admin. En phase avec le reste, Flask-Admin est livré avec beaucoup moins de choses préconfigurées pour vous. Vous devrez inclure vous-même un “boilerplate” pour chaque modèle de base de données que vous souhaitez ajouter au panneau Flask-Admin.</p><p>Un bémol autant pour Django Admin que Flask-Admin : bien qu’ils peuvent vous faire gagner beaucoup de temps au départ, à mesure que votre projet mûrit et que vos utilisateurs commencent à demander (ou exiger) des fonctionnalités supplémentaires dans vos panneaux d’administration, vous vous retrouverez à vous battre avec ces bibliothèques pour obtenir ce que vous voulez. J’ai trouvé que ces bibliothèques d’admin étaient mieux adaptées à l’usage des développeurs uniquement. Si vous avez l'intention d'ouvrir des fonctionnalités d'administration à votre base d'utilisateurs, il est préférable d'en écrire une admin à partir de zéro, en utilisant un template côté serveur ou une API avec un framework front-end, comme <a href="https://reactjs.org/">React</a>.</p><h3>Framework Django REST</h3><p>En parlant d’API, j’ai trouvé que travailler avec le framework <a href="https://www.django-rest-framework.org/">Django REST framework</a> était un rêve absolu. Les vues génériques basées sur les classes rendent la vie pas mal plus simple et la base de code bien plus légère. En gros, vous avez vos vues, vos sérialiseurs et vos permissions. “Subclassez” la vue générique appropriée (par exemple, une <a href="https://www.django-rest-framework.org/api-guide/generic-views/#listcreateapiview">ListCreateAPIView</a>), attachez une requête par défaut, un sérialiseur (quelque chose qui définit votre requête/réponse aux endpoints et gère votre conversion entre JSON et Python) et toutes les autorisations qui sont pertinentes pour cette vue. Et c’est tout.</p><pre><code>‘‘‘Example ListCreateAPIView class’’’class BlogPostListCreate(ListCreateAPIView): permission_classes = [IsAuthenticated, BlogPostPermission] serializer_class = BlogPostSerializer queryset = BlogPost.objects.all()</code></pre><p>En revanche, contruire ça sans Django REST ressemblerait davantage à ceci :</p><pre><code>from django.http import JsonResponsefrom django.views import Viewclass BlogPostListCreateView(View): def get(self, request): blog_posts = BlogPost.objects.all() blog_post_dicts = [] for blog_post in blog_posts: blog_post_dicts.append({ ‘id’: blog_post.id, ‘title’: blog_post.title, ‘body’: blog_post.body }) return JsonResponse({‘blog_post_dicts’: blog_post_dicts}) def post(self, request): new_blog_post = BlogPost.objects.create(title=request.POST[‘title’], body=request.POST[‘body’]) return JsonResponse({‘message’: ‘Blog Post Created’})</code></pre><p>L’exemple ci-dessus n’inclut pas la vérification des autorisations, la validation ou la pagination, ce que vous aurez en bonus avec le framework Django REST.</p><p>Un atout supplémentaire est toutes les bibliothèques additionnelles qui ont été développées autour de Django REST et qui rendent votre vie de développeur tellement plus facile. Deux bibliothèques remarquables que j’ai utilisées sont <a href="https://pypi.org/project/django-rest-framework-camel-case/">Django-Rest-Framework-Camel-Case</a> et <a href="https://drf-yasg.readthedocs.io/en/stable/">drf-yasg</a>. La première garantit que vos API endpoints peuvent accepter à la fois les clés en CamelCase et celles en snake_case pour la demande et utilisent le CamelCase pour la réponse, tout en conservant les noms de variables en snake_case dans tout votre code Python. Cela permet à vos développeurs front-end d'utiliser les conventions de nommage auxquelles ils sont habitués, et à vos développeurs back-end Python d'utiliser le snake_case et de se conformer à <a href="https://www.Python.org/dev/peps/pep-0008/">PEP8</a>. La seconde bibliothèque, drf-yasg, fonctionne bien avec la première et aide à générer automatiquement la documentation <a href="https://swagger.io/">swagger</a> en analysant vos vues génériques et leurs sérialiseurs. Rien ne vaut une documentation générée automatiquement !</p><p>Pour être juste à l’égard de Flask, un certain nombre de bibliothèques tierces font probablement beaucoup de ce que Django REST apporte à Django. Cependant, pour une raison ou une autre, je n’ai jamais eu recours à l’un de ces outils pendant mes développements dans Flask, ce qui mériterait en soi une enquête approfondie.</p><h3>Conclusion</h3><p>Je suis bien heureux d’avoir eu l’occasion d’essayer Django dans un cadre professionnel. Avant de rejoindre Spiria, j’étais un développeur Flask acharné et je ne voyais pas l’autre côté de la médaille de ce framework. Flask est un outil étonnant, et la liberté qu’il offre permet de faire beaucoup de choses. Cependant, cette liberté exige beaucoup de discipline, et une mauvaise décision au stade de l’architecture peut vous coûter cher à mesure que votre projet mûrit. Passer à Django m’a ouvert les yeux, me révélant de nombreuses façons de faciliter ma vie dont je ne soupçonnais même pas la possibilité en travaillant avec Flask. Bien sûr, il y a quelques éléments de Flask qui m’ont manqué (les charges jointes automatiques, par exemple), mais, pour les projets sur lesquels j’ai travaillé jusqu’à présent, j’ai vraiment eu le sentiment que Django était le bon choix.</p><p>Ma recommandation serait de faire appel à Flask pour la réalisation d’un prototype rapide, ou pour la production de quelque chose de tellement spécifique que les rails imposés par Django seraient trop contraignants. D’un autre côté, il faut faire appel à Django pour les projets qui peuvent bénéficier d’une telle structure, surtout lorsqu’on travaille avec une grosse équipe. Il y a certainement une place pour Flask et Django dans le monde, et la communauté Python dans son ensemble est bien nantie d’avoir de tels choix. Merci à tous ceux qui ont contribué aux deux projets et à tous les projets satellites open-source qui contribuent à faire de ces deux frameworks un rêve avec lequel travailler.</p>