Skip to content

Instantly share code, notes, and snippets.

@Naedri
Last active September 12, 2023 12:02
Show Gist options
  • Save Naedri/dfe3893354b0618f0ca5b5b7347342dd to your computer and use it in GitHub Desktop.
Save Naedri/dfe3893354b0618f0ca5b5b7347342dd to your computer and use it in GitHub Desktop.
Patrons de conception par modularité et typage.

Design Patterns by Modularity and Typing

Dans le cadre d'un cours sur les architectures logicielles à l'IMT, il a été décrit la notion de typage (Curry-Howard correspondence) permettant de développer des applications aux composants modulaires, sur la base de patrons de conception.

Ressources utiles

Deux correspondances pour les types

D'un point de vue logique

La correspondance dite de Curry-Howard s'appuie sur la théorie de la démonstration en logique. Elle affirme une identité de forme :

  • entre les programmes et les preuves,
  • entre les types et les propositions.
Logique Langage
Preuve Programme
Proposition Type
Relation Démonstration Habitation

La preuve démontre la proposition si et seulement si le programme habite (a pour type) le type.

D'un point de vue ensembliste

Une autre correspondance (moins structurante, d'où le fait qu'elle ne soit pas utilisée par les constructeurs de classe) pose une identité de forme :

  • entre les programmes et les éléments,
  • entre les types et les ensembles.
Théorie des ensembles Langage
Elément Programme
Ensemble Type
Relation Appartenance Habitation

L'élément appartient à l'ensemble si et seulement si le programme habite (a pour type) le type.

Modularité

Agrégation vs. Héritage

  • Par agrégation : La relation "a un" ("possède un")

  • Par héritage : La relation "est un"

  • Avec l'agrégation avec délégation, l'injection est réalisée dynamiquement,

  • Avec l'héritage multiple, l'injection est réalisée statiquement, lors de la compilation

Comment surcharger en TS ?

Ainsi, en Java, une méthode est identifiée non seulement par son nom mais aussi par le type de ses paramètres. En Typescript, seul le nom importe, puisqu'il en est ainsi en Javascript, langage dans lequel Typescript est compilé. Il est cependant possible de déclarer plusieurs méthodes de même nom, sans les définir.

interface X {
  f(x: A): B;
  f(x: C): D;
}

Cette déclaration est équivalente à une seule.

interface X {
  readonly f: ((x: A) => B) & ((x: C) => D);
  // type intersection noté aussi : { (x: A): B; (x: C): D; }.
}

C extends P

Pourquoi peut-on utiliser heritageMulti avec heritageType alors qu'il n'hérite pas de A_default ?

interface I_Abstrait {
  f(): void;
}
abstract class A_default implements I_Abstrait {
  f(): void {
    console.log("Abstrait.f");
  }
}
interface I_Concret {
  g(): void;
}
class Concret implements I_Concret {
  g() {
    console.log("Concret.g");
  }
}

export function heritageType<C extends P, P>(
  nomChamp: keyof P,
  classeParente: any
): any {
  return classeParente.prototype[nomChamp];
}

class heritageMulti extends Concret implements I_Concret, I_Abstrait {
  public readonly f: () => void;
  constructor() {
    this.f = heritageMulti<heritageMulti, A_default>("f", A_default);
  }
}

Typage

Comment définir le type d'un objet ? Quelle différence entre Java et Typescript ?

  • TS :
    • Sous typage structurel
    • structure : ensemble des propriétés (méthodes et attributs)
    • comparaison d'objet : fonction de la structure
    • on n'a pas de classname en TS
  • Java :
    • Sous typage nominal
    • nom : ce sont les noms des classes qui importent
    • comparaison d'objet : fonction des attributs

Définir le typage explicite vs implicite

  • explicite: Il faut indiquer le nom du type avant le nom de la variable. (C, C++, java 9 et plus ancien...)
  • implicite: Il n’est pas nécessaire d’indiquer le nom du type avant le nom de la variable.

