Les spécificités des langages¶
🤖 Rédaction assistée par IA.
Cette fiche est une annexe venant compléter la présentation mborne.github.io/cours-patron-conception - Les patrons de conception. Elle vise à guider dans l'adaptation des patrons de conception en fonction du langage utilisé.
En effet, si les concepts de base de la P.O.O. (classe, interface, encapsulation, héritage...) ont progressivement été intégrés dans la plupart des langages (ex : PHP 5 en 20041, TypeScript en 20122, ECMAScript 6 en 20153...), chaque langage a des spécificités qui devront être prises en compte et exploitées dans la conception.
Nous noterons aussi que les langages ont évolué depuis la parution du GoF en 1994 dont les exemples sont en C++ et en Smalltalk. Certains patrons sont désormais intégrés aux langages, simplifiés, voire rendus inutiles par l'arrivée de nouveaux concepts (generics, lambdas, générateurs...).
Typage statique ou typage dynamique¶
Quel que soit le langage de programmation utilisé, une valeur aura toujours un type. Ceci permettra par exemple d'avoir comme résultat :
5pour l'addition de deux nombres2+3"23"pour la concaténation de deux chaînes de caractères"2"+"3"
Nous distinguerons toutefois les langages en fonction du moment où les types sont vérifiés :
- Avec un typage statique, le type des variables est connu et vérifié à la compilation (Java, C#, C++, Go, Rust, TypeScript...)4.
- Avec un typage dynamique, le type est porté par la valeur et vérifié à l'exécution. Une même variable peut successivement contenir des valeurs de types différents (Python, JavaScript, PHP, Ruby...)5.
Impact
Certains patrons de conception sont moins pertinents avec un typage dynamique car le langage offre nativement ce qu'ils permettent de construire :
- Le duck typing6 permet de manipuler des objets n'ayant pas d'interface commune déclarée : les interfaces des patrons sont alors implicites.
- Le type étant connu à l'exécution, un Visitor peut appeler la méthode correspondant au type de l'objet sans passer par une méthode
accept(ex : ast.NodeVisitor en Python appellevisit_<NomDeClasse>). - Les classes étant des valeurs (Python, JavaScript, PHP), une classe peut être passée en paramètre et jouer le rôle de Factory.
En contrepartie, un typage statique permet de détecter à la compilation le non-respect des contrats définis par les patrons.
La signature des méthodes¶
En fonction des langages, une méthode sera identifiée :
- Par sa signature, c'est-à-dire son nom et le type de ses paramètres (Java, C#, C++). Plusieurs méthodes peuvent alors porter le même nom (surcharge ou overloading).
- Par son seul nom (Python, JavaScript, PHP, mais aussi Go), ce qui impose l'unicité du nom des fonctions et méthodes. TypeScript permet de déclarer plusieurs signatures, mais avec une seule implémentation.
Impact
Nous serons amenés à adapter la présentation de certains patrons de conception. Par exemple, le patron Visitor s'appuie en Java sur la surcharge (visit(Point), visit(LineString)) alors qu'il faudra des noms uniques (visitPoint, visitLineString) dans les autres langages.
La gestion de la mémoire¶
Les choix de conception se feront en fonction des mécanismes de gestion de la mémoire du langage utilisé :
- Un ramasse-miettes (garbage collector) libère automatiquement les objets qui ne sont plus accessibles (Java, C#, Go, JavaScript, Python, PHP...).
- Le comptage de références libère un objet dès qu'il n'est plus référencé, mais ne sait pas libérer seul des références circulaires qu'il faudra casser avec des références faibles (ex :
std::shared_ptr/std::weak_ptren C++,Rc/Weaken Rust, ARC en Swift). - La libération manuelle de la mémoire (ex :
malloc/freeen C,new/deleteen C++) qu'il est possible d'automatiser en C++ avec des pointeurs intelligents et le principe RAII. - La notion de propriétaire d'une valeur et d'emprunt en Rust, vérifiée à la compilation.
En fonction des cas, nous serons plus ou moins contraints sur les relations entre les objets (ex : une relation bidirectionnelle parent / enfant est triviale avec un ramasse-miettes, demande une référence faible avec le comptage de références et une conception spécifique en Rust).
Impact
Concevoir un moteur de jeu vidéo en UML sans se demander s'il sera implémenté en C++, en Java ou en JavaScript n'a pas de sens. Les contraintes ne sont pas les mêmes.
La présence de concepts complémentaires¶
Certains langages offriront des concepts complémentaires aux concepts de base de la P.O.O. :
-
Des propriétés et des accesseurs "magiques" permettant d'exécuter du code lors de l'accès à un attribut :
- Property en C#
@propertyet__getattr__en Pythonget/seten JavaScript__get/__seten PHP
Impact
Il n'est pas obligatoire (voire pas dans la culture) de définir des méthodes
getX()/setX()dans certains langages : un attribut public pourra être transformé en propriété sans modifier le code appelant. -
Des métadonnées associées au code (annotations en Java, attributs en C# et en PHP >= 8.0...) pouvant être exploitées via des mécanismes d'introspection et de réflexion.
Impact
En Java, JUnit (
@Test) et Spring (@Autowired) s'appuieront sur ces mécanismes alors que nous trouverons des mécanismes de génération de code pour obtenir des équivalents dans d'autres langages (ex : macros en C++ et Rust avec#[derive(...)], Qt Meta-Object Compiler,...)
Les patrons intégrés¶
Certains patrons de conception sont intégrés aux langages :
- Le patron Prototype, qui traite la copie d'objets, peut être :
- Présent par défaut (ex :
copy.copy()etcopy.deepcopy()en Python, méthodeclone()héritée deObjecten Java) - À la base du langage (ex : héritage par prototypes en JavaScript)7
- Présent par défaut (ex :
- Le patron Iterator est exploité par les boucles (ex :
Iterableetfor (... : ...)en Java,__iter__etfor ... inen Python,Symbol.iteratoretfor ... ofen JavaScript).
Impact
Nous nous appuierons sur ces mécanismes plutôt que de réimplémenter ces patrons.
La présence d'autres paradigmes¶
Dans de nombreux langages, nous noterons que la P.O.O. cohabitera avec d'autres paradigmes :
- La programmation générique (templates en C++, generics en Java, C#, TypeScript, Rust...) permettant d'écrire du code indépendant des types manipulés (ex :
List<T>). - La programmation fonctionnelle où les fonctions sont des valeurs pouvant être passées en paramètre (lambdas, closures) avec des traitements tels
.map,.filter,.reduce,... - Les générateurs (mot clé
yielden Python, JavaScript, C#...) permettant de produire des valeurs à la demande.
Nous soulignerons que nous pourrons :
- Hybrider la P.O.O. avec la programmation générique (ex : une interface
Repository<T>). - Simplifier certains patrons avec la programmation fonctionnelle (ex : une lambda pourra remplacer une classe implémentant une Strategy ou un Observer n'ayant qu'une méthode).
- Établir des ponts entre la P.O.O. et la programmation fonctionnelle à l'aide du patron Iterator (ex :
IterableetStreamen Java, générateurs en Python et JavaScript).
-
PHP 5 introduit un nouveau modèle objet avec la visibilité (
public,protected,private), les interfaces et les classes abstraites. ↩ -
TypeScript ajoute à JavaScript les interfaces, la visibilité et les classes abstraites. ↩
-
ECMAScript 6 (ES2015) introduit la syntaxe
classen JavaScript, qui repose toujours sur l'héritage par prototypes. ↩ -
Le type n'a pas forcément à être écrit grâce à l'inférence de type (ex :
let x = 5;en Rust,varen Java,autoen C++). ↩ -
Un nombre croissant de langages à typage dynamique permettent de déclarer des types (typage graduel) :
- En PHP, les déclarations de type sont contrôlées à l'exécution.
- En Python, les annotations de type (Python >= 3.5) ne sont pas contrôlées à l'exécution mais exploitées par des outils d'analyse statique (ex : mypy) et par les éditeurs.
- TypeScript ajoute un typage statique à JavaScript : les types sont vérifiés à la compilation puis effacés dans le code JavaScript produit.
-
Le duck typing (« si ça marche comme un canard et que ça cancane comme un canard, alors c'est un canard ») consiste à s'appuyer sur les méthodes disponibles plutôt que sur le type déclaré d'un objet. Par exemple, en Python, une fonction
render(geometry)appelantgeometry.draw()acceptera aussi bien unPointqu'uneLineStringdu moment qu'ils ont une méthodedraw(), sans qu'ils implémentent une interface commune. ↩ -
En JavaScript,
Object.create(prototype)crée un objet à partir d'un autre etstructuredClone()réalise une copie profonde des données (sans conserver la classe). ↩