-
Cours :
- Introduction et premiers patrons de conception (pdf)
- Tests unitaires et patrons de conception (pdf)
- Principes SOLID (pdf)
- Types paramétrés et patrons de conception (pdf)
- Patrons de conception (pdf)
- Planches de TD :
- Planches de TP :
- Polycopiés de cours :
- Annales :
Introduction
Présentation de l’UE
Présentation de partie conception
L’Unité d’Enseignement Initiation Génie Logiciel est consacrée à ce que l’on nomme en programmation la conception logicielle. La conception logicielle consiste à donner à un projet de programmation une organisation souple mais structurante. Une telle organisation permettra de garder un code clair au cours des différentes évolutions que le code subira. Ces évolutions devront pouvoir être faites facilement, et le code pourra à tout moment être testé facilement. La modularité intelligente est une clé de voûte de la conception. Les principes de cette modularité sont énoncés dans les principes SOLID. Le savoir-faire de cette modularité est résumé dans les patrons de conceptions qui permettent d’éviter les pièges habituels de conception en suivant des modèles sous la forme de diagrammes UML.
Les différentes points abordés durant ce cours sont :
- Gestion de version
- Tests
- Documentation
- Diagrammes de classes UML
- Principes SOLID : SRP, OCP, LSP, ISP, DIP
- Présentation et utilisation de patrons de conceptions sur des exemples concrets
Bibliographie
Ce cours sur la conception s’appuie en autre sur les ouvrages suivants :
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (trad. Jean-Marie Lasvergères), Design Patterns - Catalogue de modèles de conceptions réutilisables, France, Vuibert, 1999.
- Robert C. Martin, Agile Software Development : Principles, Patterns, and Practices, Upper Saddle River, NJ, Pearson Education, 2002.
- Robert C. Martin, Coder proprement, Pearson France, 2019.
- J B Rainsberger & Scott Stirling, JUnit Recipes - Practical Methods for Programmer Testing, Manning publishing
Déroulement des enseignements
Les cours, exercices et devoirs seront publiés tout au long du semestre. La note finale (\(NF\)) de cette UE est simplement la note à l’examen final.
Langage de programmation
Le langage utilisé durant le cours sera le Java. Il est tout à fait possible d’appliquer les bonnes pratiques de programmation dont principes SOLID ainsi que les patrons de conceptions avec d’autres langages de programmation permettant le paradigme de programmation orientée objet comme Python, C#, C++, PHP, Swift, Kotlin, … Normalement, vous devriez avoir suivi deux unités d’enseignement sur la programmation orientée objet nommées Programmation 1 et Programmation 2. Si vous avez besoin de réviser le fonctionnement de Java et les concepts de base de la programmation orientée objet, vous pouvez consulter les supports de cours de ces deux unités d’enseignements aux liens suivants :
- Programmation 1 : lien supports
- Programmation 2 : lien supports
Logiciels à installer
Contrairement à d’autres cours, il n’y a pas d’obligation d’utiliser un système d’exploitation UNIX/Linux pour pouvoir travailler sur la partie conception car Java a été conçu pour être exécutable facilement sur n’importe quel système d’exploitation. Néanmoins, l’utilisation d’un tel système est bien sûr possible et peut faciliter l’utilisation de certains outils (comme la configuration de l’authentification via une clé SSH). Vous avez la possibilité d’utiliser la machine virtuelle Lunix disponible au lien suivant : système de virtualisation. Si jamais vous avez des problèmes pour accéder à ces machines virtuelles vous pouvez posez des questions dans le forum général d’échanges du cours ou bien formuler une demande d’aide personnalisée à la DIRNUM (DIRection du NUMérique) via le service de télé-demandes accessible depuis votre ENT.
Kit de développement Java
Afin de pouvoir compiler et exécuter du code Java, il vous faudra installer un Java Development Kit (JDK). Nous vous conseillons la version 17 de Java qui peut être téléchargée sur le site d’Oracle au lien suivant.
Sous Ubuntu ou tout système
linux utilisant le gestionnaire de paquet apt
(c’est-à-dire généralement une distribution debian), vous pouvez
installer le JDK 17 avec la ligne de commande suivante :
apt install openjdk-17-jdk openjdk-17-jre
.
Environnement de développement
Pour coder proprement, il est souvent utile de disposer d’un environnement de développement (IDE pour Integrated development environment). Un IDE est un ensemble d’outils comportant un éditeur de texte dédiée à la programmation (avec une autocomplétion intelligente), des fonctions et menus qui permettent, de compiler et exécuter du code, d’accéder à des outils pour déboguer, de gérer la gestion de version, fournir des outils de refactoring efficaces, …
Il existe de nombreux IDE adaptés au langage de programmation Java. Les plus utilisés sont les suivants :
- IntelliJ IDEA de JetBrains qui est l’IDE qui va être mis en avant dans les supports et qui est disponible sur les machines virtuelles fournies par AMU dans sa version ultimate;
- Eclipse qui est très similaire à IntelliJ IDEA;
- Visual Studio Code qui est plus léger que les deux IDE précédents et pour lequel il faut installer les extensions java pour avoir le même type de fonctionnalités.
Il est recommandé d’installer un de ces logiciels ou équivalent pour cette partie car utilisé un bon environnement de développement est une première étape pour construire proprement un projet logiciel d’une taille un peu conséquente.
Moteur de production
Un moteur de production est un logiciel dont la fonction principale consiste à automatiser le processus de création d’un logiciel à partir d’un code source. Cela comprend la compilation du code source, le packaging de l’executable (création d’installateur ou d’exécutable), la gestion des dépendances (liens avec des bibliothèques externes, téléchargements, …), l’exécution automatisés de tests, …
Pour les projets de ce cours, nous utiliserons gradle comme moteur de production. Il permet en autre de rendre facile la mise en place de tests unitaires via les bibliothèques JUnit 5 et AssertJ.
Pour installer Gradle il suffit de suivre les instructions d’installation en téléchargeant la version 7.5.1 de Gradle.
Gestion de version
Lorsqu’on travaille sur le code un peu conséquent, il est utile d’utiliser un outil de gestion de version qui permet en autre de :
- conserver les différentes versions du code: conservation de l’historique des changements sous la forme d’un dépôt qui permet de revenir à n’importe quelle version;
- stocker le code à des endroits différents (machines personnelles des développeurs, serveur permettant gérer le code, …) avec des outils de synchronisation entre les différents dépôts;
- travailler en équipe en conservant l’origine de toutes les modifications (on sait donc qui blamer lorsqu’il y a des erreurs).
Dans ce cours, vous utiliserez git que vous pouvez télécharger au lien suivant : https://git-scm.com/download. Afin d’héberger votre code, vous avez accès à une instance gitlab à l’adresse suivante : https://etulab.univ-amu.fr/.
Bonnes pratiques de programmation
Avant de présenter les principes SOLID qui sont cinq principes de conception visant à produire des architecture logicielles qui sont plus compréhensibles et maintenables, il est important de faire un rappel sur les bonnes pratiques de programmation qui permettent de rendre le code plus lisible. Une des choses les plus importantes lorsqu’on écrit du code est de bien nommer les éléments du code. Nous allons donc commencer par expliquer les principes à respecter pour avoir un nommage efficace.
Une méthodologie pour bien nommer
Trouver des noms appropriés peut prendre beaucoup de temps sur le moment mais va vous permettre de gagner du temps par la suite. Les conventions de nommage sont extrêmement importante pour la maintenance et la lisibilité d’un programme. Il est donc important de :
- utiliser des noms cohérents pour tous les symboles:
- choisir un nom qui correspond au but/rôle du symbole (Le nom doit révéler le rôle de l’élément);
- utiliser l’anglais pour nommer vos éléments;
- choisir un nom en regardant le code source des librairies sérieuses comme celles fournies par la JDK.
Les raisons de l’importance du nommage sont les suivantes.
- Un nom bien choisi rend plus facile la lecture du programme.
- Lorsqu’on n’arrive pas à choisir un nom adapté, c’est souvent parce que son rôle n’est pas bien défini.
- Un programmeur passe 80% de son temps à lire le programme : il faut lui faciliter ce travail surtout qu’on peut passer beaucoup de temps à relire son propre code.
- Des incohérences logiques évidentes peuvent sauter aux yeux avec un bon nommage.
- Toutes les librairies sont codées en anglais.
Nommage des variables/arguments/attributs
Il y a certains écueils à éviter lorsqu’on cherche à trouver des noms pour des variables. Vous ne devez pas avoir des variables avec des :
- noms en une lettre (même pour les indices) :
i
,j
, … - noms numérotés :
a1
,a2
,a3
, … - abréviations ayant plusieurs interprétations :
rec
,res
, … - noms ne donnant pas le sens précis :
temporary
,result
, … - noms trompeurs : par exemple un
accountList
doit être uneList
(et pas unarray
ou un autre type) - types de l’objet au singulier pour une collection d’objet : une
liste de personnes doit s’appeler
persons
et nonperson
- noms imprononçables :
genymdhms
, …
Comme toute les bonnes pratiques, ce ne sont pas des règles absolues
et on peut évidemment y déroger. Par exemple, il est généralement
accepté d’utiliser x
et y
pour les coordonnées
d’un Point comme cela est fait pour la classe Point2D
de JavaFX. Néanmoins, comme pour
toutes les règles qui ne sont pas absolues, il est nécessaire de se
poser la question de savoir si on fait bien de ne pas les respecter.
Le Java et la quasi-totalité des langages de programmation utilise l’anglais (dans les mot-clés et les librairies standards). Par conséquent, on se doit de programmer en anglais pour avoir la cohérence du code. Utiliser l’anglais permet aussi d’augmenter le nombre de personnes pouvant lire le code et donc d’avoir de nombreux exemples existants pour s’en inspirer.
Une autre règle importante pour le nommage est de respecter le Code
Style de Java pour le nommage des variables/arguments/attributs.
Pour les noms composés d’un seul mot, on écrit tout simplement le mot en
minuscule. Pour un nom composé de plusieurs mots, on n’utilise ni espace
ni ponctuation, et on sépare les mots en mettant en capitale la première
lettre de chaque mot. Par exemple, cela donne :
flaggedCells
, gameBoard
, … Si le nom
comportait des lettres en dehors des 26 caractères non-accentué
classique de l’anglais (de a
à z
), on les
remplace par des caractères inclus dans les 26 caractères de base. Par
exemple, “root computed by Müller’s method” devient
rootComputedByMuellersMethod
.
Nommage des méthodes
Comme pour les variables, il est important de bien nommer les méthodes. Le nom d’une méthode doit décrire le service rendu à celui qui l’appelle, et non pas comment elle le fait. La convention de nommage est la même que pour les variables (capitale pour la première lettre de chaque mot sauf le premier). Dans une très grande majorité des cas, la méthode se trouve dans une des catégories suivantes :
- Ordre : méthodes exécutant une action avec comme
sujet l’objet avec lequel la méthode est appelée. Dans ce cas, on
utilise le groupe verbal à l’infinitif. Cela donne par exemple :
connection.open()
,list.sort(comparator)
,comparator.compare(object1, object2)
. - Requête booléenne : méthodes testant un prédicat
sur l’objet. Dans ce cas, on utilise un groupe verbal au présent. Cela
donne par exemple :
connection.isClosed()
,list.isEmpty()
,list.contains(object)
,node.hasNext()
,frame.canClose()
. - Requête non-booléenne : méthodes renvoyant une
partie de l’état de l’objet. Dans ce cas, on utilise un groupe nominal
ou bien un getter. Cela donne par exemple :
list.size()
,connection.getMetaData()
. - Conversion : méthodes convertissant l’objet en un
objet d’un autre type. Dans ce cas, on utilise
to
suivi du type ciblé. Cela donne par exemple :list.toArray(...)
,object.toString()
.
Comme pour le nommage des variables, ces règles ne sont pas absolues mais juste des conventions qui peuvent avoir des exceptions. En général le plus simple est de s’inspirer de l’existant : par exemple la JDK et de bien réfléchir lorsqu’on souhaite déroger aux règles.
Si vous avez des difficultés à nommer vos méthodes, c’est sans doute qu’elles font trop d’actions. Une méthode devrait avoir au maximum une dizaine de lignes de code. Il est toujours possible de satisfaire à cette contrainte en extrayant le plus possible les parties du code d’une méthode à d’autres méthodes. Il est donc important de :
- réfléchir avant de coder au rôle de la méthode ;
- se demander ce qui peut être confié à d’autres méthodes.
Une fonction ne doit donc faire qu’une seule chose. Pour cela, elle ne doit réaliser que des étapes de même niveau d’abstraction. Nous allons illustrer cela sur l’action de cuisiner. Pour faire la cuisine on doit (premier niveau d’abstraction) :
- choisir une recette ;
- réunir les ingrédients ;
- suivre la recette.
Pour choisir une recette, on doit (deuxième niveau d’abstraction):
- réfléchir à ce que j’ai envie de manger ;
- chercher sur marmiton.
Considérons le code suivant pour une méthode cook
:
void cook(){
// On choisit la recette
= thinkAboutFood();
Food wantToEat = lookOnMarmiton(wantToEat);
Recipe recipe // On réunit les ingrédients
openFridge();
for(Ingredient ingredient :
.getFreshIngredients()){
recipetakeInFrige(ingredient);
}
closeFridge();
openCupboard();
...
// On suit la recette
...
}
Dans le code de la méthode cook
, on utilise un niveau
d’abstraction trop bas. Cette approche n’est pas la bonne et on a été
obligé de rajouter des commentaires pour indiquer les étapes du premier
niveau d’abstraction. La bonne approche consiste à définir des méthodes
correspondant aux étapes du premier niveau d’abstraction et de les
appeler dans la méthode cook
. Cela nous donne le code
suivant :
void cook(){
= chooseRecipe();
Recipe recipe gatherIngredients(recipe);
followRecipe(recipe);
}
chooseRecipe(){
Recipe = thinkAboutFood();
Food wantToEat = lookOnMarmiton(wantToEat);
Recipe recipe return recipe;
}
...
Un autre écueil à éviter est de mentir dans le nom d’une méthode. Par
exemple, si on considère la méthode checkPassword
dans la
classe ci-dessous :
class User {
private boolean authenticated;
private String password;
public boolean checkPassword(String password) {
if (password.equals(this.password)) {
= true;
authenticated return true;
}
return false;
}
}
Cette méthode authentifie l’utilisateur alors qu’elle ne devrait que vérifier la validité du mot de passe d’après son nom. Il y a donc un mensonge (la méthode fait plus que ce qu’elle dit) ce qui complique fortement la compréhension du code. C’est donc un comportement à éviter.
Nommage des classes/interfaces/records/enums
En Java, pour tous les éléments du code qui correspondent à des types
(c’est-à-dire des classes, interfaces, records ou enums), on utilise une
majuscule pour la première lettre de chaque mot composant le nom du type
(y compris le premier mot contrairement aux variables et méthodes). Cela
donne par exemple : Shape
, Rectangle
,
ArrayList
, MountainBike
. Un type définissant
généralement une catégorie d’objet, le nom d’une
classe/interface/record/enum correspond dans la plupart des cas à un
groupe nominal au singulier. Cela vaut en particulier pour les
enum
de java qui doivent être au singulier.
L’enum
DayOfWeek
qui définit les sept jours de la semaine est au singulier car un objet
de type DayOfWeek
correspond à un jour de la semaine.
Parfois, on a besoin de regrouper un certain nombre de fonctions
static
ensemble. C’est par exemple ce qui se passe pour Math
qui contient des fonctions mathématiques de base ou Collections
qui contient des méthodes s’appliquant sur des collections. Dans ce cas
très particuliers, la classe ne permet pas de créer des objets (par
exemple le constructeur de Math
est privé et n’est pas
appelé dans la classe) et donc la convention de nommage ne s’applique
pas vraiment. Le nom de la classe doit juste donner les liens entre les
différentes méthodes statiques qu’on a regroupées dans celle-ci.