jeudi 16 avril 2015

BDD (Behavior Driven Development) en pratique !

Une des problématiques de développement logiciel est le manque de communication entre les différents participants. Pire, dans la plupart des cas, ils emploient des vocabulaires différents pour parler de la même chose. En effet, généralement dans les projets de développement, les experts du métier rédigent des spécifications qui sont ensuite transmises à l’équipe de développement. Les développeurs relisent les spécifications, comprennent et interprètent à leur façon avant de commencer à coder selon leur compréhension. L’équipe de développement pourrait très bien mal interpréter certains concepts spécifiés. Lorsqu'on interroge un expert métier sur une notion présente dans le code du domaine, et qu'on s'aperçoit qu'elle est en total décalage avec la réalité, même si l'application semble se comporter correctement pour l'utilisateur. Il peut s'agir d'une mauvaise interprétation ou d'une supposition hasardeuse de la part d'un développeur. Le BBD nous aide à remédier à cette problématique d’interprétation des termes, des concepts liés au domaine fonctionnel.

BDD (Behavior Driven Development) : c’est une pratique de développement qui a pour objectif de mettre en place un vocabulaire, un langage commun et généralisé pour décrire ce que doit faire notre application. Cela se fait par une collaboration étroite entre les experts du domaine fonctionnel/ métiers, concepteurs et développeurs pour construire du code qui reflète la réalité du domaine fonctionnel et permet de résoudre de vraies problématiques métier. Il s’agit d’écrire des tests qui décrivent le comportement attendu du système et que tout le monde peux comprendre. Le code n’est donc plus compréhensible uniquement par des développeurs, mais sous forme de scénario compréhensible par toutes les personnes impliquées dans le projet.
L’objectif visé est de rendre le code à l’image du domaine fonctionnel, clair et compréhensible, et donner tout de suite une vision d'ensemble à celui qui y jette un œil. Malheureusement ce n'est pas toujours le cas, souvent à cause d'une représentation brouillonne du domaine dans le code due à un manque de dialogue avec les experts métier. De plus il est facile de se perdre en créant toujours plus de helpers, de services et autres gestionnaires qui embrouillent la compréhension du domaine.

C'est un peu abstrait tout ça !

Par où commencer ??? ===> Création de Story

Nous avons vu précédemment que le comportement de l’application doit être défini en collaboration étroite avec les experts métier. Cette collaboration peut se matérialiser dans un premier temps par une demande en terme des besoins (une story/histoire) de l’expert métier. Chaque demande est priorisée. Comme le travail d’un expert du domaine fonctionnel n’est pas d’écrire des tests unitaires, il faudra trouver un langage simple avec un nombre de vocabulaire limité, compréhensible par tous et qui pourra être utilisé à la fois par les expert pour exprimer leur besoins fonctionnels (une story/histoire) et par les développeurs pour effectuer leur tests.
BDD utilise le langage Gherkin, un formalisme ultra simple qui permet de modéliser une story. Gherkin est un langage "Spécifique à un Domaine fonctionnel, et lisible par un Fonctionnel" et a été imaginé spécialement pour la description de comportements. Il vous offre la possibilité d'abstraire les détails d'implémentation de vos tests de comportement. L’expert métier commence par donner une description de ce qui est attendu par la story puis décrit en « détail » en utilisant au plus 3 vocabulaires du langage Gherkin : En tant que (As role), je veux (I want), Pour/A fin (In order to) :





La description d’une Story met d’abord en place un contexte (En tant que …). Puis va nous demander une fonctionnalité (Je veux, souhaite …). Et enfin nous explique quelle est la finalité de cette Story (Afin, Pour …)
Exemples :
-           En tant qu'étudiant, Je veux m'inscrire à une formation Afin d'obtenir le diplôme.
-           En tant que voyageur, Je veux réserver un billet de train Afin d'aller voir ma mère.
-           En tant qu'opérateur Je veux créer un compte pour un client Afin de recevoir son argent.
-           En tant qu'organisateur, Je veux connaître le nombre de personnes inscrites à la conférence Afin de choisir la salle adéquate.
-           En tant que client de la banque, Je veux pouvoir créditer mon compte, Afin d’augmenter mon solde.
Une fois les demandes formalisées sous forme de Story, il faudra écrire les scénarios de chaque Story.

Ecriture des  scénarios

En complément des demandes en termes des story, l’expert métier va devoir réfléchir à des cas de test. De la même manière que lorsqu’il rédige un cahier de recette, il va pouvoir mettre à l’épreuve sa demande et détailler son besoin. De la même manière qu’une story, les cas de test sont formalisés avec un langage Gherkin  composé de mot clés :


Un scénario met en évidence la réponse que doit fournir le système face à un événement compte tenu d’un contexte initial. Chaque scénario est constitué d’une description et d’un ensemble d’étape (Step). Il existe 3 étapes :
-              Given : qui permet de définir et de construire le contexte dans lequel le scénario va se dérouler,
-           When : soit de provoquer des événements ou des actions sollicitant le système When,
-           soit de vérifier que le comportement attendu a bien eu lieu Then; c’est généralement à ces étapes que l’on retrouvera les assertions.
Une story est constituée d’un ensemble de scénarios nous permettant de tester le cas passant (Tout se passe bien, c’est le cas le plus classique)  ou non passant.

