Création de classes

Création de classes #

Nous allons réaliser une première conception en prenant comme exemple un compteur de personnes. Ce genre d’appareil est utilisé à bord des avions pour compter les voyageurs avant le décollage. L’image ci-dessous illustre une version mécanique de l’appareil que nous allons modéliser.

source: tallycounterstore.com

Le cahier des charges est très simple. Nous souhaitons réaliser une version digitale de cet appareil. Il doit offrir les mêmes fonctionnalités :

  • l’état initial est à 0 lorsque l’appareil est enclenché
  • un premier bouton permet de compter par incrément de 1
  • compter jusqu’à 9999.
  • si le compteur dépasse la capacité, il recommence à 0
  • un deuxième bouton permet de réinitialiser le compteur à 0
  • il est possible de connaître le nombre actuel

Rappelez-vous qu’un objet doit toujours être dans un état cohérent et permettre des transitions d’un état valide à un autre état valide.

Une version simple, sans objet, serait d’avoir une variable entière que l’on incrémente:

counter += 1;

Mais cette solution ne respecte pas le cahier des charges. Il ne devrait être possible d’augmenter la valeur avec un incrément différent de 1 par exemple.

1
2
3
counter += 10; // oups
counter = -100 // pire
counter = 9999 + 2; // ==> 10001

ou alors, le dépassement de capacité oblige l’utilisateur à se préoccuper de la logique

1
2
3
4
5
if (counter == 9999) {
    counter = 0;
} else {
    counter += 1;
}

Voyons maintenant la solution objet avec une classe Counter. Des assertions sont employées pour démontrer l’exactitude de l’état du compteur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Counter counter = new Counter();
assert counter.value() == 0 : "initial value should always be set to 0";
counter.click();
counter.click();
counter.click();
assert counter.value() == 3;
counter.reset();
assert counter.value() == 0 : "value should be equal to 0 after a reset";
while (counter.value() < 9999) {
    counter.click();
}
assert counter.value() == 9999;
counter.click();
assert counter.value() == 0 : "value should be equal to 0 after an overflow";

Nous remarquons que les fonctionnalités proposées permettent de respecter le cahier des charges.

constructeur/méthodes règles-métier
Counter() (constructeur) l’état initial est à 0 lorsque l’appareil est enclenché
void click() - un premier bouton permet de compter par incrément de 1
- compter jusqu’à 9999
- si le compteur dépasse la capacité, il recommence à 0
void reset() un deuxième bouton permet de réinitialiser le compteur à 0
int value() il est possible de connaître le nombre actuel (un short aurait pu être employé)

Une assertion peut indiquer un message après les :

assert <condition> : "<message>"

Il est possible d’activer les assertions dans le jshell en le lançant avec les options suivantes:

jshell -R -ea

La classe Counter offre le strict nécessaire tout en respectant la cohérence des règles-métiers. Elle fait peu de choses, mais elle le fait bien. Voyons maintenant la réalisation de cette classe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Counter {
    private int counter; // initialisé à 0 par défaut
    public void click() {
        this.counter = (this.counter + 1) % 10000;
    }
    public void reset() {
        this.counter = 0;
    }
    public int value() {
        return this.counter;
    }
}

Quelques remarques:

  • la visibilité (mots-clés public, private…) définissent les accès aux membres, classes, … depuis l’extérieur. Consultez l’annexe pour plus d’informations
  • la classe est publique (mot-clé public), ce qui permet son utilisation et sa visibilité partout dans le code
  • le champ counter permet de définir la structure et l’état d’un objet de cette classe. L’accès doit être interdit pour préserver l’encapsulation, d’où le mot-clé private
  • chaque méthode exposée est également publique. Elles peuvent accéder au champ counter
  • le mot-clé this permet de référencer un membre de l’objet courant. L’utilisation n’est pas toujours obligatoire, mais rend le code plus explicite et moins ambigu. Le même mot-clé appelé avec des parenthèses correspond à un constructeur de la classe courante.

Caractéristiques #

Profitons de cette occasion pour apporter quelques caractéristiques plus détaillées sur les concepts d’objet et de classe.

Caractéristiques d’un objet

  • un objet est composé de membres
    • les attributs (ou champs) = structure de l’objet
    • les méthodes = comportement de l’objet
  • ses attributs ont des valeurs = état de l’objet
  • généralise le concept de variable
  • est une instance d’une classe (dans les langages basés sur les classes)

