6  Principes GRASP

GRASP est un acronyme de l’expression anglaise « General Responsibility Assignment Software Patterns », c’est-à-dire les principes pour affecter les responsabilités logicielles dans les classes.

Une approche GRASP devrait amener un design vers la modularité et la maintenabilité.

L’acronyme d’une expression vulgarisée pourrait être POMM : « Principes pour déterminer Où Mettre une Méthode. »

En tant qu’ingénieur logiciel ou ingénieure logiciel, vous devez souvent décider où placer une méthode (dans quelle classe), et cette décision ne devrait pas être prise de manière arbitraire, mais plutôt en suivant les directives d’ingénierie favorisant la modularité.

Alors, les GRASP sont les directives qui vous aident à prendre des décisions de conception, menant à un design avec moins de couplage inutile et avec des classes plus cohésives. Les classes cohésives sont plus faciles à comprendre, à maintenir et à réutiliser.

Avez-vous déjà une bonne expérience en programmation ? Avez-vous l’habitude de coder rapidement des solutions qui fonctionnent ? Si la réponse est oui, alors travailler avec les principes GRASP peut être un défi pour vous. Dans la méthodologie enseignée dans ce manuel, vous devez être en mesure de justifier vos choix de conception, et cela va vous ralentir au début (réflexe du « hacking cowboy » peut-être ?) Le but avec les principes GRASP est de (ré)apprendre à faire du code qui fonctionne, mais qui est également facile à maintenir. C’est normal au début que ça prenne plus de temps, car il faut réfléchir pour appliquer les principes. Une fois que vous aurez l’habitude d’utiliser les GRASP, vous serez encore rapide avec votre développement, mais, en plus, votre design sera meilleur sur le plan de la maintenabilité, et vous aurez plus de confiance dans vos choix.

6.1 Spectre de la conception

Neal Ford (2009) a proposé la notion d’effort pour la conception qu’il a nommée le « Spectre de la conception ». La figure 6.1 illustre le principe.

[« Hacking cowboy »+Agile]« Cascade pure »

Figure 6.1: Spectre de la conception (Ford, 2009). (PlantUML)

À une extrémité, il y a la mentalité de mettre presque zéro effort pour une conception, que l’on nomme « hacking cowboy ». C’est le cas d’un hackathon (un marathon de programmation durant 24 ou 48 heures où il faut produire une solution rapidement), au cours duquel vous ne feriez pas un logiciel avec 10 patterns GoF et vous ne feriez pas non plus les diagrammes UML pour réfléchir à votre architecture. Vous savez aussi que le code produit lors d’un hackathon ne sera pas facile à maintenir. Le seul but de cette activité est de développer du code qui marche pour montrer une idée intéressante.

Une situation similaire s’applique à certains contextes d’entreprise, par exemple une entreprise en démarrage qui a seulement un financement pour six mois. Si une solution de « produit minimum viable » (MVP en anglais)  n’existe pas à la fin de la période de financement, l’entreprise n’existera plus, car il n’y aura pas une seconde période de financement. Si, par contre, l’entreprise est financée pour une seconde période, la conception du code pourrait avoir besoin de beaucoup de soin et de maintenance, car elle aura été préalablement négligée. Cette négligence de la conception (pour la maintenabilité) est aussi nommée la dette technique.

À l’autre extrémité du spectre de la conception, on retrouve ce que l’on nomme « Cascade pure », où beaucoup d’effort a été déployé pour la conception. Dans le cycle de vie en cascade, on met un temps fixe (par exemple plusieurs mois) à étudier la conception. Comme toute chose poussée à l’extrême, ce n’est pas idéal non plus. Dans son livre, Larman (2005) explique en détail des problèmes posés par une approche en cascade. En dépit des problèmes dus à l’approche en cascade, elle est encore utilisée dans certains domaines, par exemple les logiciels pour le contrôle d’avions ou le contrôle d’appareils médicaux. La sécurité et la robustesse des logiciels sont très importantes, alors on passe beaucoup de temps à vérifier et à valider la conception. Puisque les exigences sont plus stables (et que les développeurs et les développeuses ont a priori une meilleure compréhension du domaine), l’approche en cascade n’est pas si mal. Pourtant, le coût pour produire des logiciels certifiés est énorme.

