Quelques raccourcis :

420-KBB-LG – Programmation orientée objet avancée

Ceci est un petit site de support pour le cours 420-KBB-LG – Programmation orientée objet avancée.

Vous trouverez aussi des liens sur divers langages (dont C#, notre outil de prédilection dans ce cours) un peu partout dans http://h-deb.ca/. Portez une attention particulière à ../../../Sujets/Divers--cdiese/index.html.

Les diverses sections de cette page (en fonction desquelles vous trouverez quelques liens dans l'encadré à droite) vous mèneront elles-aussi sur des pistes qui vous permettront d'explorer un peu plus par vous-mêmes, de valider vos acquis et d'enrichir votre apprentissage.

Plan de cours

Pratiques de correction

Je corrige les programmes en appliquant des codes de correction. Vous trouverez ici la liste des codes les plus fréquents.

Ma stratégie de correction en tant que telle (pour le code, à tout le moins) est résumée ici.

Cliquez sur cette cible pour les normes appliquées dans ce cours, en ce qui a trait au pseudocode

Quelques trucs pour demander de l'aide plus efficacement

Détail des séances en classe

Puisque nous serons en quelque sorte laboratoire à la fois pour les séances théoriques et les séances de laboratoire, j'ai fait le choix de construire le cours sous forme de 30 séances (de S00 à S29) plutôt que sous forme de 15 séances théoriques et 15 séances de laboratoire. Le dosage prévu de temps en théorie et de temps en laboratoire (soit environ moitié-moitié) devrait être respecté.

Calendrier par groupe

L'horaire de cette session pour ce cours est d'une grande complexité, du moins pour votre humble serviteur, et ce pour deux raisons :

Pour cette raison, je vais essayer une nouvelle manière de planifier la session, séparant les séances et les dates (jointes aux groupes). Prenez soin d'apprendre votre numéro de groupe pour bien être en mesure de vous repérer.

Séance Groupe 00001 Groupe 00002 Groupe 00003
S00 Lundi 24 août Mercredi 26 août Lundi 24 août
S01 Vendredi 28 août Vendredi 28 août Jeudi 27 août
S02 Lundi 31 août Mercredi 2 sept. Lundi 31 août
S03 Vendredi 4 sept. Vendredi 4 sept. Jeudi 3 sept.

Lundi 7 sept. Fête du travail (jour férié)

S04 Vendredi 11 sept. Mercredi 9 sept. Jeudi 10 sept.
S05 Lundi 14 sept. Vendredi 11 sept. Lundi 14 sept.
S06 Vendredi 18 sept. Mercredi 16 sept. Jeudi 17 sept.
S07 Lundi 21 sept. Vendredi 18 sept. Lundi 21 sept.
S08 Vendredi 25 sept. Mercredi 23 sept. Jeudi 24 sept.
S09 Lundi 28 sept. Vendredi 25 sept. Lundi 28 sept.
S10 Vendredi 2 oct. Mercredi 30 sept. Jeudi 1 oct.

Lundi 5 oct. Élections provinciales (cours suspendus)

S11 Mardi 6 oct. Vendredi 2 oct. Mardi 6 oct.
S12 Vendredi 9 oct. Mercredi 7 oct. Jeudi 8 oct.

Lundi 12 oct. Action de grâce (jour férié)

S13 Vendredi 16 oct. Vendredi 9 oct. Jeudi 15 oct.
S14 Lundi 19 oct. Mercredi 14 oct. Lundi 19 oct.

Mercredi 21 oct. Journée pédagogique (cours suspendus)

Jeudi 22 et vendredi 23 oct. Journées de récupération (cours suspendus)

S15 Lundi 26 oct. Vendredi 16 oct. Lundi 26 oct.
S16 Vendredi 30 oct. Mercredi 28 oct. Jeudi 29 oct.
S17 Lundi 2 nov. Vendredi 30 oct. Lundi 2 nov.
S18 Vendredi 6 nov. Mercredi 4 nov. Jeudi 5 nov.
S19 Lundi 9 nov. Vendredi 6 nov. Lundi 9 nov.
S20 Vendredi 13 nov. Mercredi 11 nov. Jeudi 12 nov.
S21 Lundi 16 nov. Vendredi 13 nov. Lundi 16 nov.
S22 Vendredi 20 nov. Mercredi 18 nov. Jeudi 19 nov.
S23 Lundi 23 nov. Vendredi 20 nov. Lundi 23 nov.
S24 Vendredi 27 nov. Mercredi 25 nov. Jeudi 26 nov.
S25 Lundi 30 nov. Vendredi 27 nov. Lundi 30 nov.
S26 Vendredi 4 déc. Mercredi 2 déc. Jeudi 3 déc.
S27 Lundi 7 déc. Vendredi 4 déc. Lundi 7 déc.
S28 Vendredi 11 déc. Mercredi 9 déc. Jeudi 10 déc.
S29 Lundi 14 déc. Vendredi 11 déc. Lundi 14 déc.

Séances

Le détail des séances en soi suit (le calendrier par groupe est plus haut).

Séance Détails

S00

Au menu :

  • Présentation du cours et du plan de cours
  • Les outils dont nous aurons besoin :
    • Nous utiliserons C# 14.0
    • Nous ferons des projets .NET 10
    • Assurez-vous que votre version de Visual Studio soit à jour!

La raison pour laquelle il est important de mettre vos outils de développement à jour n'est pas vraiment le recours à de nouveaux mécanismes du langage ou de la plateforme, mais bien parce que nous écrirons des programmes faits de plusieurs composants compilés qui devront parfois être de la même technologie.

Conséquemment, si vos outils ne sont pas à jour, vous ne pourrez pas faire certains de vos travaux pratiques.

  • Échange sur le contenu du cours, les modalités, les attentes
    • Il est possible que nous tenions une séance « en ligne » pour « roder la mécanique » au cas où la pandémie mettrait du sable dans l'engrenage (personne ne le souhaite, mais mieux vaut être prudentes et prudents)
  • Réponses aux questions de la classe :
  • Présentation d'une petite activité formative : 420KBB--Consignes-activite-revision.pdf

Si vous souhaitez le code du programme principal à partir duquel vous devrez démarrer, vous pouvez le prendre du fichier PDF ou encore le prendre ci-dessous (parfois, copier / coller d'un PDF, ça donne des résultats suspects) :

// ...
List<Orque> orques = new();
try
{
   for(string s = Console.ReadLine(); "" != s; s = Console.ReadLine())
   {
      orques.Add(new Orque(s));
      Console.WriteLine($"Orque créé : {orques[orques.Count - 1].Nom}");
   }
}
catch(NomInvalideException e)
{
   Console.WriteLine(e.Message);
}
if(Trier(ref orques, out int nbPermutations))
   Console.WriteLine("Les orques ont été entrés en ordre alphabétique");
else
   Console.WriteLine($"Trier les orques a nécessité {nbPermutations} permutations");
Console.Write("La tribu d'orques est :");
foreach (Orque orque in orques)
   Console.Write($" {orque.Nom}");

Après avoir pris un peu de temps pour se « chamailler » avec ce petit défi de remise en forme, je vous ai proposé un peu de code pour vous aider à redémarrer vos instincts de programmeuse et de programmeur. L'accent a été mis sur l'écriture de code simple :

  • Écrire des fonctions
  • Viser « une vocation par fonction »
  • Essayer d'écrire des fonctions qui se limitent à une instruction quand cela s'avère possible
  • Permission accordée d'omettre les accolades d'une structure de contrôle (for, while, if, else) quand le bloc se limite à un seul énoncé (note : ceci est une permission, pas une obligation!)
  • Permission accordée d'avoir deux points de sortie dans une fonction (comme dans Algos.EstVoyelle par exemple) mais seulement si c'est pertinent et si cela permet d'éviter du travail inutile (la paresse et le manque de rigueur seront pénalisés comme à l'habitude)
  • ... et permission accordée de se récompenser quand on y parvient, en se donnant le droit d'utiliser la notation => qui est concise et élégante

Le code produit en classe suit (note : il y a quelques ajouts et ajustements dans ce code en comparaison avec ce que nous avons fait aujourd'hui, mais nous en parlerons à la séance S01) :

Program.cs
//
// code produit pour vous aider et pour amorcer une réflexion avec vous
//
using MonNamespace;
List<Orque> orques = new();
try
{
   for (string s = Console.ReadLine(); "" != s; s = Console.ReadLine())
   {
      orques.Add(new Orque(s));
      Console.WriteLine($"Orque créé : {orques[orques.Count - 1].Nom}");
   }
}
catch (NomInvalideException nie)
{
   Console.WriteLine(nie.Message);
}
if (Trier(ref orques, out int nbPermutations))
   Console.WriteLine("Les orques ont été entrés en ordre alphabétique");
else
   Console.WriteLine($"Trier les orques a nécessité {nbPermutations} permutations");
Console.Write("La tribu d'orques est :");
foreach (Orque orque in orques)
   Console.Write($" {orque.Nom}");

//
//
//
static void Permuter(ref Orque a, ref Orque b)
{
   Orque temp = a;
   a = b;
   b = temp;
}
static bool Trier(ref List<Orque> orques, out int nbPerms)
{
   nbPerms = 0;
   Orque[] orq = orques.ToArray(); 
   for (int i = 0; i < orq.Length - 1; ++i)
      for (int j = i + 1; j < orq.Length; ++j)
         if (!(orq[i].Nom.CompareTo(orq[j].Nom) < 0)) // désordre
         {
            Permuter(ref orq[i], ref orq[j]);
            ++nbPerms;
         }
   orques = orq.ToList();
   return nbPerms == 0;
}
Algos.cs
// using...
namespace MonNamespace
{
   static class Algos
   {
      static char [] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
      public static bool Contient(char [] tab, char c)
      {
         foreach (char ch in tab)
            if (ch == c)
               return true;
         return false;
      }
      public static bool EstVoyelle(char c) =>
         Contient(voyelles, char.ToLower(c));
      public static int CompterVoyelles(string s)
      {
         int n = 0;
         foreach (char c in s)
            if (EstVoyelle(c))
               ++n;
         return n;
      }
      public static bool EstEntreInclusif(int val, int min, int max) =>
         min <= val && val <= max;
   }
}
Orque.cs
using static MonNamespace.Algos;
// ...
class NomInvalideException : Exception;

class Orque
{
   static bool EstNomValide(string nom) =>
      nom != null &&
      EstEntreInclusif(nom.Length, 1, 4) &&
      Algos.CompterVoyelles(nom) <= 1;

   string nom;
   public string Nom
   {
      get => nom;
      private init
      {
         nom = EstNomValide(value) ?
            value : throw new NomInvalideException();
         //if (!EstNomValide(value))
         //   throw new NomInvalideException();
         //nom = value;
      }
   }
   public Orque(string nom)
   {
      Nom = nom;
   }
}

Suggestions de lecture :

S01

Au menu :

  • Quelques améliorations générales à notre solution embryonnaire de S00 pour l'activité formative
  • Accélérer l'exécution du code en réduisant le nombre d'initialisations d'états immuables à travers une variable static

Rappel : un objet est immuable s'il ne peut pas être modifié une fois qu'il a été construit

  • Quelques mots sur l'idée de classe static, qui permet de pallier en partie un manque de certains langages comme C# ou Java
    • Léger allègement syntaxique rendu possible par ce mécanisme

L'idée est que les classes static en C# servent entre autres de palliatif pour l'absence de vraies fonctions dans le langage. En effet, tout comme Java, C# ne supporte que les méthodes, qui sont des fonctions membres d'une classe ou d'une instance, mais ne supporte pas les fonctions en tant que telles, hors d'une classe).

Ainsi, le programme suivant (https://dotnetfiddle.net/OWcfrC) :

using System;
Point p0 = new(),
      p1 = new(1,1);
Console.WriteLine("Distance({0},{1}) == {2}", p0, p1, Distance(p0, p1));
// Note : nous sommes (implicitement) dans la classe Program, donc
// Distance est en fait Program.Distance
static double Distance(Point p0, Point p1) =>
   Math.Sqrt(Math.Pow(p0.X - p1.X, 2) + Math.Pow(p0.Y - p1.Y, 2)); // Notez «Math.» trois fois... et pourquoi?
class Point
{
    public double X { get; init; }
    public double Y { get; init; }
    public Point() : this(0, 0) {}
    public Point(double x, double y)
    {
       X = x;
       Y = y;
    }
}

... offre une méthode Distance qui est un peu verbeuse, mais qui peut s'écrire plus simplement (https://dotnetfiddle.net/CFsrjf) :

using System;
using static System.Math;
Point p0 = new(),
      p1 = new(1,1);
Console.WriteLine("Distance({0},{1}) == {2}", p0, p1, Distance(p0, p1));
// Note : nous sommes (implicitement) dans la classe Program, donc
// Distance est en fait Program.Distance
static double Distance(Point p0, Point p1) =>
   Sqrt(Pow(p0.X - p1.X, 2) + Pow(p0.Y - p1.Y, 2)); // On va à l'essentiel!
class Point
{
    public double X { get; init; }
    public double Y { get; init; }
    public Point() : this(0, 0) {}
    public Point(double x, double y)
    {
       X = x;
       Y = y;
    }
}

... qui est un peu plus léger pour les yeux.

  • Retour sur la petite activité formative proposée à S00
  • Discussion de divers aspects techniques et architecturaux associés à cette activité
  • Avenues de raffinement ou d'optimisation, incluant les questions de réflexion proposées dans l'énoncé
  • Quelques explorations qui nous mèneront vers notre premier travail pratique, le TP00

À titre de référence, le code d'aujourd'hui est à peu près le suivant. Ce code est perfectible; nous ferons bien mieux plus tard dans la session. Je vous laisse le soin d'écrire le programme principal :

Soldat.cs
// ...
class NomInvalideException : Exception;
abstract class Soldat
{
   public string Nom { get; private init; }
   public Soldat(string nom)
   {
      Nom = nom;
   }
   public abstract void Saluer();
}
Orque.cs
// ...
using static MonNamespace.Algos;
class Orque : Soldat
{
   const int LG_NOM_MIN = 1,
             LG_NOM_MAX = 4;
   static bool EstNomValide(string s) =>
      EstEntreInclusif(s.Length, LG_NOM_MIN, LG_NOM_MAX) &&
      CompterVoyelles(s) <= 1;
   static string ValiderNom(string s) =>
      EstNomValide(s) ? s : throw new NomInvalideException();
      public Orque(string nom)
         : base(ValiderNom(nom))
      {
      }
      public override void Saluer()
      {
         Console.WriteLine($"MOI {Nom.ToUpper()}, MOI PUE");
      }
   }
}

En espérant que cela vous soit utile!

Nous avons aussi discuté sommairement du clonage et de l'idiome NVI.

À titre de référence, le code produit lors de cette séance pour démontrer le clonage était :

Image[] images = new Image[]
{
   new Jpeg(ConsoleColor.Magenta),
   new Png(ConsoleColor.Green),
   new Bmp(ConsoleColor.Blue)
};
// non, pas le droit!
//foreach (Image img in images)
//{
//   img.Dessiner();
//   img = ModifierPeutÊtre(img); // <-- ceci serait illégal
//   img.Dessiner();
//}

for (int i = 0; i != images.Length; ++i)
{
   images[i].Dessiner();
   images[i] = ModifierPeutÊtre(images[i]);
   images[i].Dessiner();
}


////////////////////////

static Image ModifierPeutÊtre(Image img)
{
   // 0 : créer un backup
   Image backup = img.Cloner();

   // 1 : modifier img
   img.Teinte = ConsoleColor.Red;

   // 2 : demander si on veut conserver les modifs
   Console.WriteLine("Conserver les modifs? ");
   // 2a : si oui, on retourne img
   // 2b : sinon, on retourne le backup
   if (Console.ReadKey(true).Key == ConsoleKey.O)
      return img;
   return backup;
}


// il existe une interface ICloneable, qui expose une méthode Clone
// ... mais ne l'utilisez pas :) Pour des détails, voir :
// https://codeql.github.com/codeql-query-help/csharp/cs-class-implements-icloneable/
abstract class Image
{
   public ConsoleColor Teinte { get; set; }
   protected Image(ConsoleColor teinte)
   {
      Teinte = teinte;
   }
   // Idiome NVI : non-virtual interface
   public void Dessiner()
   {
      ConsoleColor pre = Console.ForegroundColor;
      Console.ForegroundColor = Teinte;
      DessinerImpl(); // varie selon les enfants
      Console.ForegroundColor = pre;
   }
   protected abstract void DessinerImpl();
   public abstract Image Cloner();
}
class Jpeg : Image
{
   public Jpeg(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Jpeg {Teinte}");
   }
   protected Jpeg(Jpeg autre) : base(autre.Teinte)
   {
   }
   // spécialisation covariante
   public override Jpeg Cloner() => new (this);
}
class Bmp : Image
{
   public Bmp(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Bmp {Teinte}");
   }
   protected Bmp(Bmp autre) : base(autre.Teinte)
   {
   }
   public override Bmp Cloner() => new (this);
}
class Png : Image
{
   public Png(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Png {Teinte}");
   }
   protected Png(Png autre) : base(autre.Teinte)
   {
   }
   public override Png Cloner() => new (this);
}

En fin de séance, j'ai fait une petite activité dirigée d'un système à deux assemblages, soit le code client (qui était une application console, donc un exécutable), et le code serveur (qui était une bibliothèque de classes – une DLL).

Distribution du TP00

Le code auquel nous en sommes arrivés pour le client était le suivant (j'ai pris quelques libertés pour vous divertir) :

using Arsenal;
FabriqueArmes fab = new ();
IArme p = fab.CréerArme(Arsenal.Gravité.violent);
p.Frapper();
p = fab.CréerArme(Arsenal.Gravité.délicat);
p.Frapper();

Le code auquel nous en sommes arrivés pour le serveur était le suivant (encore une fois avec quelques libertés) : 

namespace Arsenal
{
   public interface IArme
   {
      void Frapper(); // rappel : les membres d'une interface sont implicitement publics
   }
   class Masse : IArme // note : par défaut, les membres d'un namespace sont internal
   {
      public void Frapper()
      {
         Console.WriteLine("POURRRRH");
      }
   }
   class Chainsaw : IArme
   {
      public void Frapper()
      {
         Console.WriteLine("FVRRRRRRRR!");
      }
   }
   public enum Gravité { violent, délicat }
   public class FabriqueArmes
   {
      public IArme CréerArme(Gravité grav) =>
         grav == Gravité.délicat ? new Masse() : new Chainsaw();
   }
}

Nous avons ensuite survolé les consignes du TP00.

Si votre serveur pour le TP00 fonctionne correctement, le programme de test suivant...

//
// ... code de test (note : le namespace se nomme Consommateur)
//
using GénérateurId;
using static Consommateur.Tests;

var fab = new FabriqueGénérateurs();
Test(fab, "Séquentiel", "ID", TypeGénérateur.Séquentiel);
Test(fab, "Recycleur", "ID", TypeGénérateur.Recycleur);
Test(fab, "Aléatoire", "ID", TypeGénérateur.Aléatoire, 3);
Test(fab, "Partagé", "ID", TypeGénérateur.Partagé, 3);
Test(fab, "Recycleur", "ID", TypeGénérateur.Recycleur);
Test(fab, "Partagé", "ID", TypeGénérateur.Partagé);
foreach (var (clé, valeur) in fab.ObtenirStatistiques())
   Console.WriteLine($"{clé} a été instancié {valeur} fois");
using GénérateurId;
using System;
using System.Collections.Generic;
//
//
//
static class Tests
{
   public static void Test(FabriqueGénérateurs fab, string nom, string préfixe, TypeGénérateur type)
   {
      IGénérateurId p = fab.Créer(type, préfixe);
      var lst = new List<Identifiant>();
      Console.Write($"{nom}, pige initiale :\n\t");
      for (int i = 0; i != 10; ++i)
      {
         lst.Add(p.Prendre());
         Console.Write($"{lst[lst.Count - 1]} ");
      }
      Console.WriteLine();
      foreach (var n in lst)
         p.Rendre(n);
      Console.Write($"{nom}, pige post-remise :\n\t");
      for (int i = 0; i != 10; ++i)
         Console.Write($"{p.Prendre()} ");
      Console.WriteLine();
   }

   public static void Test(FabriqueGénérateurs fab, string nom, string préfixe, TypeGénérateur type, int germe)
   {
      IGénérateurId p = fab.Créer(type, préfixe, germe);
      var lst = new List<Identifiant>();
      Console.Write($"{nom}, pige initiale :\n\t");
      for (int i = 0; i != 10; ++i)
      {
         lst.Add(p.Prendre());
         Console.Write($"{lst[lst.Count - 1]} ");
      }
      Console.WriteLine();
      foreach (var n in lst)
         p.Rendre(n);
      Console.Write($"{nom}, pige post-remise :\n\t");
      for (int i = 0; i != 10; ++i)
         Console.Write($"{p.Prendre()} ");
      Console.WriteLine();
   }
}
// ...

... devrait donner un affichage comme le suivant (il peut y avoir certaines différences dans les cas « partagé » et « aléatoire », mais il y a des limites à ces différences – le test utilise un germe choisi – alors consultez votre chic prof si vous avez des doutes) :

Séquentiel, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Séquentiel, pige post-remise :
        ID00010 ID00011 ID00012 ID00013 ID00014 ID00015 ID00016 ID00017 ID00018 ID00019
Recycleur, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Recycleur, pige post-remise :
        ID00009 ID00008 ID00007 ID00006 ID00005 ID00004 ID00003 ID00002 ID00001 ID00000
Aléatoire, pige initiale :
        ID19236 ID45716 ID56688 ID13007 ID36732 ID11833 ID16397 ID62078 ID22853 ID24902
Aléatoire, pige post-remise :
        ID32911 ID53056 ID45554 ID01984 ID05379 ID59249 ID08091 ID56063 ID49708 ID31213
Partagé, pige initiale :
        ID19236 ID45716 ID56688 ID13007 ID36732 ID11833 ID16397 ID62078 ID22853 ID24902
Partagé, pige post-remise :
        ID32911 ID53056 ID45554 ID01984 ID05379 ID59249 ID08091 ID56063 ID49708 ID31213
Recycleur, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Recycleur, pige post-remise :
        ID00009 ID00008 ID00007 ID00006 ID00005 ID00004 ID00003 ID00002 ID00001 ID00000
Partagé, pige initiale :
        ID14127 ID38283 ID32702 ID05286 ID65150 ID29877 ID33647 ID35818 ID24609 ID32588
Partagé, pige post-remise :
        ID19316 ID15965 ID33013 ID04178 ID45250 ID58540 ID33793 ID27893 ID56175 ID43324
Séquentiel a été instancié 1 fois
Recycleur a été instancié 2 fois
Aléatoire a été instancié 1 fois
Partagé a été instancié 1 fois

S02

Au menu :

  • Petits trucs pour vous aider à écrire DomaineIdentifiants.Formater : https://dotnetfiddle.net/ESdlzq
  • Retour bref sur les thèmes des séances précédentes :
  • Retour sur l'exercice de créer une bibliothèque à liens dynamiques, et d'une création d'un petit système client / serveur
    • schéma de conception Interface et sa réification en C#
    • implémentation(s) d'une interface
    • schéma de conception Fabrique
    • écriture d'un client pour ce service
    • utilité de ce type d'architecture

Je n'ai pas fait le même exemple dans tous mes groupes, mais l'un de ceux que j'ai faits pour expliquer l'intérêt des interfaces dans un langage comme C# ou Java dans lequel seul l'héritage simple d'implémentation est permis (il n'est pas possible pour une classe d'avoir plus d'une classe parent directe dans ces langages) était la catégorisation d'espèces animales. Rappel : cet exemple est farfelu, et je ne suis pas un biologiste!

Notre premier exemple allait comme suit :

class Animal
{
   // ...
}
class Oiseau : Animal
{
   // ...
}
class Insecte : Animal
{
   // ...
}
class OiseauVolant : Oiseau
{
   public void Voler() {  /* ... */ }
   // ...
}
class OiseauNonVolant : Oiseau
{
   // ...
}
class InsecteVolant : Insecte
{
   public void Voler() {  /* ... */ }
   // ...
}
class InsecteNonVolant : Insecte
{
}

Constatant que cette approche ne me permettait pas de placer ensemble tout ce qui vole (le seul parent commun à OiseauVolant et InsecteVolant est Animal et rien ne permet de croire que tout Animal sait voler!), nous avions retravaillé la hiérarchie comme suit :

class Animal
{
   // ...
}
class Volant : Animal
{
   public void Voler() { /* ... */ } // virtual?
}
class NonVolant : Animal
{
   // ...
}
class OiseauVolant : Volant
{
   // ...
}
class OiseauNonVolant : NonVolant
{
   // ...
}
class InsecteVolant : Volant
{
   // ...
}
class InsecteNonVolant : NonVolant
{
   // ...
}

Cette fois, ceci ne permettait pas de placer ensemble tous les oiseaux (ou tous les insectes). Zut!

Heureusement, C# et Java compensent (en partie) l'absence d'héritage multiple d'implémentation par la possibilité d'implémenter de multiples interfaces. Ainsi, on peut obtenir une solution plus intéressante et plus complète comme suit :

class Animal
{
    ...
}
class Oiseau : Animal
{
    ...
}
class Insecte : Animal
{
    ...
}
interface IVolant
{
   void Voler();
}

class OiseauVolant : Oiseau, IVolant
{
   public void Voler() { /* ... */ }
    ...
}
class OiseauNonVolant : Oiseau
{
    ...
}
class InsecteVolant : Insecte, IVolant
{
   public void Voler() { /* ... */ }
    ...
}
class InsecteNonVolant : Insecte
{
}

Notez que la classe parent doit apparaître avant les interfaces dans la signature d'une classe, et qu'une même classe peut implémenter plusieurs interfaces.

  • Petit rappel sur les générateurs de nombres pseudoaléatoires et sur leur bon usage
const int N = 10_000_000;
const int NB_FACES = 6;
Random dé = new();
int[] lancers = new int[12]; // 0 .. 11, et l'indice 0 contiendra 0
for (int i = 0; i != N; ++i)
{
   int a = dé.Next(1, NB_FACES + 1),
       b = dé.Next(1, NB_FACES + 1);
   // Console.WriteLine($"{a} + {b} == {a+b}");
   lancers[(a + b) - 1]++;
}
Console.WriteLine($"Après {N} lancers de deux dés à {NB_FACES} faces...");
for (int i = 1; i != lancers.Length; ++i)
   Console.WriteLine($"{i + 1} : {lancers[i]}");
  • Dans la perspective (pour le TP00) de la fonction FabriqueGénérateurs.ObtenirStatistiques :
string texte = "..."; // utilisez le texte de votre choix

Dictionary<string, int> fréquence = new();
foreach(string s in texte.Split(new char[] { ' ', '\t', '\r', '\n' }))
   if(s.Trim().Length > 0)
      if(fréquence.ContainsKey(s))
         fréquence[s]++;
      else
         fréquence.Add(s, 1);

foreach (var (mot, n) in fréquence)
   Console.WriteLine($"Le mot {mot} apparaît {n} fois");
  • Travail sur le TP00

S03

Au menu :

  • Comment s'orienter sur le site du cours 🙂
  • Retour sur S02
    • Nombres pseudoaléatoires et l'idée de germe
  • Propriétés en lecture seule ou calculées
    • Propriétés synthétiques (propriétés de deuxième ordre)
    • Écriture concise

Pour un résumé des syntaxes examinées pour les propriétés

using System;
Carré c = new(3); // 3x3

class CôtéInvalideException : Exception;
class Carré
{
   public static int LongueurMax => 1 // note : pas « = »
   // on aurait aussi pu écrire :
   // public static int LongueurMax { get => 1; }
   // ... ou encore :
   // public static int LongueurMax { get; } = 1; // note : ici, il y a une variable cachée
   //                                             // et pas seulement un calcul
   public Carré(int côté, ConsoleColor couleur)
   {
      Côté = côté;
      Couleur = couleur;
   }
   public ConsoleColor Couleur // propriété de premier ordre sans validation
   {
      get; private init;
   }
   int côté;
   public int Côté // propriété de premier ordre avec validation
   {
      get => côté;
      private init
      {
         côté = value > 0 ? value : throw new CôtéInvalideException();
      }
   }
   public int Aire { get => Côté * Côté; } // propriété calculée, version « longue »
   public int Surface => 4 * Côté; // propriété calculée, version « courte »
}

À titre de rappel, chaque notation a son utilité. Par exemple :

enum Lancer { Pile, Face }
class Cenne // pour jouer à pile ou face
{
   Random Dé { get; } = new(); // cool : crée un Random au début de la
                               // vie de l'objet et le conserve par la suite
   // Random Dé => new();     // NOOON! Créerait un Random à chaque get (quelle horreur!)
   public Lancer Jouer() => Dé.Next() % 2 == 0 ? Lancer.Pile : Lancer.Face;
}

Si votre TP00 semble bien fonctionner, voici quelques tests que vous pouvez envisager.

  • Un test validant que vous donnez bel et bien tous les identifiants avec un générateur séquentiel (vous pouvez faire quelque chose de semblable avec un générateur recycleur) :
// ...
IGénérateurId p = new FabriqueGénérateurs().Créer(TypeGénérateur.Séquentiel);
ushort val = p.Prendre().Valeur;
Console.WriteLine($"Premier identifiant pris : {val}"); // devrait être 0
try
{
   for(;;) // boucle infinie (« for ever »)
      val = p.Prendre().Valeur;
}
catch(BanqueVideException)
{
}
Console.WriteLine($"Dernier identifiant pris : {val}"); // devrait être 65535
  • Un test validant que vous donnez bel et bien tous les identifiants avec un générateur aléatoire (ou avec le générateur partagé), et que l'algorithme choisi est raisonnablement rapide :
// ...
IGénérateurId p = new FabriqueGénérateurs().Créer(TypeGénérateur.Aléatoire);
int n = 0;
ushort val = p.Prendre().Valeur;
++n;
try
{
   for(;; ++n) // boucle infinie (« for ever »)
      val = p.Prendre().Valeur;
}
catch(BanqueVideException)
{
}
Console.WriteLine($"Nombre d'identifiants pris : {n}"); // devrait être 65536

S04

Au menu :

  • Remise « papier » du TP00

Par la suite :

  • Comment s'orienter sur le site du cours 🙂
  • À propos du sain usage des exceptions (suite) :
    • pourquoi diverses sortes d'exceptions?
    • coûts des exceptions
      • chemin jamais pris
      • coût d'un throw
      • coût d'un try
      • coût d'un catch
  • Les entiers (int, uint, short, ushort, etc.) et ce qui se passe aux frontières de ces types
    • comprendre le comportement d'un débordement sur un entier signé ou non-signé
  • Démystifier les qualifications d'accès public, protected, private, internal, et pourquoi static n'a rien à voir avec ces mots 🙂
  • Constructeurs de délégation

Quelques tests de vitesse (et pourquoi ces détails sont importants!)

  • Pour les exceptions :
// note : nous avons fait plusieurs variations de ce qui suit
using System;
using System.Diagnostics;
const int N = 100_000_000;
int[] vals = CréerTableau(N);

System.Diagnostics.Stopwatch sw = new();
sw.Start();
int combien = Compter(vals);
sw.Stop();
Console.WriteLine($"Compté {combien} impairs en {sw.ElapsedMilliseconds} ms");


static bool EstPair(int n) => n % 2 == 0;
static bool EstImpair(int n) => !EstPair(n);
static int Compter(int[] vals)
{
   int combien = 0;
   foreach (int n in vals)
   {
      try
      {
         int res = F(n);
         ++combien;
      }
      catch (ZutException)
      {
      }
   }
   return combien;
}
static int F(int n)
{
   if (EstPair(n))
      throw new ZutException();
   return n;
}

static int[] CréerTableau(int n)
{
   int[] tab = new int[n];
   for (int i = 0; i != tab.Length; ++i)
      //if(i % 100 == 0)
      tab[i] = 2 * i;
   //else
   //   tab[i] = 2 * i + 1;
   return tab;
}
class ZutException : Exception;
using System;
using System.Text;
const int N = 100_000_000;
for (int i = 1; i <= N; i *= 10)
{
   System.Diagnostics.Stopwatch sw = new();
   sw.Start();
   string s = CréerTexteMeilleur(i, 'A');
   sw.Stop();
   Console.WriteLine($"Créé une chaîne de longueur {s.Length} en...");
   Console.WriteLine($"\t{sw.ElapsedMilliseconds} ms");
}
static string CréerTexteLent(int n, char c)
{
   string s = "";
   for (int i = 0; i != n; ++i)
      s += c;
   return s;
}
static string CréerTexteRapide(int n, char c)
{
   StringBuilder sb = new();
   for (int i = 0; i != n; ++i)
      sb.Append(c);
   return sb.ToString();
}
static string CréerTexteMeilleur(int n, char c)
{
   return new(c, n);
}
  • Ajouter des éléments dans une List<T> :
using System;
using System.Diagnostics;
const int N = 100_000_000;
for(int i = 1; i <= N; i *= 10)
{
   List<int> lst = new();
   Stopwatch sw = new();
   sw.Start();
   for (int j = 0; j != i; ++j)
      lst.Add(3); // 3 est une valeur quelconque; c'est pas important laquelle ici
   sw.Stop();
   Console.WriteLine($"Ajout à la fin, {i} éléments : {sw.ElapsedMilliseconds} ms");
   sw = new();
   sw.Start();
   for (int j = 0; j != i; ++j)
      lst.Insert(0, 3); // 3 est une valeur quelconque; c'est pas important laquelle ici
   sw.Stop();
   Console.WriteLine($"Ajout au début, {i} éléments : {sw.ElapsedMilliseconds} ms");
}

N'oubliez pas de remettre TP00 en format imprimé au début de la séance

S05

Au menu :

  • Q00
    • Sujet : bases de programmation orientée objet avec C# (constructeurs, propriétés, invariants, immuabilité ou pas, etc.)
  • On fait le TP00
  • Introduction à la sérialisation (avec format JSON, qui est le format en vogue en ce moment)
  • Démystifier les qualifications d'accès public, protected, private, internal, et pourquoi static n'a rien à voir avec ces mots 🙂
  • On présente le TP01
  • Travail sur le TP01

Le programme principal imposé pour le TP01 est le suivant : TP01-Program.cs

Note : pour certains aspects du TP01, vous aurez besoin d'informations qui seront présentées à la séance S06 alors commencez par écrire les classes pour lesquelles vous savez déjà ce qu'il est nécessaire de savoir!

L'exemple de consommation d'un fichier JSON vu en classe était :

using System.Text.Json;
using System.Text.Json.Serialization;

string s = "";
using (StreamReader sr = new("../../../fichier.json"))
{
   for (string str = sr.ReadLine(); str != null; str = sr.ReadLine())
      s += $"{str}\n";
}
Caractères cars = JsonSerializer.Deserialize<Caractères>(s);
Console.WriteLine($"Nom : {cars.NomSurface}");
foreach (Caractère c in cars.Cars)
{
   ConsoleColor avant = Console.ForegroundColor;
   Console.ForegroundColor = c.Couleur;
   Console.WriteLine($"Symbole {c.Symbole}");
   Console.ForegroundColor= avant;
}


//
// classes représentant les types de données lus
//
class Caractère
{
   public char Symbole { get; set; }
   [JsonConverter(typeof(JsonStringEnumConverter))]
   public ConsoleColor Couleur { get; set; }
}
class Caractères
{
   public string NomSurface { get; set; }
   public List<Caractère> Cars { get; set; }
}

... et le fichier JSON lui-même était :

{
  "NomSurface": "Surf",
  "Cars": [
    {
      "Symbole": "$",
      "Couleur": "Red"
    },
    {
      "Symbole": "*",
      "Couleur": "Blue"
    }
  ]
}

S06

Au menu :

Pour une implémentation des schémas de conception Singleton et Observateur, les deux à travers un gestionnaire de clavier, voir ceci (qui ressemble à ce que nous avons fait en classe) :

var ges = GesClavier.GetInstance();
var croc = new CroqueMort();
ges.Abonner(new Terminateur(croc));
ges.Abonner(new ÀDroite(ConsoleKey.D));
ges.Abonner(new EnHaut(ConsoleKey.W));
ges.Abonner(new ÀGauche(ConsoleKey.A));
ges.Abonner(new EnBas(ConsoleKey.S));
// ges.Abonner(new Afficheur());
while (!croc.Terminé)
   ges.Exécuter();

//
// Deux schémas de conception (Design Patterns)
// ce matin : Singleton, Observateur
//
interface IRéactionClavier
{
   void Réagir(ConsoleKeyInfo clé);
}
class Afficheur : IRéactionClavier
{
   public void Réagir(ConsoleKeyInfo clé)
   {
      Console.Write(clé.KeyChar);
   }
}
interface ISignalFin
{
   void Signaler();
   bool Terminé();
}
class CroqueMort : ISignalFin
{
   bool Fin { get; set; } = false;
   public void Signaler() => Fin = true;
   public bool Terminé => Fin;
}
class Terminateur : IRéactionClavier
{
   ISignalFin Signaleur { get; init; }
   public Terminateur(ISignalFin p)
   {
      Signaleur = p;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == ConsoleKey.Q /*ConsoleKey.Escape*/)
         Signaleur.Signaler();
   }
}
class GesClavier
{
   List<IRéactionClavier> Abonnés { get; } = new();
   public void Abonner(IRéactionClavier p)
   {
      Abonnés.Add(p);
   }
   GesClavier() // note : privé
   {
   }
   public static GesClavier Get { get; } = new();
   public void Exécuter()
   {
      var clé = Console.ReadKey(true);
      foreach (var p in Abonnés)
         p.Réagir(clé);
   }
}
//
// diantre, beaucoup de répétition de code n'est-ce
// pas? mais on va dormir là-dessus pour le moment :)
//
class ÀDroite : IRéactionClavier
{
   ConsoleKey Touche { get; init; }
   public ÀDroite(ConsoleKey clé)
   {
      Touche = clé;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == Touche)
         Console.WriteLine("Est");
   }
}
class EnHaut : IRéactionClavier
{
   ConsoleKey Touche { get; init; }
   public EnHaut(ConsoleKey clé)
   {
      Touche = clé;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == Touche)
         Console.WriteLine("Nord");
   }
}
class ÀGauche : IRéactionClavier
{
   ConsoleKey Touche { get; init; }
   public ÀGauche(ConsoleKey clé)
   {
      Touche = clé;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == Touche)
         Console.WriteLine("Ouest");
   }
}
class EnBas : IRéactionClavier
{
   ConsoleKey Touche { get; init; }
   public EnBas(ConsoleKey clé)
   {
      Touche = clé;
   }
   public void Réagir(ConsoleKeyInfo clé)
   {
      if (clé.Key == Touche)
         Console.WriteLine("Sud");
   }
}