Caractéristiques d’une classe

  • définit la structure interne des données et les méthodes disponibles d’une même famille d’objets
  • les membres publiques définissent le contrat de la classe, appelé également interface de classe
  • généralise le concept de type

Caractéristiques d’une méthode

  • méthode d’instance
    • elle est rattachée à un objet
  • méthodes statiques ou de classe
    • elle est rattachée à la classe
    • utilisation sans instanciation d’un objet

Encapsulation #

Souvenez-vous qu’une caractéristique de la POO est l’encapsulation. Il ne faudrait jamais exposer des détails d’implémentation ou une dépendance avec le monde extérieur. La modification d’une classe ne devrait pas imposer à l’utilisateur de notre API de modifier son code. Une erreur courante qui brise l’encapsulation est l’utilisation abusive des accesseurs (appelé également getters) et de mutateurs (appelé également setters). Ceux-ci ont la facheuse tendance à divulguer le nom d’un champ et son type sans être d’une grande assistance. Leur utilisation a été abondante dans les années 90 lorsque les éditeurs de code les généraient automatiquement. Dès lors, le développeur se concentrait sur la structure d’un objet en lieu et place de son comportement.

Observons une telle conception.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class BadCounter {
    private int counter;

    public void setCounter(int i){ 
        this.counter = i; 
    }
    public int getCounter() {
        return this.counter;
    }
}

Ce premier mauvais exemple est dit “anémique” : il n’offre aucune logique. Celle-ci est déplacée hors de l’objet.

1
2
BadCounter badCounter = new BadCounter();
badCounter.setCounter( badCounter.getCounter+1 ); // incrément à réaliser à l'extérieur de l'objet

Même si la deuxième version ci-dessous offre à nouveau une logique, cette classe permet de nous retrouver dans un état incohérent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class BadCounter {
    private int counter;
    public void click() {
        this.counter = (this.counter + 1) % 10000;
    }
    public void setCounter(int i){ 
        this.counter = i; 
    }
    public int getCounter() {
        return this.counter;
    }
    public void reset() {
        this.counter = 0;
    }
}

Exemple de mauvaises utilisations:

1
2
BadCounter badCounter = new BadCounter();
badCounter.setCounter( -100000 );

D’autres questions se posent:

  • que se passe-t-il si le champ change de nom ? Faut-il changer le nom des méthodes ?
  • que se passe-t-il si le type du champ change ? Faut-il également modifier la signature des méthodes ?

La méthode toString() #

Cette méthode peut-être redéfinie dans n’importe quelle classe et peut être appelée sur tous les objets. Elle permet de représenter un objet sous la forme du chaîne de caractères. Par défaut, l’appel de cette méthode sur une classe qui ne l’a pas redéfini retourne le nom de la classe, le caractère @, et le hashCode de l’objet en représentation décimale.

1
2
Counter c = new Counter();
System.out.println( c.toString() ); // ==> Afficherait "Counter@4926097b"

Ajoutons cette méthode pour afficher les quatre digits du compteur sous cette forme:

Counter(0012)

Pour réaliser cet exploit, nous pouvons ajouter cet extrait à notre classe Counter:

1
2
3
4
5
    public String toString() {
        final String tooMuchDigits = ("0000" + this.counter);
        final String fourDigits = tooMuchDigits.substring( tooMuchDigits.length()-4, tooMuchDigits.length() );
        return "Counter(" + fourDigits + ")";
    }

Avec cette méthode, l’appel suivant devient

1
2
Counter c = new Counter();
System.out.println( c.toString() );

Afficherait:

Counter(0000)

Un chapitre sera consacré au cas de la classe Objet et aux redéfinitions possibles.

Membres privés #

Par contre, puisque l’objectif est l’encapsulation, rien ne vous empêche d’ajouter d’autres fonctionnalités utiles à votre mise en oeuvre pour autant qu’elles ne soient pas exposées. Pour se faire, utilisez des membres privés (mot-clé private). C’est déjà le cas de notre champ counter qui ne doit pas être accessible depuis l’extérieur. Ajoutons une méthode privée.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Counter {
    private int counter;

    public void click() {
        this.safeSetCounter(this.counter + 1);
    }
    private void safeSetCounter(int i) {
        this.counter = i % 10000;
    }
    public void reset() {
        this.counter = 0;
    }
    public int value() {
        return this.counter;
    }
    private static String toFourDigits(int i) {
        final String tooMuchDigits = ("0000" + i);
        return tooMuchDigits.substring( tooMuchDigits.length()-4, tooMuchDigits.length() );
    }
    public String toString() {
        return "Counter(" + toFourDigits(this.counter) + ")";
    }
}