Le spectre de la conception est très important pour le monde réel, parce qu’une ingénieure ou un ingénieur devrait pouvoir s’adapter selon les attentes de son travail. Le dogme sur « la bonne manière » de développer un logiciel est souvent sans contexte. C’est le contexte de l’entreprise pour laquelle vous travaillez qui peut quantifier les efforts à mettre sur la conception. Cependant, méfiez-vous des entreprises qui ne portent aucune attention à la conception (l’extrémité « hacking cowboy » du spectre), même si l’on vous dit que c’est « agile ».

6.2 Tableau des principes GRASP

Voici un extrait du livre de Larman (2005).

Tableau 6.1: Patterns (principes) GRASP
Pattern Description
Expert en Information
F16.11/A17.11 

Un principe général de conception d’objets et d’affectation des responsabilités.

Affectez une responsabilité à l’expert – la classe qui possède les informations nécessaires pour s’en acquitter.

Créateur
F16.10/A17.10 

Qui crée ? (Notez que Fabrique Concrète est une solution de rechange courante.)

Affectez à la classe B la responsabilité de créer une instance de la classe A si l’une des assertions suivantes est vraie :

  1. B contient A
  2. B agrège A [à favoriser]
  3. B a les données pour initialiser A
  4. B enregistre A
  5. B utilise étroitement A
Contrôleur
F16.13/A17.13 

Quel est le premier objet en dehors de la couche présentation qui reçoit et coordonne (« contrôle ») les opérations système ?

Affectez une responsabilité à la classe qui correspond à l’une de ces définitions :

  1. [Contrôleur de façade] [Cette classe] représente le système global, un « objet racine », un équipement ou un sous-système (contrôleur de façade).
  2. [Contrôleur de session] [Cette classe] représente un scénario de cas d’utilisation dans lequel l’opération système se produit (contrôleur de session ou contrôleur de cas d’utilisation). [On la nomme GestionnaireX, où X est le nom du cas d’utilisation.]
Faible Couplage
(évaluation)
F16.12/A17.12 

Comment minimiser les dépendances ?

Affectez les responsabilités de sorte que le couplage (inutile) demeure faible. Employez ce principe pour évaluer les alternatives.

Forte Cohésion
(évaluation)
F16.14/A17.14 

Comment conserver les objets cohésifs, compréhensibles, gérables et, en conséquence, obtenir un Faible Couplage ?

Affectez les responsabilités de sorte que les classes demeurent cohésives. Employez ce principe pour évaluer les différentes solutions.

Polymorphisme
F22.1/A25.1 

Qui est responsable quand le comportement varie selon le type ?

Lorsqu’un comportement varie selon le type (classe), affectez la responsabilité de ce comportement – avec des opérations polymorphes – aux types selon lesquels le comportement varie.

Fabrication Pure F22.2/A25.2 

En cas de situation désespérée, que faire quand vous ne voulez pas transgresser les principes de Faible Couplage et de Forte Cohésion ?

Affectez un ensemble très cohésif de responsabilités à une classe « comportementale » artificielle qui ne représente pas un concept du domaine — une entité fabriquée pour augmenter la cohésion, diminuer le couplage et faciliter la réutilisation.

Indirection
F22.3/A25.3 

Comment affecter les responsabilités pour éviter le couplage direct ?

Affectez la responsabilité à un objet qui sert d’intermédiaire avec les autres composants ou services.

Protection des Variations
F22.4/A25.4 

Comment affecter les responsabilités aux objets, sous-systèmes et systèmes de sorte que les variations ou l’instabilité de ces éléments n’aient pas d’impact négatif sur les autres ?

Identifiez les points de variation ou d’instabilité prévisibles et affectez les responsabilités afin de créer une « interface » stable autour d’eux.

6.3 GRASP et RDCU

Les principes GRASP sont utilisés dans les réalisations de cas d’utilisation (RDCU). On s’en sert pour annoter les décisions de conception, pour rendre explicites (documenter) les choix. Voir la section Réalisations de cas d’utilisation (RDCU) pour plus d’informations.

6.4 GRASP et patterns GoF

On peut voir les principes GRASP comme des généralisations (principes de base) des patterns GoF. Voir la section Décortiquer les patterns GoF avec GRASP pour plus d’informations.

6.5 Exercices

Exercice 6.1 (GRASP Polymorphisme) Soit le diagramme de classe sur la figure 6.2 modélisant l’exemple de Fowler (2018) à Replace Conditional with Polymorphism.

Birdtype : StringnumberOfCoconuts : numbervoltage : numbergetPlumage() : String

