Skip to content

Quelques caractéristiques de la POO

Classes abstraites et Interfaces

  • Une classe abstraite ou une interface ne peuvent être instanciés sauf via un objet anonyme (exemple en TS).
  • Les classes abstraites et interfaces sont considérés comme des contrats.
  • Une interface liste des membres sans implémentation.
  • Une classe abstraite liste des membres avec ou sans implémentation.
  • Une classe (abstraite ou non) peut hériter d'une seule classe (abstraite ou non) et de plusieurs interfaces.
  • ⚠ Certaines langages autorisent l'héritage multiple de classes.
  • 💡 En typescript, un objet est compatible avec tout object qui a les mêmes champs sans avoir à explicitement implémenter son interface ou classe. On dit que c'est du duck typing; si ça marche et se comporte comme un canard, alors c'est un canard.
Classes abstraites et Interfaces
interface Shooter {
  readonly shootPower: number;
  shoot(): void;
}
interface Transporter {
  readonly numberOfPeople: number;
}
abstract class Ship {
  constructor(protected speed: number) {
    console.log("Ship constructor");
  }
  abstract move(): void;
  sayHello() {
    console.log("Hello");
  }
}

class SpaceShip extends Ship implements Shooter, Transporter {
  constructor(
    speed: number,
    readonly staelliteCount: number,
    readonly numberOfPeople: number,
    readonly shootPower: number
  ) {
    console.log("SpaceShip constructor before super");
    super(speed);
    console.log("SpaceShip constructor after super");
  }
  move(): void {
    console.log("VROOOM");
  }
  shoot(): void {
    console.log(`Shooting with power ${this.shootPower}`);
  }
}

const spaceShip = new SpaceShip(1000, 5, 500, 10);
spaceShip.shoot();
console.log("The space ship has ", spaceShip.staelliteCount, " sattelites");

function checkPower(shooter: Shooter) {
  console.log("The shooter has power of", shooter.shootPower);
}

checkPower(spaceShip);

const literalShooter: Shooter = {
  shootPower: 6,
  shoot() {
    console.log("pew pew");
  },
};
literalShooter.shoot();
checkPower(literalShooter);

Objets littéraux

  • Certains langages permettent de créer des objets sans instancier une classe (non abstraite)
  • Les propriétés et méthodes sont données directement lors de la définition de l'objet.
Objets littéraux
const ship1 = {
  price: 20000,
  name: "The decennial Eagle",
  shootPower: 6,
  shoot() {
    console.log("pew pew");
  },
};

ship1.shoot();

interface Shooter {
  readonly shootPower: number;
  shoot(): void;
}
function checkPower(shooter: Shooter) {
  console.log("The shooter has power of", shooter.shootPower);
}

checkPower(ship1);

const serpent = {
  shootPower: 1,
  shoot() {
    console.log("pew pew");
  },
};

checkPower(serpent);

const cat = {
  name: "chat-pardeur",
};

// checkPower(cat); // fails

Propriétés et accesseurs

  • Une propriété permet d'accéder et / ou modifier une donnée de l'objet avec la syntaxe objet.propriété
  • Quand un propriété est utilisée en lecture, l'objet appelle une méthode qui retourne la valeur de la propriété. Cette méthode est appelée getter
  • Quand on affecte une valeur à une propriété, l'objet appelle une méthode qui modifie la valeur propriété. Cette méthode est appelée setter
  • Les getters et setters sont appelées accesseurs
  • 💡 Certains langages gèrent nativement les accesseurs
  • Dans la plupart des cas, une propriété repose sur un variable privée de la classe.
  • ⭐ On appelle ce genre de champ, un backing field
  • Les langages qui gèrent nativement les propriétés utilisent un backing field par défaut et nous permettent de personnaliser les accesseurs par la suite.
  • Les langages qui gèrent moins bien les propriétés laissent au développeur le soin de prévoir des méthodes getPropriété et setPropriété en avance.
Propriétés
class Mage {
  // Deux propriétés dont le backing field est généré automatiquement
  constructor(public name: string, public hp: number) {}
}

// La propriété name a des accesseurs peronnalisés
class OtherMage {
  constructor(name: string, public hp: number) {
    this._name = name;
  }

  // backing field
  private _name: string;

  get name() {
    console.log("🧹 getting name", this._name);
    return this._name;
  }

  set name(value) {
    console.log("🧙 setting name with new value", value);
    this._name = value.toLowerCase();
    console.log("🧙🏾 name with new value", this._name);
  }
}

const mage = new Mage("magus", 10);
mage.name = "magicus";
console.log(mage.name);

const otherMage = new OtherMage("sorcellus", 100);
otherMage.name = "sorcellicus";
console.log(otherMage.name);

Membres statiques

  • Un membre statique d'une classe est utilisable sans créer d'instance particulière
  • On peut appeler ou accéder à un membre statique depuis le nom de la classe. Certains langages permettent d'y accéder aussi depuis une instance.
  • Les membres statiques peuvent être considérées comme des variables et fonctions globales, sauf qu'elles sont classées dans une classe
Propriétés
// Méthode globale (pas conseillée si on veut suivre l'orienté objet)
export function subsctract(x: number, y: number): number {
  return x - y;
}

export const gravity = 10;

export class MathUtils {
  // doit être appelé depuis la classe
  static readonly PI = 3.14;
  // doit être appelé depuis la classe
  static add(x: number, y: number): number {
    return x + y;
  }
  // doit être appelé depuis une instance
  multiply(x: number, y: number): number {
    return x * y;
  }
}

