GLO-4002 - Site du cours 2023

Temps estimé: 60 minutes

Exercices préparatoires II

Question 1

Écrivez le code de la classe testée par la classe de test suivante.

RegistrarTest.java
public class RegistrarTest {

    private ElectronicStudentRecord aStudentRecord;
    private ElectronicStudentRecord anotherStudentRecord;

    @BeforeEach
    public void setupStudentRecords() {
        aStudentRecord = mock(ElectronicStudentRecord.class);
        anotherStudentRecord = mock(ElectronicStudentRecord.class);
    }

    @Test
    public void givenStudentRecords_whenPublishingGrades_thenPublishAllStudentRecordGrades() {
        List<ElectronicStudentRecord> studentRecords = Arrays.asList(aStudentRecord, anotherStudentRecord);
        Registrar registrar = new Registrar(studentRecords);

        registrar.publishGrades();

        studentRecords.forEach(studentRecord -> verify(studentRecord).publishGrades());
    }
}
Registrar.java
public class Registrar {

    private final List<ElectronicStudentRecord> studentRecords;

    public Registrar(List<ElectronicStudentRecord> studentRecords) {
        this.studentRecords = studentRecords;
    }

    public void publishGrades() {
        for (ElectronicStudentRecord studentRecord : studentRecords) {
            studentRecord.publishGrades();
        }
    }
}

Question 2

Est-ce que la classe suivante respecte le Single-Reponsibility Principle (SRP) et l'Open/Closed Principle (OCP)? Dans tous les cas, justifiez pourquoi.

GroupClass.java
public class GroupClass {

    private final List<Course> courses = new ArrayList<>();
    private final List<Student> subscribedStudents = new ArrayList<>();

    public void addCourse(Course course) {
        this.courses.add(course);
    }

    public void subscribeStudent(Student student) {
        subscribedStudents.add(student);
    }

    public void calculateGrades() {
        if (courses.size() > 0) {
            Map<Student, Double> finalGradesPerStudents = new HashMap<>();

            for (Course course : courses) {
                for (Exam exam : course.getExams()) {
                    Student student = exam.getStudent();
                    double grade = exam.getGrade();
                    double weight = course.getWeight();
                    double weightedGrade = grade * weight;

                    student.getRecord().addWeightedGrade(course.getId(), weightedGrade);
                    finalGradesPerStudents.merge(student, weightedGrade, (a, b) -> Double.sum(a, b));
                }
            }

            for (Student student : subscribedStudents) {
                double finalGrade = finalGradesPerStudents.getOrDefault(student, 0.0);
                student.getRecord().setFinalGrade(finalGrade);
            }
        }
    }
}

Est-ce qu'elle respecte le SRP?

Faux: La méthode calculateGrades s'occupe du calcul des notes pondérés en plus d'inscrire les notes finales dans les bulletins étudiants, ce qui lui donne deux raisons de changer.

Est-ce qu'elle respecte l'OCP?

La réponse que vous donnée est importante, mais votre justification l'est encore plus.

Il faut comprendre que l'OCP demande aux entités d'être ouvertes à l'extension, mais fermées au changement.

Il faut donc évaluer le risque de changement de GroupClass. Voici des exemples de questions à se poser qui peuvent vous aider:

1. Est-ce qu'elle fait bien son rôle?

Même si la classe publit les scores des étudiants correctement, il est probable qu'on veuille un jour utiliser une autre technique de calcul de pondération des notes (ex. nouvelles normes universitaires).

Pour permetre l'utilisation de différents calculs de notes (et régler ce problème d'OCP), une solution intéressante serait d'utiliser le patron de conception Stratégie.

2. Est-ce qu'elle est isolée?

Cette question nous permet de remarquer que GroupClass a un fort couplage avec les classes Student, Course et Exam, car elle connait leurs implémentations.

Un changement dans une de ces dépendances risquent donc fortement d'impacter GroupClass. Surtout, considérant qu'elle utilisent leurs propriétés directement, entre autres dans le calcul des notes pondérées.

Conclusion

En plus de ne pas avoir de moyen d'extensionner ses deux responsabilités (calcul des notes pondérés et inscription des notes finales), elle est très prompte aux changements.

Tous ces éléments suggères que l'entité GroupClass ne respecte pas l'OCP.

⚠️ Si vous avez répondu que l'OCP est respecté ⚠️