Figure 6.2: Classe « Bird » à laquelle on peut appliquer le principe GRASP Polymorphisme. (PlantUML)

Appliquez le GRASP Polymorphisme pour le code suivant :

get plumage() {
    switch (this.type) {
        case 'EuropeanSwallow':
            return "average";
        
        case 'AfricanSwallow':
            return (this.numberOfCoconuts > 2) ? "tired" : "average";
        
        case 'NorwegianBlueParrot':
            return (this.voltage > 100) ? "scorched" : "beautiful";
        
        default:
            return "unknown";
    }
}

Puisque le comportement de getPlumage change en fonction du type de l’oiseau, il est préférable de créer une classe concrète pour chaque oiseau (EuropeanSwallow, AfricanSwallow, NorwegianBlueParrot) afin d’améliorer la cohésion du code. L’utilisation d’une classe abstraite Bird permet de garder un faible couplage malgré l’ajout de plusieurs nouvelles classes. Par le principe de polymorphisme, les oiseaux concrets sont aussi considérés comme des Bird. Puisque les oiseaux ne partagent pas de propriétés ou d’implémentation, il aurait aussi été possible d’utiliser une interface pour la classe Bird.

BirdgetPlumage() : StringEuropeanSwallowAfricanSwallownumberOfCoconuts : numberNorwegianBlueParrotvoltage : number

Figure 6.3: Classes « Bird » auxquelles on a appliqué le principe GRASP Polymorphisme. (PlantUML)

abstract class Bird {
    abstract get plumage(): string
}

class EuropeanSwallow extends Bird {
    get plumage() {
        return "average" 
    }
}

class AfricanSwallow extends Bird {
    _numberOfCoconuts: number;

    constructor(numberOfCoconuts: number) {
        super();
        this._numberOfCoconuts = numberOfCoconuts;
    }
  
    get plumage() {
        return (this.numberOfCoconuts > 2) ? "tired" : "average";
    }

    get numberOfCoconuts() {
        return this._numberOfCoconuts;
    }
}

class NorwegianBlueParrot extends Bird {
    _voltage: number;

    constructor(voltage: number) {
        super();
        this._voltage = voltage;
    }

    get plumage() {
        return (this.voltage > 100) ? "scorched" : "beautiful";
    }

    get voltage() {
        return this._voltage;
    }
}

Exercice 6.2 (GRASP Contrôleur, Fabrication Pure et Indirection)  

  • Expliquez pourquoi le principe du GRASP Contrôleur dans le cas d’un contrôleur de session (de cas d’utilisation) est une Fabrication Pure.
  • Soit un contrôleur de session GestionnaireVentes ; quelle est la « situation désespérée » qui est mitigée par ce contrôleur ? Astuce : relire la définition de Fabrication Pure.
  • Expliquez pourquoi le principe du GRASP Contrôleur dans le cas d’un contrôleur de session est un exemple du GRASP Indirection.

Exercice 6.3 (GRASP Expert en Information et Forte Cohésion) Appliquez le GRASP Expert en Information pour améliorer la conception des classes dans le code suivant :

class Point {
    x:number;
    y:number;

    constructor(x:number, y:number) {
        this.x = x;
        this.y = y;
    }
}

class Circle {
    center:Point;
    radius:number;

    constructor(center:Point, radius:number) {
        this.center = center;
        this.radius = radius
    }
}

class SomeClass {
    someMethod() {
        const center = new Point(0, 12);
        const c = new Circle(center, 14);
        
        const area = Math.PI * c.radius* c.radius;
        const diameter = c.radius * 2;
        const circumference = Math.PI * c.radius * 2;
    }
}

Puisque la classe Circle possède les informations nécessaires (radius) pour calculer area, diameter et circumference, c’est elle qui doit contenir les méthodes pour calculer ces propriétés. Cela augmente la cohésion de la classe SomeClass puisqu’elle a moins de responsabilités. La complétude de la classe Circle est améliorée et ses méthodes peuvent être testées et réutilisées dans d’autres classes.

Pour être concis, la classe Point est omise puisqu’aucune modification n’y est apportée.

class Circle {
    center:Point;
    radius:number;

    constructor(center:Point, radius:number) {
        this.center = center;
        this.radius = radius
    }

    get area():number {
        return Math.PI * this.radius * this.radius;
    }

    get diameter():number {
        return this.radius * 2;
    }