Exemple 1 : Story «  En tant que client de la banque, Je veux pouvoir créditer mon compte, Afin d’augmenter mon solde »
On pourra trouver le scénario:

Scénario 1 : créditer un compte
Given je dispose de 20 euros sur mon compte bancaire
When je crédite mon compte de 10 euros
Then mon solde devrait être de 30 euros.

Exemple 2 : Story « En tant qu’utilisateur, je veux me connecter à google afin d’accéder à tous mes services en lignes »

Scénario 1 : Accéder à la page de connexion
Given L’utilisateur n’est pas connecté
And L’utilisateur est sur la page d’accueil google
When L’utilisateur demande à se connecter
Then La page de connexion est affichée

Scénario 2 : Se connecter
Given L’utilisateur n’est pas connecté
And L’utilisateur est sur la page de connexion
When L’utilisateur saisi son identifiant
And L’utilisateur saisi son mot de passe
And L’utilisateur clique sur « Connexion »
Then La page d’accueil google s’affiche en <30s
And L’identifiant de l’utilisateur est affiché à droite dans le bandeau.

Exemple 3 : Story « En tant que client, je veux pouvoir acheter un billet de train afin de me rendre à ma destination»

Scénario 1 : Acheter un billet pour Bamako avec ma carte bancaire et avec assez d’argent sur mon compte 
Given  il reste de la place dans le train pour Bamako
And ma carte bancaire est valide
And j’ai suffisamment d’argent sur mon compte
When je valide l’achat d’un billet pour Londres avec ma carte bancaire
Then je reçois une confirmation de la transaction
And j’obtiens 1 billet pour Bamako
And j’obtiens le reçu de ma carte bancaire
And mon compte est débité du montant du billet

Scénario 2 : Acheter un billet pour Bamako avec ma carte bancaire et avec peu d’argent sur mon compte 
Given il reste de la place dans le train pour Londres
And ma carte bancaire est valide
And je n’ai pas suffisamment d’argent sur mon compte
When je valide l’achat d’un billet pour Londres avec ma carte bancaire
Then je reçois un message m’indiquant que la transaction a échoué

BDD en pratique avec Jbehave 

Historiquement le premier frameworks à avoir vu le jour pour BDD fut Jbehave

Pour commencer, vous pouvez installer le plugin Jbehave. C’est simple pour eclipse :

Help > Install New Software...
Add the new site location http://jbehave.org/reference/eclipse/updates/
Select JBehave Eclipse feature and follow standard Eclipse installation procedure



Dépendance Maven :


  <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <jbehave.version>3.9</jbehave.version>
    </properties>

       <dependency>
            <groupId>org.jbehave</groupId>
            <artifactId>jbehave-core</artifactId>
             <version>${jbehave.version}</version>
        </dependency>
          <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>jbehave-junit-runner</artifactId>
            <version>1.1.2</version>
            <scope>provided</scope>
        </dependency>

Les scénarios d'une Story sont décrits dans un fichier texte (une Story par fichier). Par où commencer ? La méthodologie BDD préconise de prioriser en choisissant la Story qui se porte sur le comportement le plus important de l’application. La décision de priorisation est prise en concertation avec le chargé fonctionnel. Le choix se porte le plus souvent sur la valeur business la plus importante. La question simple à se poser, est : Quelle est la fonctionnalité la plus importante qui manque actuellement à mon application ?

La structure du fichier est la suivante :



 Le fichier texte définit nos jeux de tests à vérifier, définissant donc nos critères d’acceptabilité. Usuellement, le fichier texte porte l’extension « .Story », mais il est également possible de customiser via la classe  «StoryPathResolver» qui permet de rechercher les fichiers histoires d’après l’extension.

Jbehave vous permet tout simplement de mapper le fichier contenant les scénarios d’une story sur une classe de test Java exécutable. Les étapes à suivre sont :

-          Ecriture des scénarios de chaque story dans un fichier texte
-          Ecriture de classes POJO qui mappent sur les scénarios
-          Ecriture de la classe de configuration permettant d’exécuter les scénarios.


Un exemple I : DAB Distributeur Automatique de Billet

1.      Ecriture des scénarios

 Pour une raison de simplicité, nous allons voir deux Stories d'une application de gestion de DAB. Une Story pour la création de compte bancaire et une autre pour le retrait d’argent depuis le distributeur de billet.

Story 1 : Création d'un compte bancaire


Story 2 : Retrait d'argent depuis un distributeur de billet


Jbehave propose une table contenant une liste de paramètre afin de pouvoir exécuter le même scénario plusieurs fois. Chaque ligne de la table représente un jeu de données à exécuter en séquence pour tester différentes déclinaison du scénario. Dans cet exemple, le scénario sera effectué deux fois avec comme paramètre respectif la ligne.