Il est plus facile de perdre des points. Cependant, assurez-vous d'argumenter en mentionant les éléments suivants:

  • Il est très peu probable qu'on demande de changer le calcul de pondération des notes, car la technique utilisée est optimisée au maximum.
  • La classe est tellement liée avec ses dépendances que tout changement, nécessairement, forcera la modification de l'ensemble des classes, donc ça n'apporterait rien de modifier ce code.

Ces explications sont tirées par les cheveux, mais ça démontre une certaine compréhension de l'OCP.

Question 3

Réglez tous les problèmes de Tell-Don't-Ask de la classe GroupClass, vu dans l'exercice précédent. Pour vous aidez, on vous fournit les classes modifiables suivantes:

Course.java
public class Course {

    private final String id;
    private final double weight;
    private final Map<Student, Exam> exams = new HashMap<>();

    public Course(String id, double weight) {
        this.id = id;
        this.weight = weight;
    }

    public String getId() {
        return id;
    }

    public double getWeight() {
        return weight;
    }

    public List<Exam> getExams() {
        return new ArrayList<>(exams.values());
    }

    public void addExam(Exam exam) {
        exams.put(exam.getStudent(), exam);
    }
}
Student.java
public class Student {

    private final ElectronicStudentRecord studentRecord;

    public Student(ElectronicStudentRecord studentRecord) {
        this.studentRecord = studentRecord;
    }

    public ElectronicStudentRecord getRecord() {
        return studentRecord;
    }
}
ElectronicStudentRecord.java
public interface ElectronicStudentRecord {

    void addWeightedGrade(String id, double weightedGrade);
    void setFinalGrade(double finalGrade);
    void publishGrades();
}
Exam.java
public class Exam {

    private final double grade;
    private final Student student;

    public Exam(double grade, Student student) {
        this.grade = grade;
        this.student = student;
    }

    public double getGrade() {
        return grade;
    }

    public Student getStudent() {
        return student;
    }
}
GroupClass.java
public class GroupClass {

    private final List<Course> courses = new ArrayList<>();

    public void addCourse(Course course) {
        this.courses.add(course);
    }

    public void calculateGrades() {
        for (Course course : courses) {
            course.calculateGrades();
        }
    }
}
Course.java
public class Course {

    private final String id;
    private final double weight;
    private final List<Exam> exams = new ArrayList<>();
    private final List<Student> subscribedStudents = new ArrayList<>();

    public Course(String id, double weight) {
        this.id = id;
        this.weight = weight;
    }

    public void addExam(Exam exam) {
        exams.add(exam);
    }

    public void subscribeStudent(Student student) {
        subscribedStudents.add(student);
    }

    public void calculateGrades() {
        for (Exam exam : exams) {
            exam.calculateWeightedGrade(id, weight);
        }

        for (Student student : subscribedStudents) {
            student.calculateFinalGrade();
        }
    }
}
Exam.java
public class Exam {

    private final double grade;
    private final Student student;

    public Exam(double grade, Student student) {
        this.grade = grade;
        this.student = student;
    }

    public void calculateWeightedGrade(String id, double weight) {
        double weightedGrade = grade * weight;
        student.calculateWeightedGrade(id, weightedGrade);
    }
}
Student.java
public class Student {

    private final ElectronicStudentRecord studentRecord;

    private double finalGrade = 0;

    public Student(ElectronicStudentRecord studentRecord) {
        this.studentRecord = studentRecord;
    }

    public void calculateWeightedGrade(String id, double weightedGrade) {
        studentRecord.addWeightedGrade(id, weightedGrade);
        finalGrade += weightedGrade;
    }

    public void calculateFinalGrade() {
        studentRecord.setFinalGrade(finalGrade);
    }
}

Il n'y pas de secret. Il faut y aller étape par étape:

  • Déplacer toute la logique de GroupClass dans Course.
  • On remarque que la liste des étudiants inscrits n'est plus utile à GroupClass. On peut donc la déplacer à Course.
  • Déplacer la logique de calcul des notes pondérees de Course dans Exam, et ensuite d'Exam vers Student.
  • On remarque que la map d'examens dans Course n'est plus nécessaire, car un examen connait déjà son étudiant. Nous pouvons donc la transformer en liste.
  • Déplacer la logique de publication des notes finales de Course dans Student.
  • Notez qu'il faut ajouter une variable dans Student pour cumuler les notes d'examens, avec un défaut de 0 si l'étudiant a passé aucun examen.

Pour savoir vers où déplacer la logique, fiez-vous aux Getters.

Si vous ne comprenez pas la réponse, continuez de vous pratiquer et faites les exercices sur le TDA de la semaine 4.