Pour un exemple connexe avec délégués :

GesClavier ges = GesClavier.Get;
SignalArrêt signal = new();
ges.Abonner(new Magie().Wow);
ges.Abonner(new Assassin(signal).Réagir);
ges.Abonner(new Afficheur().Réagir);
ges.Abonner(new Est().Réagir);
ges.Abonner(new Nord().Réagir);
ges.Abonner(new Ouest().Réagir);
ges.Abonner(new Sud().Réagir);
while (!signal.Valeur)
{
   ges.Exécuter();
}
class SignalArrêt
{
   public bool Valeur { get; set; } = false;
}
delegate void RéactionTouche(char c);
class Magie
{
   public void Wow(char c)
   {
      // ...
   }
}
class Assassin
{
   SignalArrêt Signal { get; init; }
   public Assassin(SignalArrêt signal)
   {
      Signal = signal;
   }
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 'q')
         Signal.Valeur = true; //Environment.Exit(0);
   }
}
class Afficheur
{
   public void Réagir(char c)
   {
      Console.WriteLine(c);
   }
}
class Est
{
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 'd')
         Console.WriteLine("Vers l'Est");
   }
}
class Nord
{
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 'w')
         Console.WriteLine("Vers le Nord");
   }
}
class Ouest
{
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 'a')
         Console.WriteLine("Vers l'Ouest");
   }
}
class Sud
{
   public void Réagir(char c)
   {
      if (char.ToLower(c) == 's')
         Console.WriteLine("Vers le Sud");
   }
}

class GesClavier
{
   List<RéactionTouche> Abonnés { get; } = new();
   public void Abonner(RéactionTouche /*IRéactionTouche*/ p)
   {
      Abonnés.Add(p);
   }
   public void Exécuter()
   {
      char c = Console.ReadKey(true).KeyChar;
      foreach (RéactionTouche f in Abonnés)
         f(c);
   }
   public static GesClavier Get { get; } = new();
   GesClavier() // note : privé
   {
   }
}
  • Travail sur le TP01

S07

Au menu : à venir

S08

Au menu : à venir

S09

Au menu : à venir

S10

Au menu : à venir

S11

Au menu : à venir

S12

Au menu : à venir

S13

Au menu : à venir

S14

Au menu : à venir

S15

Au menu : à venir

S16

Au menu : à venir

S17

Au menu : à venir

S18

Au menu : à venir

S19

Au menu : à venir

S20

Au menu : à venir

S21

Au menu : à venir

S22

Au menu : à venir

S23

Au menu : à venir

S24

Au menu : à venir

S25

Au menu : à venir

S26

Au menu : à venir

S27

Au menu : à venir

S28

Au menu : à venir

S29

Au menu :

  • Chic examen final
    • Vous avez droit à toute documentation écrite (pas d'ordinateur, pas d'IA générative, pas d'ami(e)s, pas d'Internet)