    get circumference():number {
        return Math.PI * this.radius * 2;
    }
}

class SomeClass {
    someMethod() {
        const center = new Point(0, 12);
        const c = new Circle(center, 14);
        
        const area = c.area;
        const diameter = c.diameter;
        const circumference = c.circumference;
    }
}

Exercice 6.4 (GRASP Fabrication Pure et Indirection) Appliquez le GRASP Fabrication Pure au contrôleur BirdManager pour améliorer sa cohésion. Ajoutez une fabrique de Bird à qui le contrôleur pourra déléguer la responsabilité de créer les objets Bird. Par la suite, expliquez pourquoi la fabrique que vous avez ajoutée est un exemple des GRASP Indirection et Fabrication Pure.

class BirdManager {
    addBird(b: {type: String, numberOfCoconuts: number, voltage: number}) {
        let bird: Bird;

        if (b.type == "EuropeanSwallow") {
            bird = new EuropeanSwallow();
        }
        else if (b.type == "AfricanSwallow") {
            bird = new AfricanSwallow(b.numberOfCoconuts);
        }
        else if (b.type == "NorwegianBlueParrot") {
            bird = new NorwegianBlueParrot(b.voltage)
        }
        else {
            throw new Error("Unsupported bird.");
        }

        // Then, we do something with the bird...
    }
}

L’ajout de la fabrique BirdFactory permet d’améliorer la cohésion du contrôleur BirdManager puisque ses responsabilités sont réduites. La fabrique est un exemple du GRASP Indirection puisqu’elle élimine le couplage direct entre le contrôleur BirdManager et les différents types de Bird. Si d’autres types de Bird sont ajoutés à l’application, l’impact sera moins important sur le contrôleur. La fabrique est une Fabrication Pure, car elle n’existe pas dans le domaine de l’application. Elle a été inventée pour améliorer la cohésion de BirdManager et réduire son couplage avec les Bird concrets.

class BirdManager {
    addBird(b: {type: String, numberOfCoconuts: number, voltage: number}) {
        let bird: Bird = BirdFactory.createBird(b);

        // Then, we do something with the bird...
    }
}

class BirdFactory {
    public static createBird(b: {type: String, numberOfCoconuts: number, 
                             voltage: number}) {
        let bird: Bird;

        if (b.type == "EuropeanSwallow") {
            bird = new EuropeanSwallow();
        }
        else if (b.type == "AfricanSwallow") {
            bird = new AfricanSwallow(b.numberOfCoconuts);
        }
        else if (b.type == "NorwegianBlueParrot") {
            bird = new NorwegianBlueParrot(b.voltage)
        }
        else {
            throw new Error("Unsupported bird.");
        }

        return bird;
    }
}

Exercice 6.5 (GRASP Expert en Information et Forte Cohésion) Appliquez le GRASP Expert en Information aux classes Movie, Person et MovieManager pour améliorer leur cohésion. Pour être concis, les constructeurs, les accesseurs et les mutateurs des classes Movie et Person sont omis.

class Movie {
    _id:String
    _title:String
    _synopsis:String
    _year:number
    _duration:number
    _genres:String[]
    _directors:Person[]
}

class Person {
    _name:String
    _biography:String
}

class MovieManager {
    public getMovie(String id) {
        const movie:Movie = movies.get(id); // We get the movie from the map

        // We convert the movie to JSON
        var movieJSON = {
            id: movie.id,
            title: movie.title,
            synopsis: movie.synopsis,
            year: movie.year,
            duration: movie.duration,
            genres: movie.genres,
            directors: []
        };

        // We convert the directors to JSON and we add them to JSON representation of the movie
        for (var director:Person of movie.directors) {
            var directorJSON = {
                name: director.name,
                biography: director.biography
            };

            movieJSON.directors.append(directorJSON);
        }

        return movieJSON;
    }
}

Puisque les classes Movie et Person possèdent les informations nécessaires pour se convertir en JSON, ce sont elles qui doivent posséder les méthodes pour effectuer la conversion. Cela augmente la cohésion de la classe MovieManager puisqu’elle a moins de responsabilités. La complétude des classes Movie et Person est améliorée et leurs méthodes peuvent être testées et réutilisées dans d’autres classes.

class Movie {
    _id:String
    _title:String
    _synopsis:String
    _year:number
    _duration:number
    _genres:String[]
    _directors:Person[]

