+ 33 6 68 40 27 75 contact@odeven.fr

Comprendre les Concepts Objets – Qualité de Code

par | Oct 7, 2022 | Concepts Objets

Sommaire

Programmation Orientée Objet, Design/Architectural Patterns, … Vous en avez déjà entendu parler. Mais savez vous précisément ce en quoi ça consiste ?

Ces 8 articles vous permettront de comprendre les grands principes qui se cachent derrière ces termes techniques, mais également d’apprendre comment et pourquoi les utiliser.

Introduction

Le développement d’applications basiques, non destinées à évoluer, telles que des sites vitrines, ou une petite application n’excédant pas quelques jours de développement, ne permet généralement pas de justifier le temps passé à améliorer le code. En effet, si l’application fonctionne, on ne souhaite généralement pas passer plus de temps que nécessaire pour améliorer la qualité du code. Pourtant, si l’application est destinées à évoluer, ou nécessite l’intervention de développeurs multiples, il devient alors nécessaire de définir un ensemble de règles pour garantir une maintenabilité aisée de l’application.

Bonnes pratiques

En tant que développeurs, nous avons tous été confrontés à une des problématiques suivantes :

  • Retour chariot avant l’accolade ou non ?
  • Espaces ou tabulations ?
  • Variables nommées en camelCase ou underscore_case ou autre ?
  • Etc…

Vous avez peut-être même déjà argumenté pour défendre votre choix, et même si vous êtes persuadé d’avoir raison, vous savez au fond de vous que chacun de ces choix est argumentable. Au final, ce qui vous dirige vers un choix plutôt qu’un autre, c’est votre opinion.

Pourtant, il existe toujours une solution meilleure que les autres. Il s’agit de celle employée par la convention en vigueur. Si la convention d’un langage exige l’utilisation d’une règle en particulier, c’est elle qui prime. Si votre entreprise impose d’autres conventions, alors ce sont ces dernières qui passent en priorité.

Pourquoi ? Tout simplement parce que ces solutions offrent un avantage non négligeable, qui au final est le plus important : c’est de définir une convention générale, sur laquelle chaque développeur peut se baser indépendamment de son opinion. Il s’agit du meilleur moyen permettant de garantir un code unifié, réalisé comme si l’équipe avait travaillé comme un seul développeur. L’utilisation des conventions du langage pour une entreprise est très avantageuse, car permet d’embaucher des développeurs déjà habitués à ces conventions.

Il en va de même pour l’utilisation de Frameworks, ou de n’importe quelle technologie en général.

Dans un même soucis d’uniformisation, il est judicieux d’employer l’Anglais, langue internationale, pour nommer vos variables/fonctions/etc… En effet, les langages de programmation emploient généralement une syntaxe anglaise. Il en va de même pour les Frameworks. Or, l’utilisation du français mélangé à ces technologies vient casser cette uniformité (comment nommer mon getter ? getPrenom ? obtenirPrenom ?). De plus, les caractères spéciaux comme les accents en français, sont rarement compatibles. Concernant les commentaires, ils peuvent bien évidemment être rédigés dans la langue locale, bien qu’il serait mieux d’utiliser l’anglais si le contexte le permet (développeurs compétents dans cette langue). En effet, l’anglais bénéficie d’une syntaxe et de mots clés souvent simples et concis, représentant précisément une idée, sans avoir à ajouter de superflu. Dans le domaine de l’open-source, ce choix est indispensable, car le code est rarement adressé à un seul pays.

Documentation

Il est évident qu’un algorithme de plusieurs centaines de lignes nécessite d’écrire des commentaires. Pour les autres développeurs, mais aussi pour vous, si vous revenez sur une fonctionnalité plusieurs mois après l’avoir développée. Dans ce cas, pas sûr que vous vous souveniez de la logique que vous aviez employée.

Mais j’aimerais surtout parler de la documentation générale de votre code, qu’on appel généralement, suivant le langage “JavaDoc”, “PhpDoc”, etc…

