
En programmation informatique , le modèle de conception Flyweight désigne un objet qui minimise l'utilisation de la mémoire en partageant certaines de ses données avec d'autres objets similaires. Le modèle Flyweight est l'un des vingt-trois modèles de conception GoF .
Dans d'autres contextes, l'idée de partager des structures de données est appelée hachage consing .
Le concept a été inventé et exploré pour la première fois en profondeur par Paul Calder et Mark Linton en 1990 pour gérer efficacement les informations des glyphes dans un éditeur de documents WYSIWYG . Des techniques similaires étaient déjà utilisées dans d'autres systèmes, cependant, dès 1988.
Aperçu
Le modèle Flyweight est utile pour gérer un grand nombre d'objets partageant des éléments simples et répétitifs, dont l'intégration individuelle consommerait une quantité importante de mémoire. Il est courant de stocker les données partagées dans des structures de données externes et de les transmettre temporairement aux objets lors de leur utilisation.
Un exemple classique est celui des structures de données utilisées pour représenter les caractères dans un traitement de texte . En théorie, chaque caractère d'un document pourrait avoir un objet glyphe contenant son contour, ses métriques et d'autres données de mise en forme. Cependant, cela consommerait des centaines, voire des milliers d'octets de mémoire par caractère. En revanche, chaque caractère peut avoir une référence à un objet glyphe partagé par toutes les occurrences de ce même caractère dans le document. Ainsi, seule la position de chaque caractère doit être stockée en interne.
En conséquence, les objets de poids mouche peuvent :
- stocker un état intrinsèque invariant, indépendant du contexte et partageable (par exemple, le code du caractère 'A' dans un jeu de caractères donné)
- fournir une interface permettant de transmettre un état extrinsèque variable, dépendant du contexte et non partageable (par exemple, la position du caractère « A » dans un document texte).
Les clients peuvent réutiliser Flyweightles objets et transmettre des données d'état extrinsèques selon les besoins, réduisant ainsi le nombre d'objets physiquement créés.
Structure