Constructeurs #

Nous avons vu que la valeur par défaut d’un entier est 0. Le champ counter prend donc la valeur 0 lors de sa création. Une classe offre implicitement un constructeur sans argument. Nous pouvons toutefois déclarer notre propre constructeur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Counter {
    private int counter;
    public Counter() {
        this.counter = 0; // inutile, pour la démo uniquement
    }
    public void click() {
        this.counter = (this.counter + i) % 10000;
    }
    public void reset() {
        this.counter = 0;
    }
    public int value() {
        return this.counter;
    }
}

Pour interdire la création implicite du constructeur sans argument, deux options sont possibles:

  • créer un constructeur sans argument privé
   private Counter() {}
  • créer au moins un constructeur avec argument privé ou public

Construction via des méthodes statiques #

Il est également possible d’exclure la création d’un objet par son constructeur et d’offrir des méthodes statiques, appelées fabriques, pour la création d’objets. Modifions notre exemple selon la nouvelle fonctionnalité de notre cahier des charges:

  • nous souhaitons initialiser notre compteur avec une valeur comprise entre 0 et 9999 comprise.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Counter {

    private int counter;

    private Counter(int i) {
        this.safeSetCounter(i);
    }

    public void click() {
        this.safeSetCounter(this.counter + 1);
    }

    private void safeSetCounter(int i) {
        this.counter = i % 10000;
    }

    public void reset() {
        this.counter = 0;
    }

    public int value() {
        return this.counter;
    }

    public static Counter zero() {
        return new Counter(0);
    }

    /**
     * Returns a new Counter with de defined value included between 0 and 9999. Throws an IllegalArgumentException
     * if the value is out of the range
     *
     * @param value the initial value between 0 and 9999
     * @return a counter with the given value set
     * @throws IllegalArgumentExcpetion if value is not included in the specific range
     */
    public static Counter withValue(int value) {
        if (value < 0 || value > 9999) {
            /* Nous préférons retourner une exception si l'utilisateur fait une bêtise */
            throw new IllegalArgumentException("Value must be between 0 and 9999");
        }
        return new Counter(value);
    }

    private String toFourDigits(int i) {
        final String tooMuchDigits = ("0000" + this.counter);
        return tooMuchDigits.substring( tooMuchDigits.length()-4, tooMuchDigits.length() );
    }

    public String toString() {
        return "Counter(" + toFourDigits(this.counter) + ")";
    }
}

Un des avantages des fabriques est qu’elles permettent d’avoir un nom explicite. Voyons l’utilisation d’un objet appartenant à notre nouvelle classe:

1
2
3
4
Counter counter = new Counter(); // ==> IMPOSSIBLE, ne compile pas !
Counter counter1 = Counter.zero(); // ==> compteur initialisé à 0
Counter counter2 = Counter.withValue(100); // ==> compteur initialisé à 100
Counter counter3 = Counter.withValue(10000); // ==> plante ! L'utilisateur ne respecte pas le contrat

Remarquez le commentaire de notre méthode statique withValue. Il est préférable de lever une exception rapidement plutôt que de tenter de corriger un mauvais argument passé en paramètre et d’avoir un programme dans un état instable. Il est donc important d’indiquer à l’utilisateur cette condition dans la documentation.

Le diagramme de classes uml (Unified Modeling Language) est parfois utilisé pour illustrer une conception. Les membres soulignés signifient qu’ils sont statiques. Le symbol + est utilisé pour indiquer qu’un membre est public. Le symbole - indique qu’il est privé. Cependant, l’indication des membres privés n’exprime rien d’intéressant pour l’utilisateur de notre API et le développeur est libre de se préoccuper lui-même de la mise en oeuvre d’une classe, du choix des structures de données et des méthodes privées. Exposer des membres privés revient à briser l’encapsulation dès la conception. Dans ce cours, nous exposerons uniquement les membres non privés.

La classe Counter peut dès lors être représentée ainsi:

classDiagram class Counter { +click() void +reset() void +value() int +toString() String +zero()$ Counter +withValue(int value)$ Counter }

Exécuter Counter #

Pour rendre la classe exécutable, il est nécessaire d’ajouter une méthode statique main:

    public static void main(String[] args) {
      Counter counter = Counter.withValue(10); 
      System.out.println( counter );
    }

Vous trouvez cet extrait de code sur githepia .












comments powered by Disqus