    public toJSON() {
        return {
            id: this._id,
            title: this._title,
            synopsis: this._synopsis,
            year: this._year,
            duration: this._duration,
            genres: this._genres,
            directors: this._directors // The conversion to JSON will be done automatically
        };
    }
}

class Person {
    _name:String
    _biography:String

    public toJSON() {
        return {
            name: this._name,
            biography: this._biography
        };
    }
}

class MovieManager {
    public getMovie(id:String) {
        const movie:Movie = movies.get(id); // We get the movie from the map

        return movie.toJSON();
    }
}

Exercice 6.6 (GRASP Expert en Information et Forte Cohésion) Appliquez le GRASP Expert en information aux classes Book et BookManager pour améliorer leur cohésion. Pour être concis, les accesseurs et les mutateurs de la classe Book sont omis.

class Book {
    _title:String
    _year:number
    _pages:number
    _isbn:String

    constructor(title:String, year:number, pages:number, isbn:String) {
        this._title = title;
        this._year = year;
        this._pages = pages;
        this._isbn = isbn;
    }
}

class BookManager {
    public addBook(title:String, year:number, pages:number, isbn:String) {
        title = title.trim();
        year = parseInt(year);
        pages = parseInt(pages);
        isbn = isbn.trim();

        if (title.length == 0) {
            throw new Error("The book title must not be empty.");
        }

        if (year > (new Date()).getFullYear()) {
            throw new Error("You cannot add a book that will be published in the future.");
        }

        if (pages <= 0) {
            throw new Error("The book must contain at least one page.");
        }

        if (!(isbn.length == 10 || isbn.length == 13)) {
            throw new Error("ISBN must have 10 or 13 characters.");
        }

        const book = new Book(title, year, pages, isbn);

        // We add the book to the map
        books.add(isbn, book);
    }
}

Puisque la classe Book contient les propriétés title, year, pages et isbn, c’est sa responsabilité de déterminer ce qui est une information erronée et d’effectuer les validations. Cela augmente la cohésion de la classe BookManager puisqu’elle a moins de responsabilités. De plus, si on doit à modifier les informations d’une livre existant, il ne sera pas nécessaire de dupliquer les validations. La complétude de la classe Book est améliorée et les validations peuvent être testées à l’aide de tests unitaires.

class Book {
    _title:String
    _year:number
    _pages:number
    _isbn:String

    constructor(title:String, year:number, pages:number, isbn:String) {
        this._title = title;
        this._year = year;
        this._pages = pages;
        this._isbn = isbn;
    }

    public get title() {
        return this._title;
    }

    public set title(title:String) {
        title = title.trim();

        if (title.length == 0) {
            throw new Error("The book title must not be empty.");
        }

        this._title = title;
    }

    public get year() {
        return this._year;
    }

    public set year(year:number) {
        year = parseInt(year);

        if (year > (new Date()).getFullYear()) {
            throw new Error("You cannot add a book that will be published in the future.");
        }

        this._year = year;
    }

    public get pages() {
        return this._pages;
    }

    public set pages(pages:number) {
        pages = parseInt(pages);

        if (pages <= 0) {
            throw new Error("The book must contain at least one page.");
        }

        this._pages = pages;
    }

    public get isbn() {
        return this._isbn;
    }

    public set isbn(isbn:String) {
        isbn = isbn.trim();
        
        if (!(isbn.length == 10 || isbn.length == 13)) {
            throw new Error("ISBN must have 10 or 13 characters.");
        }

        this._isbn = isbn;
    }
}

class BookManager {
    public addBook(title:String, year:number, pages:number, isbn:String) {
        const book = new Book(title, year, pages, isbn);

        // We add the book to the map
        books.add(isbn, book);
    }
}

Exercice 6.7 (GRASP Polymorphisme, Créateur, Fabrication Pure et Indirection) Appliquez les GRASP Polymorphisme et Créateur à la classe PartManager pour améliorer sa cohésion et réduire son couplage. Créez une interface PartParser qui contient les méthodes pertinentes aux parsers et qui sera implémentée par ces derniers. Ajoutez un PartParser concret pour chaque type de fichier traitée par la méthode parsePart. Ajoutez une fabrique de PartParser à qui PartManager pourra déléguer la responsabilité de créer les objets PartParser. Par la suite, expliquez pourquoi la fabrique que vous avez ajoutée est un exemple des GRASP Indirection et Fabrication Pure.

class Part {
    name:string
    description:String