Il s’agit tout simplement de blocs de commentaires, situés au dessus de chaque déclaration (classe, attribut, méthode, …). La syntaxe dépend du langage que vous utilisez.

Avantages

Cette documentation offre plusieurs avantages :

  • Auto-complétion offerte par l’IDE (surtout pour les langages faiblement typés, ex: PHP, JS, …)
  • Possible génération automatique de la documentation (Site Web, PDFs, etc…)

Fonctionnement

Vous pouvez documenter plusieurs choses :

La classe

Il s’agit principalement de définir la responsabilité de la classe.

Les attributs

Il s’agit de définir le type de l’attribut, éventuellement suivi d’un commentaire le définissant

Les méthodes

Ces dernières, un peu plus complexes, demandent de renseigner le type de chaque paramètre (avec un commentaire optionnel), mais aussi le type de retour (avec un commentaire optionnel), et les Exceptions pouvant être levées par la méthode.

Dans chaque bloc, vous pouvez renseigner les paramètres, la valeur retournée, les exceptions pouvant être levées, etc…

Exemple en PHP :

/**
* Provides an easy way to compute operations
* @package Foo\Bar
*/
class Calculator
{
   /**
    * @var Math
    */
   public $math;

   /**
    * @param int|float $a
    * @param int|float $b
    *
    * @return int|float
    */
   public function sum($a, $b)
   {
       return $a + $b;
   }
}

A noter : avec les versions 7/8, il est désormais possible de typer les arguments/valeurs de retour. Il est donc recommandé d’utiliser le typage lorsque cela est possible, plutôt que la PHPDoc.

Factorisation

Inutile de rappeler à quel point il est important de factoriser votre code afin de coder chaque fonctionnalité de manière unitaire. Et pourtant, nous allons voir une autre excellente raison de pousser l’atomisation de votre code au maximum. Il s’agit des tests. Mais avant, rappelons rapidement les avantages de la factorisation :

  • Meilleure maintenabilité
  • Meilleure évolutivité
  • Gain de temps (sur le moyen et long terme), et parfois aussi sur le court terme
  • Limite la complexité de chaque opération
  • Meilleure compréhension du code

Tests

Imaginez la situation suivante :

Vous avez, vous ainsi que de nombreux autres développeurs, créé une application complexe, aux fonctionnalités multiples, probablement sujette à des évolutions, et certainement à de la maintenance. 

Cette application est formidable, et pourtant vous devez ajouter une nouvelle fonctionnalité, ou bien modifier une fonctionnalité déjà existante. Bref, vous devez modifier le code. Vous tapez vos quelques lignes de codes, vos classes, etc… et vous testez. Tout à l’air en ordre, vous passez à autre chose, car vous vous dites que ce n’est pas la peine de tester l’intégralité de l’application, car ça prendrait trop de temps.

Alors vous mettez la mise à jour de votre application en production. Et là, un client/utilisateur vous dit “La page X ne fonctionne plus”.

Vous regardez donc le code de la fonctionnalité concernée, et vous vous rendez compte que vos modifications ont eut un impact indirectement. Et vous vous demandez si d’autres fonctionnalités n’ont pas été impactées. De même, en corrigeant l’erreur, vous vous demandez si autre chose ne sera pas impacté par cette modification (est-ce que j’ai bien pensé à tout ?).

Bref, vous l’avez compris, les tests c’est importants, et les développeurs ont horreur de ça, en plus de ne pas avoir le temps de tester une application dans son intégralité à chaque modification.

Heureusement, quelques développeurs ont découvert qu’il était possible d’automatiser ces tests. Imaginez avoir un bouton “Tester” qui vous dirait si toute votre application fonctionne, et si non, quelles sont les parties qui ne fonctionnent plus et pourquoi. Bonne nouvelle : c’est possible !

En développement, il existe de nombreux types de tests, les deux plus classiques étant les tests unitaires, et les tests fonctionnels.