2.      Ecriture de classe de mapping

La classe de mapping Story Création de compte bancaire


package com.larbotech.jbehave;

import junit.framework.Assert;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;

/**
 * Created by mtoure on 12/04/15.
 */
public class CompteBancaireSteps {

    private CompteBancaire compteBancaire;

    @When("Je crée un nouveau compte")
    public void creationCompte() {
        compteBancaire = new CompteBancaire();
    }

    @Then("J'obtiens un compte bancaire initialisé à zero comportant un numéro à 4 chiffres")
    public void compteCree() {
        Assert.assertEquals(compteBancaire.getSolde(), 0);
        Assert.assertEquals(String.valueOf(compteBancaire.getNumero()).length(), 4);
    }

    @Given("Le solde du compte est à zero")
    public void soldeZero() {
        Assert.assertEquals(compteBancaire.getSolde(), 0);
    }

    @When("J'ajoute 100 euros")
    public void crediterCompte() {
        compteBancaire.crediter(100);
    }

    @Then("Le solde du compte indique 100 euros")
    public void compteCredite() {
        Assert.assertEquals(compteBancaire.getSolde(), 100);
    }

}

La classe mapping Story Retrait d'argent depuis un distributeur de billet


package com.larbotech.jbehave;

import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Named;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;

public class DABScenarioSteps {

    private DistributeurAutomatiqueBillets dab;
    private CompteBancaire compteBancaire;
    private CarteBancaire carteBancaire;

    private int argent;

    @Given("le compte du client est crédité d'un montant <solde>")
    public void creerCompteBancaire(@Named("solde") int solde) {
        compteBancaire = new CompteBancaire(solde);
    }

    @When("le client dipose d'une carte bancaire")
    public void creerCarteBancaire() {
        carteBancaire = new CarteBancaire(compteBancaire);
    }

    @When("le distributeur contient un montant <fonds>")
    public void creerDistributeurBillet(@Named("fonds") int fonds) {
        dab = new DistributeurAutomatiqueBillets(fonds);
    }

    @When("le client demande un montant <montant>")
    public void retirer(@Named("montant") int montant) {
        argent = dab.retrait(carteBancaire, montant);
    }

    @Then("le distributeur délivre un montant <montant_delivre>")
    public void checkMontantDelivreParDistributeur(@Named("montant_delivre") int montant) {
        assertThat(argent, is(montant));
    }

    @Then("le nouveau solde du compte <nouveau_solde>")
    public void checkSoldeCompteApresRetrait(@Named("nouveau_solde") int solde) {
        assertThat(solde, is(carteBancaire.getCompteBancaire().getSolde()));
    }

}

3.      Ecriture de classe de configuration


La classe de configure Story Création de compte bancaire


package com.larbotech.jbehave;

import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML_TEMPLATE;

import java.util.List;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InstanceStepsFactory;

public class CompteBancaireScenarios extends JUnitStory {

    // Here we specify the configuration, starting from default MostUsefulConfiguration, and changing only what is needed
    @Override
    public Configuration configuration() {
          StoryReporterBuilder storyReporter = //
          new StoryReporterBuilder() //
                  .withDefaultFormats() //
                  .withFormats(CONSOLE, //
                          HTML_TEMPLATE) //
                  .withFailureTrace(true) //
                  .withFailureTraceCompression(true)
                  .withRelativeDirectory("jbehave-report")//
                  ;
        return new MostUsefulConfiguration()
                // where to find the stories
                .useStoryLoader(new LoadFromClasspath(this.getClass()))
                       .useStoryReporterBuilder(storyReporter);
                        // CONSOLE and TXT reporting
               // .useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(StoryReporterBuilder.Format.CONSOLE, StoryReporterBuilder.Format.TXT, StoryReporterBuilder.Format.HTML, StoryReporterBuilder.Format.XML));
    }

    // Here we specify the steps classes
    @Override
    public List<CandidateSteps> candidateSteps() {
        // varargs, can have more that one steps classes
        return new InstanceStepsFactory(configuration(), new CompteBancaireSteps()).createCandidateSteps();
    }
}

La classe de configuration Story Retrait depuis un distributeur de billet


package com.larbotech.jbehave;

import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML_TEMPLATE;

import java.util.List;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InstanceStepsFactory;

public class DABScenario extends JUnitStory {

    // Here we specify the configuration, starting from default MostUsefulConfiguration, and changing only what is needed
    @Override
    public Configuration configuration() {
          StoryReporterBuilder storyReporter = //
          new StoryReporterBuilder() //
                  .withDefaultFormats() //
                  .withFormats(CONSOLE, //
                          HTML_TEMPLATE) //
                  .withFailureTrace(true) //
                  .withFailureTraceCompression(true) //
                  ;
        return new MostUsefulConfiguration()
                // where to find the stories
                .useStoryLoader(new LoadFromClasspath(this.getClass()))
                       .useStoryReporterBuilder(storyReporter);
                        // CONSOLE and TXT reporting
               // .useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(StoryReporterBuilder.Format.CONSOLE, StoryReporterBuilder.Format.TXT, StoryReporterBuilder.Format.HTML, StoryReporterBuilder.Format.XML));
    }

