TP2 : Comment renommer les choses ?
Résolution de conflits
Vous n’avez pas encore appris à résoudre les conflits de fusion (branches divergentes) sur Git, qui surviennent lorsqu’il existe plusieurs versions d’un même fichier dans différents dépôts et que vous tentez de les synchroniser. En cas de difficulté, appelez votre encadrant. Une fois cette compétence maîtrisée, vous serez autonome dans la gestion des conflits.
Objectifs¶
Le but de ce TP est de comprendre les points suivants :
- Travailler avec Git
- Magic numbers
- User input
- Membres d’une classe C++
- Getter et setter
- Constructeur d’une classe
- Les différentes conventions de nommage
- Noms révélateur d’intention
Exercice 1 : Magic numbers¶
#include <iostream>
#include <string>
using namespace std;
bool qualifiesForDiscount(double originalPrice) {
return originalPrice >= 10;
}
double calculateDiscountPercentage(double originalPrice, bool isStudent, bool isLoyalCustomer) {
double discountPercentage = 0.0;
if (qualifiesForDiscount(originalPrice)) {
if (isStudent) {
discountPercentage += 10;
}
if (isLoyalCustomer) {
discountPercentage += 10;
}
}
return discountPercentage;
}
double calculateDiscountAmount(double originalPrice, double discountPercentage) {
return originalPrice * discountPercentage/100;
}
double calculateFinalPrice(double originalPrice, bool isStudent, bool isLoyalCustomer){
double discountPercentage = calculateDiscountPercentage(originalPrice, isStudent, isLoyalCustomer);
double discountAmount = calculateDiscountAmount(originalPrice, discountPercentage);
double finalPrice = originalPrice - discountAmount;
return finalPrice;
}
double redeemStudentPoints(double discountAmount){
return discountAmount*(100-10)/100;
}
double redeemLoyaltyPoints(double discountAmount){
return discountAmount*(100-2*10)/100;
}
double calculateRewardPoints(double discountAmount, bool isStudent, bool isLoyalCustomer){
double rewardPoints = 0.0;
if (isStudent){
rewardPoints = redeemStudentPoints(discountAmount);
} else if (isLoyalCustomer){
rewardPoints = redeemLoyaltyPoints(discountAmount);
}
return rewardPoints;
}
int main() {
const double originalPrice = 100;
const bool isStudent = true;
const bool isLoyalCustomer = true;
cout << "Original price: " << originalPrice << " euros." << endl;
double finalPrice = calculateFinalPrice(originalPrice,isStudent,isLoyalCustomer);
cout << "Final price with student and loyalty discounts: " << finalPrice << " euros." << endl;
double discountAmount = originalPrice - finalPrice;
cout << "Your reward points: " << calculateRewardPoints(discountAmount,isStudent,isLoyalCustomer) << endl;
return 0;
}
- Enregistrez puis compilez le code (avec
g++ -o magic-numbers magic-numbers.cpp
).
Exécutez
./magic-numbers
.Comprenez-vous ce que fait ce code ?
Indices
Un client achète un produit au prix de originalPrice
.
Il peut bénéficier de réductions s’il est étudiant ou client fidèle, et les réductions obtenues permettent également de calculer des points de récompense.
Les règles sont les suivantes :
- Une réduction ne s’applique que si
originalPrice
dépasse 10 euros. - Un étudiant bénéficie d’une réduction de 10 %.
- Un client fidèle bénéficie également d’une réduction de 10 %.
- Si le client est à la fois étudiant et fidèle, les réductions s’additionnent pour atteindre 20 %.
- Avec
originalPrice = 100
euros, la réduction totale est donc de 20 euros. - Les points de récompense sont calculés en appliquant un pourcentage à cette réduction :
- (soit 90 % ici) donne 18 points.
- (soit 80 % ici) donne 16 points.
- La réduction étudiante étant toujours plus avantageuse, elle est appliquée en priorité.
- Ainsi, dans cet exemple, le client reçoit 18 points.
- Bien que les noms des éléments soient appropriés, ce code n’est pas propre. En voyez-vous les raisons ?
Magic numbers
On appelle magic numbers des nombres utilisés directement dans le code. Souvent, ces nombres apparaissent sans justification claire. Dans cet exemple, bien que la signification de certains nombres soit déductible grâce aux autres éléments bien nommés, il est préférable de les éviter.
Certains nombres magiques peuvent être acceptables, comme 100
pour les pourcentages, 0
pour initialiser une variable ou 1
pour un incrément.
Toutefois, la majorité d’entre eux devrait être évitée.
Ici, le nombre 10
apparaît plusieurs fois, mais sa signification varie selon le contexte. Imaginons que nous voulions modifier le seuil d’éligibilité pour une réduction, ajuster les pourcentages de réduction ou modifier les formules de récompenses. Le risque d’erreur devient alors considérable. De plus, il est difficile de repérer et de modifier les bonnes valeurs sans devoir replonger dans l’ensemble du code pour en comprendre la logique.
La solution consiste à attribuer des noms explicites à ces nombres magiques en fonction de leur portée, qu’ils soient locaux ou globaux. Il est essentiel d’adopter les bonnes conventions de nommage abordées en cours afin d’éviter les nombres magiques.
- Nommez les magic numbers.
Avez-vous accompli votre tâche de manière satisfaisante ?
Une façon simple de vérifier si vous avez correctement appliqué les conventions de nommage est d’essayer de modifier un seul paramètre à la fois (par exemple, le pourcentage de réduction étudiant, réduction client, etc.).
- Auriez-vous pu faire cette modification dans un code similaire de 10 000 lignes en utilisant une recherche (Ctrl + F) ?
- Avez-vous réussi à modifier une seule variable (une seule ligne dans le code) ?
Dans le cas contraire, renommez-les.
User input¶
Pour éviter de recompiler le code à chaque fois que vous modifiez originalPrice
, isStudent
et isLoyalCustomer
, nous pouvons récupérer les valeurs par clavier lors de l’exécution.
- Remplacez les lignes suivantes par le code qui suit.
const double originalPrice = 100;
const bool isStudent = true;
const bool isLoyalCustomer = true;
double userOriginalPrice;
cout << "Enter the original price: ";
cin >> userOriginalPrice;
const double& originalPrice = userOriginalPrice;
bool userIsStudent;
cout << "Are you a student? (1:Yes, 0:No) ";
cin >> userIsStudent;
const bool& isStudent = userIsStudent;
bool userIsLoyalCustomer;
cout << "Do you have a loyalty card? (1:Yes, 0:No) ";
cin >> userIsLoyalCustomer;
const bool& isLoyalCustomer = userIsLoyalCustomer;
const
const
L’utilisation de const <type>&
après les entrées utilisateur permet de garantir que ces valeurs (qui restent constantes tout le long de l’exécution) ne pourront pas être modifiées accidentellement dans le code, ce qui évite des erreurs imprévues.
La référence (&
) évite de faire des copies des valeurs de variables, ceci est surtout important quand nous travaillons avec des objets qui prennent beaucoup de mémoire.
Ici, l’utilisation d’une référence n’est pas nécessaire, car les types double
et bool
sont peu gourmands en mémoire. C’est principalement pour vous montrer un exemple d’usage de &
.
De façon générale, l’usage de const
devant les variables immutables permet de rendre le code plus clair en indiquant explicitement que ces valeurs ne doivent pas être modifiées, ce qui facilite la maintenance et la compréhension du programme.
Avez-vous bien utilisé les const
dans votre code ?
- Testez votre code et répondez aux questions du Quiz.
Git
Avez-vous pensé à maintenir vos dépôts ?
Exercice 2 : Classes et méthodes¶
#include <iostream>
using namespace std;
double b1 = 0.0;
double b2 = 100.0;
void d1(double x) {
b1 += x;
}
void d2(double x) {
b1 += x;
}
void w1(double x) {
if (x + 4 <= b1) {
b1 -= (x + 4);
} else {
cout << "Insufficient funds!" << endl;
}
}
void w2(double x) {
if (x + 2 <= b2) {
b2 -= (x + 2);
} else {
cout << "Insufficient funds!" << endl;
}
}
int main() {
d1(100.0);
w1(50.0);
w2(50.0);
cout << "First account balance: " << b1 << endl;
cout << "Second account balance: " << b2 << endl;
return 0;
}
- Comprenez-vous ce que fait ce code ?
Indices
- Il existe deux comptes (account) avec deux types différents : le type
1
que nous allons appeler “Basic” et le type2
, appelé “Standard”. - Le premier compte
b1
possède un solde (balance) initial de0.0
euro, tandis que le deuxième compteb2
a un solde initial de100.0
euros. - Il est possible de déposer (deposit) une somme (amount)
x
sur chacun des comptes, respectivement viad1
pour le premier compte etd2
pour le second. - L’argent peut être retiré (withdraw) avec
w1
pour le premier compte etw2
pour le second. - Lors d’un retrait, le compte de type
1
(Basic) applique un frais de retrait (withdrawal fee) de 4 euros, tandis que le compte de type2
(Standard) applique un frais de 2 euros.
Quiz : Lequel des deux comptes dispose du solde le plus élevé après ces transactions ?
Remplacez les transactions précédentes par les suivantes :
d1(100.0);
w1(50.0);
d2(100.0);
w2(50.0);
- Quiz : Maintenant, quel est le compte qui dispose du solde le plus élevé ?
Avez-vous repéré l’erreur ?
- Le code est-il logique ?
- Quelle est l’origine de l’erreur comptable ?
- Est-il facile d’ajouter et de réaliser des transactions avec un troisième compte ?
- Quelles améliorations peuvent être apportées à ce code ?
- Pour améliorer ce code, nous allons le réécrire de la manière suivante :
#include <iostream>
using namespace std;
enum t {B, S};
class b {
private:
double mB1;
t mT;
double mF;
public:
b(double b1, t t1) : mB1(b1), mT(t1) {
switch(t1){
case B:
mF = 4;
break;
case S:
mF = 2;
break;
}
}
void d(double x) {
mB1 += x;
}
void w(double x) {
if (x + mF <= mB1) {
mB1 -= (x + mF);
} else {
cout << "Insufficient funds!" << endl;
}
}
double getB1() const {
return mB1;
}
string tToString() const {
switch(mT){
case B: return "Basic";
case S: return "Standard";
default: return "Unknown";
}
}
};
int main() {
b a1(0.0,B);
b a2(100.0,S);
a1.d(100.0);
a1.w(50.0);
a2.d(100.0);
a2.w(50.0);
cout << "First account type: " << a1.tToString() << " and balance: " << a1.getB1() << endl;
cout << "Second account type: " << a2.tToString() << " and balance: " << a2.getB1() << endl;
return 0;
}
Méthode const
dans une classe
const
dans une classeUne méthode const est une fonction membre d’une classe qui, par le mot-clé const placé à la fin de sa déclaration, garantit qu’elle ne modifie pas les attributs de l’objet. Cela s’applique notamment aux getters, qui sont souvent définis de la manière suivante :
<attributeType> getAttribute() const {
return mAttribute;
}
- Comprenez-vous ce que fait ce code ?
Indices
- L’objet
t
de typeenum
peut prendre les valeursB
(pour “Basic”) ouS
(pour “Standard”). - La classe
b
représente un compte bancaire (bank account). - Elle possède les attributs suivants :
mB1
, qui représente son solde (balance).mT
, qui indique son type (Basic ou Standard).mF
, qui est son frais de retrait (withdrawal fee).
Le code suivant est le constructeur de la classe :
b(double b1, t t1) : mB1(b1), mT(t1) {
switch(t1){
case B:
mF = 4;
break;
case S:
mF = 2;
break;
}
}
- Un objet de la classe
b
est construit avec deux arguments :double b1
, qui est son solde initial, ett t1
, qui définit le type du compte (account type). - Le
switch(t1)
permet de gérer les différents types de comptes.- Si
t1 == B
, le frais de retrait est de 4 euros. - Si
t1 == S
, le frais de retrait est de 2 euros.
- Si
- La méthode
tToString()
permet d’afficher les types sous forme de chaînes de caractères correspondantes.
Quiz : Cette fois, quel est le compte qui dispose du solde le plus élevé ?
Ce code compile et fonctionne comme il se doit, mais il est une véritable atrocité pour les yeux. Il me fait mal au cœur. Il est impératif de renommer ces horreurs.
Avez-vous accompli votre tâche ?
- Avez-vous correctement appliqué la convention camelCase ?
- Avez-vous respecté les conventions de nommage pour les classes et les méthodes ? (Les noms des objets
enum
doivent également commencer par une majuscule.) - Avez-vous suivi les conventions concernant l’utilisation du préfixe
m
(ou_
) et des getters et setters ? - Y a-t-il encore des variables, méthodes ou objets dont le nom se résume à une seule lettre ?
- Des nombres sans signification figurent-ils encore dans les noms ?
- Les noms que vous avez choisis sont-ils suffisamment révélateurs de l’intention derrière chaque variable, méthode ou objet ?
Si vous avez des doutes, n’hésitez pas à revenir sur le cours.
Si vous pensez avoir correctement renommé les éléments, rappelez-vous qu’un bon programmeur devrait être capable d’écrire ce même code correctement dès le départ. Bien sûr, il peut toujours revenir sur son code pour l’améliorer, mais le niveau de code présenté dans cet exercice est tout simplement inacceptable. À la fin de ce cours, vous devez être capable de bien nommer les éléments dès le départ. D’ailleurs, j’ai passé plus de temps à rendre ce code fonctionnel, mais visuellement désastreux, qu’à l’écrire correctement dès le début. Des noms incohérents et flous rendent le code difficile à comprendre et remettent en question sa logique.
J’espère que cet exercice vous a convaincu de l’importance de coder de manière propre 😊.
Recompilez et testez votre code.
Ajoutez un troisième type “Premium” avec un frais de retrait de 1 euro.
Dans la fonction
int main()
, remplacez le code existant par les instructions suivantes :
- Créez trois comptes avec les trois types différents (Basic, Standard et Premium), chacun ayant un solde initial de 100,0 euros.
- Effectuez un dépôt de 50 euros sur chaque compte.
- Effectuez un retrait de 148 euros de chaque compte.
- Affichez les informations (types et soldes) des trois comptes.
- Quiz : Quelle est la somme des soldes des trois comptes ?
Revenez aux objectifs et cochez les points que vous avez maîtrisés. Revenez sur les points que vous n’avez pas encore bien compris. Appelez votre encadrant si besoin.
Git
À la fin de ce TP, votre dépôt distant devrait être structuré comme suit :
- À la racine, vous trouverez les dossiers
TP1/
etTP2/
, ainsi que les fichiers.gitignore
etREADME.md
. Vous pouvez aussi voir le dossier de configuration.git
si vous affichez les dossiers cachés. - Le dossier
TP1/
doit contenir les fichiershello-world.cpp
etmy-first-file.txt
. - Le dossier
TP2/
doit contenir les fichiersclasses-et-methodes.cpp
etmagic-numbers.cpp
Votre répertoire de travail peut contenir des fichiers ignorés, mais veillez à vérifier que votre dépôt sur GitLab est propre, car c’est lui qui sera évalué à la fin du cours !
Si ce n’est pas le cas, retournez à la fin du TP1, où des instructions supplémentaires ont été ajoutées pour vous aider à nettoyer votre dépôt distant.