Définir le typage statique vs dynamique

  • Statique: Les types sont chargés à la compilation (une variable ne peut donc pas changer de type à l’exécution)
  • Dynamique: Les variables sont typées à l’exécution, le transtypage est donc possible (en Python par exemple). Le contrôle se fait à l’exécution.

Définir une loi d'action, une loi interne de composition

  • Loi externe de composition (loi d’action) : une loi qui s’applique sur des objets de types différents par le polymorphisme.
    • Ex : 5 + 2,0 = 7,0
    • Approche objet: les classes d’implémentation se basent sur la même interface pour interagir en protégeant l’intégrité de leurs données
  • Loi interne de composition : loi s’appliquant sur deux objets de même type
    • Ex : 5 + 2 = 7
    • Approche fonctionnel: Si on veut changer la signature de la méthode, on doit étendre tout les types.

Comment choisir entre l'approche fonctionnelle et l'approche objet ?

  • approche fonctionnelle
    • l’intra-opérabilité permet de spécialiser les calculs pour une implémentation donnée de l’interface.
  • approche objet
    • l’inter-opérabilité permet de rendre interopérable les différentes implémentations d’une interface

Héritage de type

Le fichier heritageType.ts montre un exemple concret d'héritage de type.

How to declare a function that throws an error in Typescript ?

You can not in TS.

function Test(): never => {
  throw new Error();
}

Test(); // won't cause type error
let test: boolean = Test(); // will cause type error

When there is a possibility for a function to return a value, never is absorbed by return type.

It's possible to specify it in function signature, but for reference only:

function Test(test: boolean): boolean | never {
  if (test === true) return false;

  throw new Error();
}

never = type / ensemble vide

  • throws une error
  • switch -> type never pour signaler que l'on ne devrait pas arriver là
type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape; // => pas d'erreur
      return _exhaustiveCheck;
  }
}
type Shape = Circle | Square | Triangle;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape; // Erreur, car on peut arriver dans ce cas là
      return _exhaustiveCheck;
  }
}

Covariance et contravariance dans les génériques

Covariance

Vous permet d'utiliser un type plus dérivé que celui spécifié à l'origine.

Vous pouvez assigner une instance de IEnumerable<Derived> à une variable de type IEnumerable<Base> .

Contravariance

Vous permet d'utiliser un type plus générique (moins dérivé) que celui spécifié à l'origine.

Vous pouvez assigner une instance de Action<Base> à une variable de type Action<Derived> .

Invariance

Signifie que vous pouvez utiliser uniquement le type spécifié à l’origine. Un paramètre de type générique indifférent n’est ni covariant ni contravariant.

Vous ne pouvez pas assigner une instance de List<Base> à une variable de type List<Derived> , ou vice versa.

Patrons de conceptions

Définition Inductive et patron associé

  • exemple d'ensemble inductif :
    • arbre avec feuille
    • liste : Une interface liste, et deux classes représentant “Vide” et un élément.

Le patron Composite, il représente le noyau de la définition inductive (à ne pas confondre avec Interprétation qui présente des fonctions supplémentaires)

Les méthodes nécessaires sont :

  • les services offerts par le type abstrait de données,
  • les sélecteurs, permettant de déterminer le cas de définition auquel correspond l'objet cible,
  • les projecteurs, permettant de décomposer l'objet cible, lorsqu'il s'agit d'un cas construit.

Quel patron généralise le patron Interprétation?

  • interprétation sert pour :
    • il va servir à décrire les opération que le logiciel va pouvoir utiliser