    // Here we specify the steps classes
    @Override
    public List<CandidateSteps> candidateSteps() {
        // varargs, can have more that one steps classes
        return new InstanceStepsFactory(configuration(), new DABScenarioSteps()).createCandidateSteps();
    }

//
}

Un exemple II : Inscription de client sur un site eCommerce

Afin de pouvoir passer sa commande, un nouveau  client doit pouvoir s’inscrire sur le site internet en renseignant ses informations personnelles. Une fois identifié par le système via son nom, prénom, adresse mail ainsi que son mode passe, un client peut parcourir et choisir des articles, qu’il souhaite acheter.

1.      Ecriture des scénarios




2.      Ecriture de classe de mapping



package com.larbotech.jbehave;

import junit.framework.Assert;

import org.jbehave.core.annotations.Alias;
import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Named;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;

import com.larbotech.dao.ClientDao;

/**
 * Created by mtoure on 12/04/15.
 */
public class InscriptionClientSteps {

    private Client client;
    private ClientDao clientDao = new ClientDao();
   
    @Given("Un nouveau client")
    @Alias("Un client exitant")
    public void creationCompte() {
     client = new Client();
    }

    @When("le client entre ces informations <nom>, <prenom>, <email>, <password>")
    @Alias("le client entre ces informations larbo, Jean, larbo.jean@gmail.com, secret41")
    public void saisirInformationsClient(@Named("nom") String nom, @Named("prenom") String prenom,
      @Named("email") String email,@Named("password") String password) {
      client.setNom(nom);
      client.setPrenom(prenom);
      client.setMail(email);
      client.setPassword(password);
     
    }
    
    @When("le client valide ses informations")
    public void submit() {
     clientDao.insert(client);
     
    }
    
    @Then("le compte client est crée")
    public void clientCree() {
     Assert.assertTrue(client.getId() != null && client.getId()>0);
        Assert.assertTrue(clientDao.find(client) != null);
    }
    
    @Then("le client n'est pas crée")
    public void clientNonCree() {
     Assert.assertTrue(client.getId() == null);
    }
   
    @Then(" le client est invalide")
    public void informationsManquantes() {
     Assert.assertFalse(client.estValide());
    }
   
 
}


Remarquons l’annotation @Alias. Elle permet de spécifier deux lignes de texte d’un même scénario ou presque !  Cela permet par exemple de ne pas réécrire les mêmes actions dans deux étapes (Given, When, Then) différentes.

3.      Ecriture de classe de configuration

.

package com.larbotech.jbehave;

import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML_TEMPLATE;

import java.util.List;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InstanceStepsFactory;

public class InscriptionClientScenarios extends JUnitStory {

    // Here we specify the configuration, starting from default MostUsefulConfiguration, and changing only what is needed
    @Override
    public Configuration configuration() {
          StoryReporterBuilder storyReporter = //
          new StoryReporterBuilder() //
                  .withDefaultFormats() //
                  .withFormats(CONSOLE, //
                          HTML_TEMPLATE) //
                  .withFailureTrace(true) //
                  .withFailureTraceCompression(true)
                  .withRelativeDirectory("jbehave-report")//
                  ;
        return new MostUsefulConfiguration()
                // where to find the stories
                .useStoryLoader(new LoadFromClasspath(this.getClass()))
                       .useStoryReporterBuilder(storyReporter);
                        // CONSOLE and TXT reporting
               // .useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(StoryReporterBuilder.Format.CONSOLE, StoryReporterBuilder.Format.TXT, StoryReporterBuilder.Format.HTML, StoryReporterBuilder.Format.XML));
    }

    // Here we specify the steps classes
    @Override
    public List<CandidateSteps> candidateSteps() {
        // varargs, can have more that one steps classes
        return new InstanceStepsFactory(configuration(), new InscriptionClientSteps()).createCandidateSteps();
    }
}


Pour finir, vous avez la possibilité d'exécuter plusieurs Stories :


package com.larbotech.jbehave;

import static org.jbehave.core.io.CodeLocations.codeLocationFromClass;
import static org.jbehave.core.reporters.Format.CONSOLE;
import static org.jbehave.core.reporters.Format.HTML_TEMPLATE;

import java.net.URL;
import java.util.List;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.io.StoryFinder;
import org.jbehave.core.junit.JUnitStories;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.InstanceStepsFactory;

import org.junit.runner.RunWith;

import de.codecentric.jbehave.junit.monitoring.JUnitReportingRunner;

@RunWith(JUnitReportingRunner.class)
public class AllStoriesTest extends JUnitStories {