Le diagramme de classes UML ci-dessus montre :
- la
Clientclasse, qui utilise le modèle de poids mouche - la
FlyweightFactoryclasse, qui crée et partageFlyweightdes objets - l'
Flyweightinterface , qui prend en entrée un état extrinsèque et effectue une opération - la
Flyweight1classe, qui implémenteFlyweightet stocke l'état intrinsèque
Le diagramme de séquence illustre les interactions suivantes lors de l'exécution :
- L'
Clientobjet fait appelgetFlyweight(key)à la fonctionFlyweightFactory, qui renvoie unFlyweight1objet. - Après avoir appelé l'
operation(extrinsicState)objet renvoyéFlyweight1, le .ClientgetFlyweight(key)FlyweightFactory - La fonction renvoie l' objet
FlyweightFactorydéjà existant .Flyweight1
Détails de mise en œuvre
Il existe plusieurs façons d'implémenter le modèle Flyweight. L'une d'elles est la mutabilité : la possibilité de modifier les objets stockant l'état Flyweight extrinsèque.
Les objets immuables se partagent facilement, mais nécessitent la création de nouveaux objets externes à chaque modification d'état. À l'inverse, les objets mutables peuvent partager leur état. La mutabilité permet une meilleure réutilisation des objets grâce à la mise en cache et à la réinitialisation des anciens objets inutilisés. Le partage est généralement impossible lorsque l'état est très variable.
Parmi les autres préoccupations principales figurent la récupération (comment le client final accède à la ressource légère), la mise en cache et la concurrence .
Récupération
L' interface de fabrication permettant de créer ou de réutiliser des objets légers n'est souvent qu'une façade pour un système sous-jacent complexe. Par exemple, cette interface est généralement implémentée comme un singleton afin de fournir un accès global à la création d'objets légers.
De manière générale, l'algorithme de récupération commence par une demande de nouvel objet via l'interface de l'usine.
La requête est généralement transmise au cache approprié en fonction du type d'objet. Si un objet présent dans le cache répond à la requête, il peut être réinitialisé et renvoyé. Sinon, un nouvel objet est créé. Si l'objet est composé de plusieurs sous-composants extrinsèques, ces derniers seront assemblés avant le renvoi de l'objet.
Mise en cache
Il existe deux façons de mettre en cache des objets très légers : les caches entretenues et les caches non entretenues.
Les objets dont l'état est très variable peuvent être mis en cache à l'aide d'une structure FIFO . Cette structure conserve les objets inutilisés dans le cache, évitant ainsi d'avoir à le parcourir.
À l'inverse, les caches non gérés engendrent moins de frais initiaux : les objets sont initialisés en bloc lors de la compilation ou du démarrage. Une fois le cache rempli, l'algorithme de récupération des objets peut présenter une surcharge plus importante que les opérations d'insertion/dépilement d'un cache géré.
Lors de la récupération d'objets externes à état immuable, il suffit de rechercher dans le cache un objet possédant l'état souhaité. Si aucun objet correspondant n'est trouvé, il faut en initialiser un. Lors de la récupération d'objets externes à état mutable, si aucun objet utilisé n'est trouvé, il faut rechercher dans le cache un objet inutilisé afin de le réinitialiser. S'il n'y a pas d'objet inutilisé disponible, un nouvel objet doit être instancié et ajouté au cache.
Des caches distincts peuvent être utilisés pour chaque sous-classe unique d'objet extrinsèque. Plusieurs caches peuvent être optimisés séparément, chacun étant associé à un algorithme de recherche spécifique. Ce système de mise en cache d'objets peut être encapsulé selon le modèle de la chaîne de responsabilité , favorisant ainsi un faible couplage entre les composants.
Concurrence
Une attention particulière doit être portée à la création d'objets légers sur plusieurs threads. Si la liste des valeurs est finie et connue à l'avance, les objets légers peuvent être instanciés en amont et récupérés depuis un conteneur sur plusieurs threads sans conflit. Si des objets légers sont instanciés sur plusieurs threads, deux options sont possibles :
- Rendez l'instanciation des objets légers mono-thread, introduisant ainsi une contention et garantissant une instance par valeur.
- Autoriser les threads concurrents à créer plusieurs instances légères, éliminant ainsi les conflits et permettant plusieurs instances par valeur.
Pour permettre un partage sécurisé entre les clients et les threads, les objets légers peuvent être transformés en objets de valeur immuables , où deux instances sont considérées comme égales si leurs valeurs sont égales.
Exemples
C#
Dans cet exemple, chaque instance de la MyObjectclasse utilise une Pointerclasse pour fournir des données.
// Définit un objet Flyweight qui se répète. public class Flyweight { public string Name { get ; set ; } public string Location { get ; set ; } public string Website { get ; set ; } public byte [] Logo { get ; set ; } }public static class Pointer { public static readonly Flyweight Company = new Flyweight { Name = "ABC" , Location = "XYZ" , Website = "www.example.com" }; }public class MyObject { public string Name { get ; set ; } public string Company => Pointer . Company . Name ; }
C++
La bibliothèque standard de modèles C++ (STL) fournit plusieurs conteneurs permettant d'associer des objets uniques à une clé. L'utilisation de conteneurs contribue à réduire davantage la consommation de mémoire en évitant la création d'objets temporaires.
import std ;template < typename K , typename V > using TreeMap = std :: map < K , V > ; using String = std :: string ; using StringView = std :: string_view ; template < typename K , typename V > using HashMap = std :: unordered_map < K , V > ;// Les instances de Tenant seront la classe Flyweights Tenant { private : const String name ; public : explicit Tenant ( StringView name ) : name { name } {}[[ nodiscard ]] String getName () const noexcept { return name ; } };// Registry sert de fabrique et de cache pour les objets légers Tenant class Registry { private : HashMap < String , Tenant > tenants ; public : Registry () = default ;[[ nodiscard ]] Tenant & findByName ( StringView name ) { if ( ! tenants . contains ( name )) { tenants [ name ] = Tenant { name }; } return tenants [ name ]; } };// La classe Apartment associe un locataire unique à son numéro de chambre. class Apartment { private : TreeMap < int , Tenant *> occupants ; Registry registry ; public : Apartment () = default ;void addOccupant ( StringView nom , int pièce ) { occupants [ pièce ] = & registry.findByName ( nom ) ; }void printTenants () { // room: int, tenant: Tenant for ( const auto & [ room , tenant ] : occupants ) { std :: println ( "{} occupe la chambre {}" , tenant . name (), room ); } } };int main ( int argc , char * argv []) { Appartement appartement ; appartement . addOccupant ( "David" , 1 ); appartement . addOccupant ( "Sarah" , 3 ); appartement . addOccupant ( "George" , 2 ); appartement . addOccupant ( "Sarah" , 12 ); appartement . addOccupant ( "Michael" , 10 ); appartement . printTenants ();retourner 0 ; }
PHP
<?phpclasse SaveurCafé {tableau statique privé $CACHE = [];fonction privée __construct ( chaîne privée $name ) {}public static function intern ( string $name ) : self { self :: $CACHE [ $name ] ??= new self ( $name ); return self :: $CACHE [ $name ]; }public static function flavorsInCache () : int { return count ( self :: $CACHE ); }public function __toString () : string { return $this- > name ; }}classe Commande {fonction privée __construct ( private CoffeeFlavour $flavour , private int $tableNumber ) {}public static function create ( string $flavourName , int $tableNumber ) : self { $flavour = CoffeeFlavour :: intern ( $flavourName ); return new self ( $flavour , $tableNumber ); }public function __toString () : string { return "Service de { $this -> flavor } à la table { $this -> tableNumber } " ; } }classe CoffeeShop {tableau privé $orders = [];public function takeOrder ( string $flavour , int $tableNumber ) { $this- > orders [] = Order :: create ( $flavour , $tableNumber ); }public function service () { print ( implode ( PHP_EOL , $this- > orders ) . PHP_EOL ); } }$shop = new CoffeeShop (); $shop- > takeOrder ( "Cappuccino" , 2 ); $shop- > takeOrder ( "Frappe" , 1 ); $shop- > takeOrder ( "Espresso" , 1 ); $shop- > takeOrder ( "Frappe" , 897 ); $shop- > takeOrder ( "Cappuccino" , 97 ); $shop- > takeOrder ( "Frappe" , 3 ); $shop- > takeOrder ( "Espresso" , 3 ); $shop- > takeOrder ( "Cappuccino" , 3 ); $shop- > takeOrder ( "Espresso" , 96 ); $shop- > takeOrder ( "Frappe" , 552 ); $shop- > takeOrder ( "Cappuccino" , 121 ); $shop- > takeOrder ( "Espresso" , 121 ); $shop -> service (); print ( "Objets CoffeeFlavor dans le cache : " . CoffeeFlavour :: flavorsInCache () . PHP_EOL );