Tests Unitaires

Le but de ces tests est de tester de manière unitaire, atomique, au niveau le plus fin, une fonction/méthode. 

Imaginez que je vous demande de développer une classe “Calculator” avec une méthode “sum”. Cette méthode devra retourner la somme de deux nombres, être capable de retourner une Exception si les deux nombres passés en paramètre ne sont pas de type numérique, et gérer des nombres positifs, négatifs, et décimaux.

Toutes ces règles que je viens d’énoncer, constituent le cadre des tests unitaires à effectuer sur cette méthode. Et pour les mettre en place, il suffira de développer une fonction de test pour chaque cas.

Comment cela fonctionne en réalité ? Pour savoir si la fonction marche avec deux entiers positifs, il suffit de faire appel à la méthode “sum”, de lui passer deux nombres positifs, et de vérifier si le nombre retourné est bien égal à la somme des deux nombres.

Exemple :

public function testSumWithPositiveIntegers()
{
   $calculator = new Calculator();
   $result = $calculator->sum(5, 7);
   $this->assertEquals(12, $result);
}

Dans cette méthode, on instancie un objet “Calculator”, puis nous faisons appel à sa méthode “sum” en lui passant deux nombres en dur. Le résultat attendu, nous le connaissons, car la somme de 5 et 7 est 12. La dernière ligne est un peu particulière, il s’agit d’une méthode mise à disposition par le framework permettant de comparer le résultat attendu, et le résultat réellement retourné. Si les deux sont égaux, le framework considère que le test fonctionne, sinon, il enregistrera la fonction comme n’ayant pas fonctionnée.

Bien sûr, “assertEquals” n’est pas la seule méthode disponible, il est aussi possible de faire un test “supérieur”, “inférieur”, sur des collections, de vérifier qu’une exception est bien levée etc…

Frameworks

Suivant le langage que vous utilisez, vous êtes à peu près sûr de trouver le framework de test unitaire qui va avec. Généralement, il s’appel [nom_du_langage]Unit. Exemple :

  • JUnit
  • PHPUnit
  • CsUnit
  • etc…

Ce qui est génial, c’est qu’ils fonctionnent à peu près de la même façon, donc pas besoin de tous les apprendre, la logique reste la même !

Mise en place

Avant de commencer, vous devrez installer le framework de test unitaire correspondant à votre langage. Vous pouvez vous référer à la documentation correspondante, ce n’est généralement pas très compliqué à installer.

Une fois installé, je vous conseil de créer un dossier “Tests” à la racine de votre projet (ou du moins de vos sources). Pour chaque classe que vous souhaitez tester (donc toutes n’est-ce pas  ? :D), vous créerez une autre classe portant le même nom, en ajoutant le suffixe “Test”, avec les sous-dossiers (namespace/package) qui correspondent. Exemple :

  • Util/
    • Math/
      • Calculator
    • Business/
      • UserBusiness
  • Tests/Util/
    • Math/
      • CalculatorTest
    • Business
      • UserBusinessTest

Cette architecture permet de séparer vos tests de votre application. Bien sûr, à vous d’adapter suivant l’architecture que vous avez définie.

Ensuite, à vous de développer une méthode de test au moins pour chaque méthode de la classe à tester. N’hésitez pas à en créer un maximum, correspondant à tous les cas pouvant survenir et provoquer un comportement différent. De même, nommez chaque méthode d’une manière explicite (préfixée par “test”), même si le nom de la méthode fait au final 200 caractères.

Méthodes spéciales

Vous pouvez créer des méthodes pouvant être appelées avant ou après chaque test, ou bien avant le premier test de la classe, ou après le dernier test de la classe. Vous pouvez vous servir de ces méthodes pour instancier des objets par exemple, ou pour charger des données…

Ces méthodes s’appellent généralement :

  • setUpBeforeClass
  • tearDownBeforeClass
  • setUp
  • tearDown

La simple surcharge de ces méthodes suffit généralement, bien que nécessite parfois l’utilisation d’annotations suivant le langage. A vous de vous référez à la doc !

Exemple :

class CalculatorTest extends TestCase
{
   private Calculator $calculator;
  
   public function setUp()
   {
       $this->calculator = new Calculator();
   }

   public function testSumWithPositiveIntegers()
   {
       $result = $this->calculator->sum(5, 7);
       $this->assertEquals(12, $result);
   }
}

Data Providers

Un Data Provider est une méthode qui retourne des données. Il s’agit des paramètres devant être passés à la méthode pour la tester, ainsi que le résultat attendu. Au lieu de renseigner les paramètres et le résultat attendu en dur dans le test, on peut lui demander de l’effectuer pour chaque jeu de données.

Dans l’exemple de la méthode “sum”, si on veut tester des entiers positifs, des négatifs, et des nombres décimaux, on va pouvoir fournir toutes ces données grâce à un Data Provider. Ainsi, la méthode “sum” pourra être exécutée pour chaque cas, sans avoir à recréer plusieurs tests similaires.

Exemple :

class CalculatorTest extends KernelTestCase
{
   /**
    * @dataProvider sumProvider
    */
   public function testAdd($a, $b, $expected)
   {
       $result = $this->calculator->sum($a, $b);
       $this->assertEquals($expected, $result);
   }

   public function sumProvider(): array
   {
       return [
           [7, 5, 12],
           [5, -5, 0],
           [-2, -2, -4],
           [0.8, 2.5, 3.3]
       ];
   }
}

Dans cet exemple, il suffit de renseigner l’annotation “@dataProvider” pour dire au framework d’exécuter la méthode avec chaque set de données fournit par la méthode “sumProvider”.

Les Mocks

Et les dépendances dans tout ça ? Si jamais une classe ne fonctionne plus correctement, et que toutes les autres classes de l’application dépendent de celle-ci, alors plus aucun test ne fonctionnera ! Comment savoir dans ce cas d’où vient l’erreur si tous les tests s’affichent en rouge ? Mock à la rescousse !

Les Mocks sont des remplaçants. Leur but est de remplacer un objet, en lui injectant un comportement bien défini, et limité au strict nécessaire.

Si la classe A que je souhaite tester dépend de la classe B, car fait appel à une méthode “foo” de cette dernière, alors je dois remplacer la classe B. Dans cet exemple, on va créer une classe (pas besoin de créer de fichier pour ça, rassurez-vous), définir une méthode “foo”, et le résultat qu’elle retournera.

Les Mocks sont en fait très simples, mais très puissants. Ils permettent de rendre toutes nos classes indépendantes dans le contexte des Tests Unitaires.

Exemple :

public function testAdd()
{
    // Define a Mock for the class Math with a virtual method "compute"
    $math = $this->getMockBuilder(Math::class)
                 ->setMethods(['compute'])
                 ->getMock()
    ;

    // Math's method "compute"
    // should be called once
 
    // with 5 and 7 as parameters
    // and will return 12
    $math->expects($this->once())
         ->method('compute')
         ->with($this->equalTo(5), $this->equalTo(7)
)
         ->willReturn(12)
    ;

    $calculator = new Calculator($math);
    $result = $calculator->sum(7, 5);
    $this->assertEquals(12, $result);
}

Dans cette méthode, le test ne fera pas appel à la vraie classe “Math”, mais à un Mock qui retournera instantanément 12 lorsqu’on fera appel à sa méthode “compute” avec 7 et 5 en paramètre. De cette manière, pas besoin que la classe Math fonctionne pour tester “Calculator”.

On peut même aller beaucoup plus loin en configurant chaque méthode, le nombre de fois qu’elles sont sensées être appelées, et plus encore.

Les Fixtures

On nomme “fixtures”, les données nécessaires pour effectuer des tests. Vous vous doutez bien qu’on ne va pas tester la suppression d’un utilisateur avec la base de données de production. Les fixtures, sont là pour définir un contexte, c’est à dire un état de la base de données qui pourra être rechargé avant d’effectuer un test.

Imaginez devoir développer 5 tests, chacun nécessitant plusieurs lignes dans la base de données. Grâce aux fixtures, on pourra restaurer la base de données (en mémoire par exemple) avant chaque test.

Code Coverage

Ces Frameworks de Tests Unitaires permettent aussi de voir le code couvert par les tests. Pour une classe testée, on peut ainsi obtenir le pourcentage de code qui a été “parcouru” par des tests. De plus, il est possible de visualiser directement sur le fichier les lignes concernées.

Cette fonctionnalité est très pratique, et peu permettre de se rendre compte, par exemple, si une parcelle de code (dans un if par exemple), n’a pas été testée. Cela aide ainsi à déterminer si le nombre de test semble suffisant. En général, on s’accord à dire qu’il faut environ 80% de couverture pour une bonne application.

Tests Fonctionnels

Ces tests n’ont pas pour but de tester l’application au niveau le plus fin, mais plutôt en général. En effet, tout ce qu’on test avec les Tests Unitaires, ce sont les classes et leurs méthodes. Mais si on souhaite aller encore plus loin, et s’assurer que la page X affiche bien le nom de l’utilisateur dans le cadre en haut à droite par exemple, il faut faire des tests fonctionnels.

Contrairement aux Tests Unitaires, l’utilisation de tests fonctionnels dépend des technologies que vous utilisez, et non juste du langage. Si vous développez un site web, le fonctionnement sera différent d’un logiciel. C’est pourquoi nous n’aborderons ici que la théorie. A vous de vous référer à votre technologie préférée pour voir la pratique 😉

En général, on développe moins de tests fonctionnels que de tests unitaires, car la limite est difficile à cerner. Tout dépend de votre application, et du temps que vous pouvez y consacrer.

Avec les Tests Fonctionnels, vous pouvez utiliser le même raisonnement que les Tests Unitaires, et vérifier qu’une valeur correspond bien au résultat attendu. Pour ces tests, on se positionne non plus comme un développeur (Tests Unitaires), mais comme un simple utilisateur. En effet, la différence entre un développeur et un utilisateur, c’est que le premier vérifie généralement que le code qui est derrière fonctionne bien, alors que le second regarde ce qui est affiché à l’écran.

A noter qu’il n’y a pas forcément d’interface utilisateur à tester. Un test fonctionnel peut donc correspondre à l’enchaînement de plusieurs actions consécutives, et vérifier chaque étape, ou bien un état « final » (ex: « vérifier si l’utilisateur a bien été supprimé en base »).

Test Driven Development

Le Test Driven Development, ou TDD, est une technique de développement, généralement employée dans les méthodes agiles, qui consiste à écrire les tests, avant de développer la fonctionnalité.

Cette technique permet d’avoir une approche basée sur une analyse poussées, dans laquelle on aura bien réfléchi aux divers cas d’utilisations possibles. Une fois les tests écrit, on sera sûr de ne rien manquer.

Méthodes privées et autres cas particuliers

Il existe une infinité de cas particuliers. Par exemple, comment faire pour tester unitairement une méthode privée ? La solution de ces cas particuliers dépend généralement du framework que vous utilisez. N’hésitez pas à consulter la doc !

Pour résumer

Bonnes Pratiques : Il faut toujours s’y référer, car définissent un standard.

Documentation : Permet à d’autres développeurs, ou à vous-même, de comprendre le code de l’application plus facilement.

Factorisation : De nombreux avantages en plus de pouvoir faire des Tests Unitaires

Tests Unitaires : Du code qui permet de tester un petit morceau de code

Tests Fonctionnels : Permet de tester des cas d’utilisation plus poussés

Test Driven Development : Écrire les tests avant de développer la fonctionnalité