    public AllStoriesTest() {
        configuredEmbedder()//
                .embedderControls()//
                .doGenerateViewAfterStories(true)//
                .doIgnoreFailureInStories(false)//
                .doIgnoreFailureInView(true)//
                .doVerboseFailures(true)//
                .useThreads(2)//
                .useStoryTimeoutInSecs(6000);
    }

    @Override
    public Configuration configuration() {
          StoryReporterBuilder storyReporter = //
          new StoryReporterBuilder() //
                  .withDefaultFormats() //
                  .withFormats(CONSOLE, //
                          HTML_TEMPLATE) //
                  .withFailureTrace(true) //
                  .withFailureTraceCompression(true) //
                  ;
        return new MostUsefulConfiguration()
                // where to find the stories
                .useStoryLoader(new LoadFromClasspath(this.getClass()))
                       .useStoryReporterBuilder(storyReporter);
                        // CONSOLE and TXT reporting
               // .useStoryReporterBuilder(new StoryReporterBuilder().withDefaultFormats().withFormats(StoryReporterBuilder.Format.CONSOLE, StoryReporterBuilder.Format.TXT, StoryReporterBuilder.Format.HTML, StoryReporterBuilder.Format.XML));
    }
    
    @Override
    protected List<String> storyPaths() {
        URL searchInURL = codeLocationFromClass(this.getClass());
        return new StoryFinder().findPaths(searchInURL, "**/*.story", "");
        //return Arrays.asList("de/codecentric/simplejbehave/Math.story");
    }

    /**SpringStepsFactory pour spring : annoter des composant avec un custom annotation (stepComponent)
     *  
     */
    @Override
    public InjectableStepsFactory stepsFactory() {
        return new InstanceStepsFactory(configuration(),
           new CompteBancaireSteps(), new DABScenarioSteps(), new InscriptionClientSteps());
    }
   
}

lundi 30 mars 2015

Covariance, Contravariance et Invariant

Afin de rendre les classes et les méthodes plus génériques possibles, Java nous propose 3 niveaux de "généricité" : Covariance, Contravariance et Invariance. Je vous propose dans ce petit article, une brève présentation de ces 3 concepts en m'inspirant des exemples simples.

Pour commencer, voici un petit diagramme de classe ainsi que le code des classes associées permettant d’illustrer mes propos : 



Nous disposons également de trois méthodes avec une liste générique en paramètre de chaque méthode  :


 /** 
     * 
     * @param ovals
     */
    static void toDoInvariant(List<Oval> ovals)
    {
     //.....
    }

    /**
     * 
     * @param ovals
     */
    static void toDoCovariance(List<? extends Oval> ovals)
    {
        //.....
    }

    /**
     * 
     * @param ovals
     */
    static void toDoContravariance(List<? super Oval> ovals)
    {
        //.....
    }

Covariance 

Une classe C<T> est en covariance en T si toute classe A, B : si A est une sous-classe de B alors C<A> est une sous classe de C<B>. C’est une façon de spécifier que toutes les classes filles/les sous-classes sont acceptables.

Un exemple de covariance en Scala (T[+A], la classe Vector est nativement covariante) ou en Java T =  ? extends A  avec A une classe ou une interface. À la place de T, il est possible d’utiliser tout type (B et C) qui extends/implements A (B/C etends A ou implements A).


La covariance permet de définir un type qui peut recevoir n’importe quel type plus spécifique en limitant son utilisation à partir d’un type parent (A). Tout A ou qui l’hérite (direct ou indirect) est accepté.Le type A peut être vu comme la borne supérieure dans l’arborescence héritage de classes :



La méthode toDoCovariance prenant en paramètre une liste du type covariant ( ? extends Oval ) n'accepte que les type héritant de la classe Oval (la classe Oval comprise !) :



L’utilisation d’une covariance sur une liste la rend immutable. La liste est accessible en lecteur seule (Read-Only). L'appel de la méthode add sur la liste ovals engendre une erreur de compilation :


Contravariance  

Une classe C<T> est en contravariance en T si toute classe A, B : si A est une super-classe de B alors C<A> est une super classe de C<B>. C’est une autre façon de spécifier que toutes les classes parents/les super-classes sont acceptables.

En Scala, la contravariance est notée par [-A] avec A une classe ou une interface. Un exemple en Java T =  ? super A. Le type T peut être remplacé par tout type (B, C) parent de A (A implemnts B ou A extends C).


A l’inverse de la covariance, la contravariance permet de définir un type qui peut recevoir n’importe quel type moins spécifique. Le type A ( ? super A) détermine une sorte de borne inférieure : tout type parent direct ou indirect de A est accepté :



La méthode toDoContravariance prenant en paramètre une liste du type contravariant ( ? super Oval ) n'accepte que les type parent de la classe Oval (la classe Oval comprise) :


Une liste avec un type contravariant est accessible en écriture seule (Write-Only).  Cependant, l’accès aux éléments reste possible mais seulement sous forme d’Object.  Dans l'appel de la méthode toDoContravariance , nous remarquons bien que Object objectOval = ovals.get(0); ne génère pas de problème de compilation :


Invariance

Un type est invariant signifie que vous pouvez utiliser uniquement le type spécifié à l'origine. Un type invariant n’est pas modifiable ni pour le passer en paramètre de méthode ou pour l’affecter à une autre instance. Il est par exemple impossible d'affecter une liste de Forme à une liste de Rectangle :



La méthode toDInvariant n'accepte qu'une liste d'Oval :


La liste reste accessible en lecture et écriture :




lundi 16 mars 2015

GIT Introduction : Concepts indispensables à comprendre

Après avoir utilisé GIT en mission et migré mes projets perso, je vous propose un petit recueil de mes différentes lectures !

Créé par Linus Torvalds en 2005, le père du noyau Linux, Git est aujourd'hui un système de contrôle de version OpenSource à la mode. L’objectif d’un système de contrôle de  version est de garder un historique de toutes les modifications apportées aux fichiers d’un projet donné. Il est ainsi plus aisé de savoir quel développeur a effectué une modification, quand il a effectué, et surtout en quoi consistait cette modification. GIT est plus particulièrement un outil de gestion de version décentralisé, cette caractéristique signifie seulement que l’historique n’a pas nécessairement besoin  d’être stocké sur un serveur central.

GIT n'était pas, au départ, à proprement parler un logiciel de gestion de versions. Linus Torvalds expliquait que, « par bien des aspects, vous pouvez considérer Git comme un système de fichiers : il permet un adressage associatif, et possède la notion de versionnage, mais surtout, a été conçu en résolvant le problème du point de vue d'un spécialiste des systèmes de fichiers. Il n'y avait donc aucun intérêt à créer un système de gestion de version traditionnel. ». Il a aujourd'hui évolué pour intégrer toutes les fonctionnalités d'un gestionnaire de versions.

GIT est aujourd'hui un outil incontournable pour les développeurs, les architectes et les chefs de projet qui collaborent sur le même code source. De nombreux outils tels qu’Eclipse ou Netbeans l'intègrent par défaut ainsi que des plateformes de travail collaboratif en ligne comme le réseau Github.com.

Concepts clés à comprendre

Les systèmes de contrôle de version centralisés comme Subversion ou CVS stockent pour chaque fichier modifié, les modifications dans le temps. Il est important de noter que GIT est très différent de ces systèmes. Subversion, CVS, et les autres utilisent tous un système de stockage de Patch (∆) — ils stockent les différences entre un commit et le suivant. Par exemple, le fichier de la version 2 dans l’exemple ci-contre, est obtenu en appliquant le patch (∆) à la version v1 du fichier (fichier A v2 = fichier A (v1) + ∆). Par contre, GIT stocke les fichiers modifiés complets, et pas seulement les modifications (seulement s’ils ont été modifiés). Ainsi pour obtenir la version 2, Git prend directement le fichier v2.


Puisque GIT enregistre les modifications de manière globale au projet, la reconstruction de l’historique d’un seul fichier demande plus de travail qu’avec un système de gestion de versions  (CVS par exemple) qui traque les fichiers individuellement.

Objets manipulés par GIT

Il existe 4 types d’objet dans GIT. Chaque objet est caractérisé par 3 attributs : un type, une taille et le contenu. La taille est simplement la taille du contenu, le contenu dépend du type de l’objet et il y a 3 types d’objets différents : « blob », « tree », « commit » :

• Un « blob » est utilisé pour stocker les données d’un fichier — il s’agit en général d'un fichier.
• Un « tree » est comme un répertoire — il référence une liste d’autres « tree » et/ou d’autres « blobs » (i.e. fichiers et sous-répertoires).
• Un « commit » pointe vers un unique "tree" et le marque afin de représenter le projet à un certain point dans le temps. Il contient des méta-informations à propos de ce point dans le temps, comme le timestamp, l’auteur du contenu depuis le dernier commit, un pointeur vers le (ou les) dernier(s) commit(s), etc.

Il faudra souligner que ces objets de types Blob, Tree ou Commit ne sont pas mutables et ne doivent normalement pas être supprimés. Dans la suite de cet article, nous allons détailler chaque type d’objet.

Blob

Pour effectuer la gestion des versions d’un projet, GIT dispose d’un répertoire spécifique dénommé .git (nom par défaut). Le répertoire .git contient tout l’historique de GIT et les méta-informations du projet. Chacune des versions de vos données est conservée dans la base d’objets (object database) qui réside dans le sous-dossier .git/objectsGIT utilise un classement par contenu : les fichiers ne sont pas stockés selon leur nom mais selon leur contenu. Ce contenu est stocké dans des fichiers du répertoire GIT (.git) dont le nom est déterminé avec l’empreinte des données qu’ils (les fichiers versionnés) contiennent. Nous pouvons considérer l’empreinte comme un ID unique du contenu d’un fichier permettant de retrouver un fichier par son contenu. L’empreinte est obtenue en appliquant une fonction de hachage.