    constructor(name:string, description:String) {
        this.name = name;
        this.description = description;
    }
}

class PartManager {
    public parsePart(fileType:string, content:string) {
        var part:Part;

        if (fileType == "csv") {
            const partsInfo = content.split(",");

            part = new Part(partsInfo[0], partsInfo[1]);
        }
        else if (fileType == "json") {
            const part = JSON.parse(content);

            part = new Part(part.name, part.description);
        }
        else if (fileType == "xml") {
            var parser = new DOMParser();
            var xml = parser.parseFromString(content, "application/xml");

            var name = xml.getElementsByTagName("name")[0].childNodes[0].nodeValue || ""; // Return an empty name if it is not in the XML tree
            var description = xml.getElementsByTagName("description")[0].childNodes[0].nodeValue || ""; // Return an empty description if it is not in the XML tree

            part = new Part(name, description);
        }
        else {
            throw new Error("Unsupported file type");
        }

        // Then, we do something with the part...
    }
}

La création de plusieurs PartParser permet d’améliorer la cohésion de la classe PartManager puisqu’elle a moins de responsabilités. Les PartParser peuvent être plus facilement réutilisés et testés à l’aide de tests unitaires. La création de l’interface PartParser permet de créer un API stable pour les PartParser, ce qui réduit le couplage entre PartManager et les différents types de PartParser. La fabrique est un exemple du GRASP Indirection puisqu’elle élimine le couplage direct entre la classe PartManager et les différents types de PartParser. Si d’autres types de PartParser sont ajoutés à l’application, l’impact sera moins important sur le contrôleur. La fabrique est une Fabrication Pure, car elle n’existe pas dans le domaine de l’application. Elle a été inventée pour améliorer la cohésion de PartManager et réduire son couplage avec les PartParser concrets.

class Part {
    name:string
    description:String

    constructor(name:string, description:String) {
        this.name = name;
        this.description = description;
    }
}

class PartManager {
    public parsePart(fileType:string, content:string) {
        var part:Part = ParserFactory.createParser(fileType, content).parse();

        // Then, we do something with the part...
    }
}

interface PartParser {
    public parse():Part
}

class CSVPartParser implements PartParser {
    public parse():Part {
        const partsInfo = content.split(",");

        return new Part(partsInfo[0], partsInfo[1]);
    }
}

class JSONPartParser implements PartParser {
    public parse():Part {
        const part = JSON.parse(content);

        return new Part(part.name, part.description);
    }
}

class XMLPartParser implements PartParser {
    public parse():Part {
        var parser = new DOMParser();
        var xml = parser.parseFromString(content, "application/xml");

        var name = xml.getElementsByTagName("name")[0].childNodes[0].nodeValue || ""; // Return an empty name if it is not in the XML tree
        var description = xml.getElementsByTagName("description")[0].childNodes[0].nodeValue || ""; // Return an empty description if it is not in the XML tree

        return new Part(name, description);
    }
}

class ParserFactory {
    public static createParser(fileType:string, content:string) {
        if (fileType == "csv") {
            return new CSVPartParser(content);
        }
        else if (fileType == "json") {
            return new JSONPartParser(content);
        }
        else if (fileType == "xml") {
            return new XMLPartParser(content);
        }
        else {
            throw new Error("Unsupported file type");
        }
    }
}

Exercice 6.8 (GRASP Protection des Variations et Fabrication Pure) Le code suivant permet de gérer les emprunts de livres pour une bibliothèque. À chaque emprunt un événement est ajouté dans le calendrier Google de l’emprunteur pour lui rappeler de retourner le livre à temps. Pour ce faire, l’API de Google Calendar est utilisé. Appliquez les GRASP Protection des Variations et Fabrication Pure pour améliorer la cohésion de ReservationManager. Pour être concis, la gestion des réservations dans l’application est omise.