Petits coups de pouces

Vous trouverez ici quelques documents, la plupart petits, qui peuvent vous donner un petit coup de pouce occasionnel.

Vous trouverez aussi des exemples de code C# dans la section Divers – C# du site, mais notez que je n'ai pas nécessairement harmonisé ces exemples (écrits pour des cours plus avancés, sous forme de survols) aux standards de programmation appliqués dans le présent cours. À lire avec prudence et discrimination, donc.

Consignes des travaux pratiques

Les consignes des travaux pratiques suivent.

Consignes Détails supplémentaires À remettre...

Activité de révision

Voir S00 et S01

s/o

TP00

Du code de test vous a été proposé à la séance S01

Au début de la séance S04 en format imprimé

TP01

Le code du programme principal est disponible sur TP01-Program.cs. Vous devez l'utiliser intégralement (vous n'avez pas le droit de le modifier de quelque façon que ce soit).

Le fichier config.json vous appartient, mais vous pouvez utiliser ceci pour commencer :

{
  "Surface": {
    "Hauteur": 10,
    "Largeur": 20,
    "Abeilles": 3,
    "Décorations": 30
  },
  "Abeille": {
    "Symbole": "A",
    "Couleur": "Blue"
  },
  "Alvéole": {
    "Symbole": "@",
    "Couleur": "Red"
  },
  "Miel": {
    "Symbole": "M",
    "Couleur": "Yellow"
  },
  "Vide": {
    "Symbole": ".",
    "Couleur": "Green"
  }
}