Une fonction de hachage est une fonction particulière qui, à partir d'une donnée fournie en entrée, calcule une empreinte servant à identifier rapidement la donnée initiale. Les fonctions de hachage servent à rendre plus rapide l'identification des données : calculer l'empreinte d'une donnée ne doit coûter qu'un temps négligeable. Une fonction de hachage doit par ailleurs éviter autant que possible les collisions (états dans lesquels des données différentes ont une empreinte identique).

Pour chaque objet (un fichier, un répertoire) modifié, GIT le stocke en appliquant :

-          La fonction de hachage SHA1 (sha1sum) pour pouvoir identifier ou référencer (« référence » est à comprendre dans le même sens qu'une « référence » Java, ou qu'un pointeur en C) son contenu de façon unique. Cet identifiant est aussi utilisé comme nom du fichier dans lequel le contenu compressé du fichier est sauvegardé
-          Une fonction de compression du contenu du fichier (zlib) en données binaires.



Le résultat de ces deux opérations nous donne un blob :


Un « blob » n’est donc rien de plus qu’un morceau de données binaires. Il ne fait référence à rien et n’a aucun autre attribut excepté son type (Blob) et la taille, même pas un nom de fichier. Puisque le « blob » est entièrement défini par son contenu, si deux fichiers dans un répertoire (ou dans différentes versions du dépôt) ont le même contenu, ils partageront alors le même objet blob.

Si par exemple le SHA de l'objet est ab04d884140f7b0cf8bbf86d6883869f16a46f65, alors le fichier sera stocké dans le chemin suivant :
 
.git/objects/ab/04d884140f7b0cf8bbf86d6883869f16a46f65

Il récupère les 2 premiers caractères (ab) et les utilisent comme sous-répertoire, comme ça il n'y a jamais trop d'objets dans le même dossier. Le nom du fichier est alors composé des 38 caractères restant.

La stratégie de stockage des objets précédemment décrite peut devenir inefficace si par exemple vous avez un fichier long de quelques milliers de lignes. Si une seule ligne est modifiée, c’est un gaspillage si on se permet d’enregistrer un autre fichier dans son intégrité (car les empreintes SHA1 seront différentes). Pour pallier à ce problème, GIT ne sauve que la partie du fichier qui a été modifiée dans un autre fichier avec un pointeur vers le fichier similaire (packfile).

Peut-être vous demandez-vous ce qui se produit pour des fichiers ayant le même contenu. Essayez en créant des copies de votre premier fichier, avec des noms quelconques. Le contenu de .git/objects reste le même quel que soit le nombre de copies que vous avez ajoutées. GIT ne stocke le contenu qu’une seule fois.

Mais que deviennent les noms des fichiers ? Ils doivent bien être stockés quelque part à un moment. Pour répondre à la question, nous abordons le sujet d’un autre type d’objet GIT appelé : tree.

Tree

L'objet tree (mot anglais signifiant « arbre »), est une liste d'objets de type blobs ou d’autres tree et des informations associées : le type (si c’est un fichier ou un répertoire), le nom du fichier et les permissions, etc. Il représente en général, le contenu d’un répertoire ou d’un sous-répertoire. Un objet référencé par un « tree » peut être un « blob » représentant le contenu d’un fichier ou un autre « tree » représentant le contenu d’un sous-répertoire. Puisque les « trees » et les « blobs », comme les autres objets, sont identifiés par le hash SHA1 de leur contenu, deux « trees » ont le même identifiant SHA1 si, et seulement si, leur contenu (en incluant récursivement le contenu de tous les sous-répertoires) est identique. Cela permet à git de déterminer rapidement les différences entre deux objets « trees » associés puisqu'il peut ignorer les entrées avec le même nom d’objet. Comme le contenu d’un tree dépend de son contenu (ses fichiers et ses répertoires),  la question du sous répertoire vide se pose. Comment stocker les sous répertoires vides ? La réponse est claire, les sous répertoires vides ne peuvent pas être stockés. Placez-y des fichiers sans intérêt pour remédier à ce problème.



Commit 