class ReservationManager {
    public addReservation(String memberId, String bookId, String bookCopyId, Date expiration) {
        var member = members.get(memberId);
        var book = books.get(bookId);
        
        // We assume that we create the reservation for the member some way...

        // Then we add an event to the Google Calendar of the member to remind them to return their book
        var dateAsString = expiration.toLocaleDateString("fr-CA", {year: "numeric", month: "2-digit", day: "2-digit"});
        
        // References
        // https://developers.google.com/calendar/api/v3/reference?hl=fr
        // https://dev.to/pedrohase/create-google-calender-events-using-the-google-api-and-service-accounts-in-nodejs-22m8
        var event = {
            "summary": `Your reservation for ${book.getTitle()} is expired`,
            "location": "475 Boul. de Maisonneuve E, Montréal, QC H2L 5C4",
            "description": "You must return your book today to avoid late fees.",
            "start": {
                "date": dateAsString,
                "timeZone": "America/Toronto"
            },
            "end": {
                "date": dateAsString,
                "timeZone": "America/Toronto"
            },
            "reminders": {
                "useDefault": false,
                "overrides": [
                    // Send a reminder the day before by email and push notification
                    {"method": "email", "minutes": 24 * 60},
                    {"method": "popup", "minutes": 24 * 60}
                ]
            }
        };

        // Create a client that we can use to communicate with Google 
        const client = new JWT({
            email: member.email,
            key: member.googlePrivateKey,
            scopes: [
                "https://www.googleapis.com/auth/calendar",
                "https://www.googleapis.com/auth/calendar.events",
            ],
        });

        const calendar = google.calendar({ version: "v3" });

        // We make a request to Google Calendar API.
        const res = await calendar.events.insert({
            calendarId: "primary",
            auth: client,
            requestBody: event,
        });
    }
}

La classe GoogleCalendar est une Fabrication Pure, car elle n’existe pas dans le domaine de l’application. Elle a été ajoutée pour améliorer la cohésion de ReservationManager. Avec la classe GoogleCalendar, il est aussi plus facile de réutiliser la fonctionnalité dans une autre partie de l’application. Selon le GRASP Protection des Variations, la classe GoogleCalendar permet aussi de réduire le couplage entre ReservationManager et l’interface API de Google. Si Google apporte des changements à son interface API, l’impact sur la classe ReservationManager sera moins important.

Pour améliorer la cohésion de la classe ReservationManager, est-il possible de donner la responsabilité de créer l’objet event à une autre classe ?

Il serait possible de créer une classe Event qui représente un événement de l’interface API de Google. La cohésion de la classe ReservationManager ne serait pas significativement améliorée, puisqu’elle conserverait toujours la responsabilité de fournir les paramètres de l’événement. L’ajout de la classe Event permettrait de valider l’utilisation des bonnes propriétés et des bons types lors de la compilation du code. Toutefois, puisque les événements de l’interface API de Google sont complexes, l’ajout de la classe Event demanderait beaucoup de temps de développement et de tests. La pertinence de la classe Event dépend du contexte de développement dans lequel vous vous trouvez.

class ReservationManager {
    public addReservation(String memberId, String bookId, String bookCopyId, Date expiration) {
        var member = members.get(memberId);
        var book = books.get(bookId);
        
        // We assume that we create the reservation for the member some way...

        // Then we add an event to the Google Calendar of the member to remind them to return their book
        var dateAsString = expiration.toLocaleDateString("fr-CA", {year: "numeric", month: "2-digit", day: "2-digit"});

        GoogleCalendar.addEvent({
            "summary": `Your reservation for ${book.getTitle()} is expired`,
            "location": "475 Boul. de Maisonneuve E, Montréal, QC H2L 5C4",
            "description": "You must return your book today to avoid late fees.",
            "start": {
                "date": dateAsString,
                "timeZone": "America/Toronto"
            },
            "end": {
                "date": dateAsString,
                "timeZone": "America/Toronto"
            },
            "reminders": {
                "useDefault": false,
                "overrides": [
                    // Send a reminder the day before by email and push notification
                    {"method": "email", "minutes": 24 * 60},
                    {"method": "popup", "minutes": 24 * 60}
                ]
            }
        }, member.email, member.googlePrivateKey);
    }
}

class GoogleCalendar {

    /*
     * References
     * https://developers.google.com/calendar/api/v3/reference?hl=fr
     * https://dev.to/pedrohase/create-google-calender-events-using-the-google-api-and-service-accounts-in-nodejs-22m8
     */
    public static addEvent(event, email, privateKey) {
        // Create a client that we can use to communicate with Google 
        const client = new JWT({
            email: email,
            key: privateKey,
            scopes: [
                "https://www.googleapis.com/auth/calendar",
                "https://www.googleapis.com/auth/calendar.events",
            ],
        });

        const calendar = google.calendar({ version: "v3" });

        // We make a request to Google Calendar API.
        const res = await calendar.events.insert({
            calendarId: "primary",
            auth: client,
            requestBody: event,
        });
    }
}