Au début de la séance S11 en format imprimé

TP01b

À venir

À venir

TP02

À venir

À venir

TP03

À venir

À venir

PFI

   

Solutionnaires

Solutionnaire de la classe Messagerie proposée à la séance S17 :

using System;
using System.Collections.Generic;

namespace ActivitéArdoise
{
   class HauteurInvalideException : Exception { }
   class Messagerie : IObservateurArdoise
   {
      Dictionary<string, List<string>> Messages { get; } = new();
      Dictionary<string, ConsoleColor> Couleurs { get; } = new();
      static readonly ConsoleColor[] couleurs =
      {
         ConsoleColor.Red, ConsoleColor.Green, ConsoleColor.Blue,
         ConsoleColor.Yellow, ConsoleColor.Cyan, ConsoleColor.Magenta
      };
      int Hauteur { get; init; }
      public Messagerie(int hauteur)
      {
         if (hauteur <= 1) throw new HauteurInvalideException();
         Hauteur = hauteur;
      }
      public void Nouveauté(string qui, string quoi)
      {
         lock (this)
         {
            if (Messages.ContainsKey(qui))
            {
               Messages[qui].Add(quoi);
               if (Messages[qui].Count > Hauteur)
                  Messages[qui].RemoveAt(0);
            }
            else
            {
               Messages.Add(qui, new());
               Couleurs.Add(qui, couleurs[Messages.Count % couleurs.Length]);
            }
         }
      }
      public void Afficher()
      {
         int[] largeurs;
         string[] noms;
         List<string> [] messages;
         lock (this)
         {
            largeurs = new int[Messages.Count];
            noms = new string[Messages.Count];
            messages = new List<string>[Messages.Count];
            int i = 0;
            foreach (var (qui, msgs) in Messages)
            {
               noms[i] = qui;
               largeurs[i] = Algos.Cumuler(
                  msgs, (cur, s) => Math.Max(cur, s.Length), noms[i].Length
               );
               messages[i] = new(msgs);
               ++i;
            }
         }
         // titres
         Console.Clear();
         for (int i = 0; i != largeurs.Length; ++i)
            Console.Write(noms[i].PadRight(largeurs[i]));
         Console.WriteLine();
         // messages
         for (int j = 0; j != Hauteur; ++j)
         {
            for (int i = 0; i != largeurs.Length; ++i)
            {
               string nom = noms[i];
               if (j < messages[i].Count)
               {
                  ConsoleColor pré = Console.ForegroundColor;
                  Console.ForegroundColor = Couleurs[nom];
                  Console.Write(messages[i][j].PadRight(largeurs[i]));
                  Console.ForegroundColor = pré;
               }
               else
                  Console.Write(new string(' ', largeurs[i]));
            }
            Console.WriteLine();
         }
      }
   }
}

En espérant que ça vous aide à organiser vos idées!


Valid XHTML 1.0 Transitional

CSS Valide !