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.
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.
À 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).
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 :
|
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 :
|
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.
Appliquez le GRASP Polymorphisme pour le code suivant :
plumage() {
get 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";
} }
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;
} }
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") {
= new EuropeanSwallow();
bird
}else if (b.type == "AfricanSwallow") {
= new AfricanSwallow(b.numberOfCoconuts);
bird
}else if (b.type == "NorwegianBlueParrot") {
= new NorwegianBlueParrot(b.voltage)
bird
}else {
throw new Error("Unsupported bird.");
}
// Then, we do something with the 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
;
}
.directors.append(directorJSON);
movieJSON
}
return movieJSON;
} }
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.trim();
title = parseInt(year);
year = parseInt(pages);
pages = isbn.trim();
isbn
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
.add(isbn, book);
books
} }
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(",");
= new Part(partsInfo[0], partsInfo[1]);
part
}else if (fileType == "json") {
const part = JSON.parse(content);
= new Part(part.name, part.description);
part
}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
= new Part(name, description);
part
}else {
throw new Error("Unsupported file type");
}
// Then, we do something with the part...
} }
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,
;
})
} }