console.log(MathUtils.PI);
console.log(MathUtils.add(2, 5));

const mu = new MathUtils();
console.log(mu.multiply(2, 5));

// erreur
//console.log(MathUtils.multiply(2, 5));

// On ne peut pas appeler du statique depuis un objet en TS
// console.log(mu.PI);
// console.log(mu.add(2, 5));

Polymorphisme

  • Consiste à donner des comportement différents à une méthode selon la classe qui l'implémente et quand on appelle la méthode depuis la classe mère, on aura la méthode de sa classe concrète qui sera appelée
Propriétés
abstract class Shape {
  abstract get area(): number;
  showName() {
    console.log("shape");
  }
}

abstract class TwoDimensionShape extends Shape {
  // surchage (ou override) de showName
  showName() {
    console.log("TwoDimensionShape");
  }
}

class Rectangle extends TwoDimensionShape {
  constructor(readonly width: number, readonly height: number) {
    super();
  }

  get area() {
    return this.width * this.height;
  }

  showName() {
    console.log("Rectangle");
  }
}

class Line extends Shape {
  get area() {
    return 0;
  }

  showName() {
    console.log("Line");
  }
}

const s: Shape = new Rectangle(10, 100);
console.log(s.area);
s.showName();

const shapes: Shape[] = [new Line(), new Rectangle(40, 80), new Line()];
// Si TS n'était pas polymorphe
for (const shape of shapes) {
  if (shape instanceof Line) {
    // casting: changer le type
    (shape as Line).showName();
  } else if (shape instanceof Rectangle) {
    (shape as Rectangle).showName();
  }
}

// mais comme TS est polymorhpe
for (const shape of shapes) {
  shape.showName();
}

Exercices

Exo 1

Dans cet exercice, on manipule des formes géométriques que l'on définit par la classe abstraite GeometricShape. Cette dernière déclare deux méthodes abstraites computeArea() qui renvoie un nombre et computePerimeter() qui retourne un nombre.

  1. Définir les classes Rectangle, Square et Circle qui héritent de la classe GeometricShape.
  2. Définir la classe Drawing (dessin en Anglais) qui contient une liste de formes géométriques.
  3. Créer un programme qui instancie deux dessins avec chacun des formes géométriques différentes (4 formes environ par dessin).

Exo 2

Un fermier veut modéliser ses animaux via leurs caractéristiques. Certain animaux peuvent crier, d’autres sont muets. On représentera le fait de crier au moyen d’une méthode affichant à l’écran le cri de l’animal.

  1. Ecrire une interface Shouter (crieur en Anglais) contenant la méthode permettant de crier qu'on appellera shout().
  2. Ecrire les classes des chats Cat, des chiens Dog et des lapins Rabbit (qui sont muets)
  3. Ecrire un programme avec un tableau pour les animaux qui savent crier, le remplir avec 1 chiens et deux chats, puis faire crier tous ces animaux. Décrire ce qui s’affiche à l’écran à l’exécution de ce programme.
  4. L'éleveur donne un nom à chacun de ses animaux. Créer la classe abstraite Animal avec la propriété name qui est la classe mère de tous les animaux.
  5. Créer une classe Farmer (fermier en Français) qui contient un tableau d'animaux passé dans le constructeur.
  6. Créer une instance de la classe Farmer en lui passant tableaux d'animaux contenant trois chiens, deux chats et trois lapins.
  7. Le fermier souhaite retrouver facilement ses animaux pas leurs caractéristiques. Créer une méthode findShoutingAnimals() qui retourne un tableau des animaux du fermier qui crient (astuce: utiliser if ("shout" in animal) pour savoir si un animal peut crier).
    • Pour info, if ("shout" in animal) n'est pas super propre mais c'est la méthode la plus simple pour débuter.
  8. Le fermier souhaite passer par une propriété calculée à la place de la méthode de la question précédente. Créer une propriété calculée get shoutingAnimals() qui retourne le même résultat que findShoutingAnimals().
  9. Afficher le résultat de la méthode findShoutingAnimals() et la valeur de la propriété get shoutingAnimals(). Quelle syntaxe préférez-vous ?
  10. Le fermier élève également un Canari (Canary en Anglais). Il souhaite en profiter pour modéliser les caractéristiques voler et marcher de ses animaux. Définir la classe Canary ainsi que les interfaces Walker et Flyer. Mettre à jour les classes existantes pour implémenter les interfaces qui leurs correspondent.
  11. Créer une propriété calculée get FlyingAnimals() qui retourne une liste d'animaux qui volent.

Exo 3

  1. Créer une classe Station qui a deux champs en readonly: id de type number et name de type string.
  2. Le constructeur ne prend que name en argument et id est généré via un compteur qui est incrémenté à chaque nouvelle instance créé (astuce, utiliser une propriété statique)
  3. créer trois stations et vérifier que leur id est bien positionné.
  4. définir une méthode isEqualTo(s: Station) qui retourne true si la station passée en argument à le même id
  5. Faire quelques appels de isEqualTo sur différentes stations.

Exo 4

Créer une classe StringUtils qui définit les méthodes statiques suivantes:

  1. askQuestion(message: string): string: qui retourne une nouvelle chaîne de caractères qui est une concaténation un "?" à message
  2. countVowels(message: string): number qui retourne le nombre de voyelles dans message
  3. removeVowels(message: string): string qui retourne une nouvelle chaîne de caractères qui est message dépourvu de ses voyelles (en gardant l'ordre des lettres initiales)

Sources