Nous avons expliqué 2 des 3 types d’objets. Le troisième est l’objet commit. C’est un terme trompeur auquel je préfère le mot snapshot ou version d’un projet. Par commit, nous entendons ici une version du repository, i.e. un état (une photographie ou un snapshot) du projet à un instant donné. Le mot commit désigne à la fois la création d'une nouvelle version (lorsque c'est un verbe) et cette nouvelle version (lorsque c'est un nom). "Je commit" veut dire, j'entérine les changements que j'ai effectués et ils constituent une version. "Le deuxième commit de mon projet", désigne sa deuxième version. Notez qu’un « commit » ne contient pas d’information à propos de ce qui a été modifié ; tous les changements sont calculés en comparant les contenus du « tree » référencé dans ce « commit » avec le « tree » associé au(x) parent(s) du « commit ».

A chaque fois que vous effectuez un commit dans GIT, il prend effectivement une copie du contenu de votre espace de travail à ce moment et enregistre une référence à cette copie. Si les fichiers n’ont pas changé, GIT ne stocke pas le fichier à nouveau, juste une référence vers le fichier original qui n’a pas été modifié. Quand on parle d’un commit git, il faut systématiquement comprendre une version/snapshot du projet. 

Un commit est caractérisé par une référence à un commit parent (la version à partir de laquelle, la nouvelle version du projet (commit) a été créée), un auteur et un message qui décrit les modifications effectuées. Une autre caractéristique d’un commit est la référence à un tree. Cette référence tree, est une référence sur un répertoire contenant les objets du commit.



Vous pouvez voir que nous avons créé un objet tree pour chaque répertoire (pour la racine aussi) et un objet blob pour chaque fichier. Ensuite nous avons un objet commit qui pointe vers la racine, afin que nous puissions récupérer l’apparence du projet quand il a été committé.
Comme évoqué précédemment, un commit n’est ni plus ni moins qu’une version du projet, un snapshot pris à un instant T. Partant d'un commit, disons A, on peut faire plusieurs commits ayant comme parent ce commit A. C'est le cas dès que plusieurs personnes font un commit en parallèle en partant de la même version.
A chaque fois que vous faites un commit, GIT enregistre un «arbre» qui permettra d’identifier vos fichiers modifiés (qui seront stockés sous forme de blobs) et lui associe un objet commit qui, lui-même, pointera vers le (ou les) commit immédiatement précédent et la figure suivante en donne un exemple :


Le commit v2 est créé à partir du commit v1 qui lui aussi créé à partir de la version initiale v0. Vous pouvez constater que des objets GIT sont partagés entre les commits. En effet, par exemple si le contenu d’un fichier n’est pas modifié entre deux commits, son Blob reste inchangée et sera référencé par les deux commits.

Branches
Dans le développement d'un projet, il arrive souvent qu'on souhaite travailler sur une fonctionnalité en limitant les interactions avec le reste de l'équipe. Un moyen de faire ceci est d'utiliser ce qu’on appelle des branches. Une branche est une « ligne de développement ». Les branches permettent de séparer la version principale d’un projet, des versions de travail, elles permettent également de travailler sur plusieurs fonctionnalités de manière simultanée. 
Vu qu'un commit n'est qu'une version du projet, il est tout naturellement possible de travailler su un commit. Lorsque vous connaissez la référence d’un commit (disons C), vous pouvez effectuer un checkout afin de travailler sur cette version du projet. Le développement démarre à partir du commit C et tous vos futures commits formeront une branche dans l’arbre de commits du projet. Cette arborescence de commits qu’on appelle une branche peut être identifiée par un nom (le nom de la branche). Le nom est utilisé comme référence qui pointe toujours vers le dernier commit de votre branche (la dernière version du projet de votre branche). Une branche GIT est donc tout simplement un nom donné à un commit.  Lorsqu’un projet est initié pour être géré par GIT (git init), le tout premier commit crée automatiquement une branche dénommée master (qu’on appelle la branche principale). A chaque commit sur cette branche principale, la référence master se déplace et pointe sur le dernier commit.

Comment GIT connaît-il la branche sur laquelle vous vous trouvez ? Il conserve la référence du dernier commit de la branche lors du checkout et lui donne un nom spécifique HEAD. Comme le nom de votre branche qui pointe vers la référence du dernier commit, le HEAD change et se déplace sur cette même référence. Le HEAD change à chaque fois qu'on change de branche et à chaque commit. Le HEAD pointe vers le commit sur lequel on est en train de travailler (la référence est sauvegardée dans le fichier .git/HEAD).

Partons d’un projet root vide initialisé depuis un serveur GIT (arma) :



 Ensuite, on se connecte sur une autre machine (larbo) afin de faire un checkout (clone dans le jargon de GIT) du projet root vide :


Avec la commande git branch, vous pouvez afficher les branches. Comme indiqué sur la figure suivante, nous pouvons constater qu’il n’existe aucune branche. En effet GIT exige un premier commit lui permettant de créer la branche principale master. Ajoutons un nouveau fichier test.txt avec comme contenu (git introduction : version initiale du fichier test.txt) :



Procédons au premier commit puis visualisons les objets GIT avec la commande git cat-file –p SHA1 :


Le HEAD pointe bien sur le commit effectué et la référence du tree (root) du répertoire .git/objects/cc : cc6e54 …. Visualisons les autres objets :


La visualisation du Blob (7856be…) du fichier test.txt nous montre bien son contenu (git introduction : version initiale du fichier test.txt).

Deuxième commit : Ajoutons d’abord un nouveau fichier new.txt et modifions le fichier test.txt :





Visualisons les objets :




Troisième commit :

Pour finir, ajoutons un nouveau répertoire bak et effectuons une copie de la version initiale  (version 1) du fichier test.txt dans ce nouveau répertoire tout en modifiant la version actuelle (version 2) du fichier test.txt pour avoir une v3. Le fichier new.txt reste inchangé :



Les objets GIT :




Remarquons juste que le HEAD pointe bien sur le dernier commit :