Le patron Visiteur,Il permet d’éviter certains problèmes du patron Interprétation (qui n’est utilisable que pour l'implémentation de quelques fonctions)

Quelle est la différence entre la visite et le filtrage ?

Déplacement de responsabilité.

  • Filtrage : le parcours récursif est à la charge du visiteur. On l’appelle “filtrage”, car le visiteur va devoir filtrer sur les sélecteurs (on parle de filtrage par cas). Il a l’avantage de permettre l’accès aux propriétés privées.
  • Visite : le parcours récursif est à la charge du Composite. Il est fait à l’intérieur de l’accueil() dans la liste.

Understanding Iterator Pattern in JavaScript/Typescript using Symbol.Iterator

Il existe deux concepts fondamentaux pour le modèle d'itérateur :

  1. Iterable est une structure de données qui fournit un moyen d'exposer ses données au public. En JavaScript, l'implémentation est basée sur une méthode dont la clé est Symbol.iterator. En réalité, Symbol.iterator est une usine d'itérateurs.
  2. Iterator est une structure qui contient un pointeur vers l'élément suivant dans l'itération.
const iterable {
 [Symbol.iterator](){
   // Any code
 }
}
/*********/
class iterable {
 [Symbol.iterator](){
     // Any code
 }
}

Les modèles d'adaptateurs permettent essentiellement aux classes de travailler ensemble ce qu'elles ne pouvaient pas faire en raison d'interfaces incompatibles. L'adaptateur convertit l'interface d'une classe en quelque chose qui peut être utilisé par une autre classe.

De la même manière que si vous voyagez à l'étranger, vous devez emporter un adaptateur électrique pour pouvoir utiliser les prises murales.

Le modèle de stratégie, d'autre part, prend un groupe d'algorithmes et les rend interchangeables (en s'étendant à partir d'une interface commune). Ainsi, quelle que soit la classe qui va utiliser la stratégie, elle peut facilement l'échanger avec une autre stratégie du groupe.

En d'autres termes, l'adaptateur n'ajoute aucun comportement, il modifie simplement l'interface existante pour permettre à une autre classe d'accéder à la fonctionnalité existante.

Le modèle de stratégie, quant à lui, encapsule les différents comportements et permet de les changer au moment de l'exécution.

  • Les États
    • stockent une référence à l'objet de contexte qui les contient. Ce n'est pas le cas des stratégies.
    • sont autorisés à se remplacer (IE : pour changer l'état de l'objet de contexte en quelque chose d'autre), tandis que les stratégies ne le sont pas.
  • Les stratégies
    • sont transmises à l'objet de contexte en tant que paramètres, tandis que les états sont créés par l'objet de contexte lui-même.
    • ne gèrent qu'une seule tâche spécifique, tandis que les états fournissent l'implémentation sous-jacente pour tout (ou presque tout) ce que fait l'objet de contexte.

Certes les deux modèles sont des exemples de composition avec délégation, mais ils diffèrent :

  • Le modèle d'état traite de ce qu'est (l'état ou le type) un objet (in) - il encapsule le comportement dépendant de l'état,
  • le modèle de stratégie traite de la façon dont un objet effectue une certaine tâche - il encapsule un algorithme.

Patrons de conception à connaitre

  • Structuration
    • Composite : Permet d'agencer les objets dans des arborescences afin de pouvoir traiter celles-ci comme des objets individuels.
  • Comportement
    • Interpréteur : définit la grammaire d'un langage utilisé pour décrire les opérations qu'ils peuvent réaliser et utilise celle-ci pour interpréter des états dans ce langage.
    • Iterator : Permet de parcourir les éléments d’une collection sans révéler sa représentation interne (liste, pile, arbre, etc.).
    • Strategy : Permet de définir une famille d’algorithmes, de les mettre dans des classes séparées et de rendre leurs objets interchangeables.
    • Visitor : Permet de séparer les algorithmes et les objets sur lesquels ils opèrent.

Utilisation des λ-expressions

exemple

Classe K correspondant au constructeur de termes k, avec deux attributs c : C et x : I

filtrage<R>(..., k : (c : C, x : I) => R, ...): R {
    return v.k(this.c, this.x); // Pas de parcours récursif
}

Usage utilisant des λ-expressions (des fermetures)

function f(i : I) : R {
  return i.filtrage( // filtrage ou pattern matching
    ...,
    (c, x) => ..., // lambda-expression
    ...
  );
}

L'usage des fermetures ("closures") ou des λ-expressions simplifie l'écriture du filtrage, qui se rapproche de la pratique dans les langages fonctionnels.

Java vs TS

  • λ abstractions :
 x -> x
 (x, y) -> x + y
 x -> {
   System.out.println("int : " + x);
   return x;
}
  • abstractions constantes :
    • à gauche une classe et à droite une fonction, ou
    • à gauche une référence d'objet et à droite une méthode de la classe à laquelle appartient l'objet.
I c = x -> Integer.toString(x);
c = Integer::toString; // Formulation équivalente
  • Le corps d'une λ-abstraction peut contenir des variables libres :
    • soit des variables locales,
    • soit des paramètres.
  • Restrictions : Variables doivent être constantes (final) ou considéré comme variable de classe interne (fermeture)
  • Cette fermeture ne s'applique pas aux attributs d'une classe englobante qui seraient utilisés dans le corps d'une λ-abstraction : les attributs ne sont pas des variables, mais plutôt des opérateurs de projection ou de sélection pour un objet.
let somme: (x: number, y: number) => number = (x: number, y: number) => x + y;

let sommeALancienne: (x: number, y: number) => number = function (
  x: number,
  y: number
) {
  return x + y;
};

Le corps des λ-abstractions peut contenir des variables libres, sans restriction, contrairement à Java. Une variable libre devient liée par fermeture lorsqu'elle quitte l'environnement.

Remarque : une λ-abstraction, contrairement à une fonction anonyme déclarée par function ne possède pas de paramètre implicite this.

// attention,la méthode de P peut etre abstrait mais à la différence C est bien concret
// on ne peut pas faire classeParente: C car il n'y a pas de propriété 'prototype' associée
// - usage : this.f = heritageType<SousType, SurType>('f', SurType)
// (SousType < SurType, 'f' champ de SurType)
// - avec contrôle de typage (à la ts)
export function heritageType<C extends P, P>(nomChamp: keyof P,
classeParente: any)
: any {
return classeParente.prototype[nomChamp];
};
interface Abstrait {
f(): void;
}
// Classe donnant une implémentation par défaut
abstract class Abstrait_defaut implements Abstrait {
f(): void {
console.log("Abstrait.f")
}
}
interface Concret {
g(): void;
}
class ImplemConcret implements Concret {
g() {
console.log("Concret.g")
}
}
//on force la définition de f() en faisant implémenter une interface (Abstrait)
//(et non dans une classe abstraite car on veut garder le plus de généralité possible)
class Implem extends ImplemConcret implements Concret, Abstrait {
// Déclaration nécessaire sous la forme d'attributs fonctionnels
// -> Répétition des déclarations d'Abstrait
public readonly f: () => void;
// f est une propriété en readonly pour éviter sa modification après instanciation de la classe
// et on veut pas le mettre en private pour pouvoir le réutiliser par la suite
constructor() {
super();
// Agrégation du code
// on utilise bien Abstrait_defaut parce que c'est là où la fonction est implémentée
this.f = heritageType<Implem, Abstrait_defaut>('f', Abstrait_defaut);
}
}
// QUESTIONS :
// (0) Pourquoi cela fonctionne avec Abstrait_default et pas juste avec Abstrait ?
// TS es un langage strucuturel et non nominal comme Java
// (1) Devrait on toujours utiliser heritageType<C extends P,P> dans la classe C ?
// oui, car on souhaite s'auto-controlé
// (2) pourquoi ne pas avoir mis Abstrait au lieu de Abstrait default,
// dans heritageType<Implem, Abstrait_defaut> ?
// On souhaite viser la classe concrète car si l on vise l'interface ce n'est pas suffisant
// en effet une classe peut implémenter plusieurs interface
// et on souhate s'asssurer que Implem recoit bien la fonction définie au sein de Abstrait_default
// (il est possible de definir du code dans une interface avec le mot clé default)
// (3) pourquoi ne pas faire plus simple ?
// Non, car on souhaite éviter les erreus : sureté de typage
export function heritageType2<P>(nomChamp: keyof P, classeParente: any)
: any {
return classeParente.prototype[nomChamp];
}
class Implem2 extends ImplemConcret implements Concret, Abstrait {
public readonly f: () => void;
constructor() {
super();
this.f = heritageType2<Abstrait>(
'f', Abstrait_defaut);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment