L’un des faits immuables de la vie est que le changement est la constante de tous les cycles de vie d’un logiciel, un défi auquel vous ne pouvez pas échapper. Le défi consiste à s'adapter à ce changement avec un minimum de latence et une flexibilité maximale.
La bonne nouvelle est que quelqu'un a probablement déjà résolu vos problèmes de conception et que leurs solutions ont évolué pour devenir de meilleures pratiques. ces meilleures pratiques convenues sont appelées "modèles de conception". Aujourd'hui, nous allons explorer deux modèles de conception populaires et apprendre comment une bonne conception peut vous aider à écrire du code parfaitement propre et extensible..
Supposons que vous ayez un système existant. Vous devez maintenant le faire fonctionner avec une nouvelle bibliothèque tierce, mais cette bibliothèque a une API différente de celle utilisée précédemment. Le système existant attend maintenant une interface différente de celle proposée par la nouvelle bibliothèque. Bien sûr, vous pourriez être assez courageux (lu, idiot) pour penser à modifier votre code hérité pour l’adapter à la nouvelle interface, mais comme pour tout système hérité - jamais, jamais..
Des adaptateurs à la rescousse! Écrivez simplement un adaptateur
(une nouvelle classe d'encapsulation) entre les systèmes, qui écoute les demandes des clients vers l'ancienne interface et les redirige ou les traduit en appels vers la nouvelle interface. Cette conversion peut être implémentée avec héritage ou composition.
Un excellent design ne concerne pas seulement la réutilisation, mais aussi l'extensibilité.
Les adaptateurs aident les classes incompatibles à travailler ensemble en prenant une interface et en l'adaptant à une interface que le client peut analyser.
Assez bavardé; passons aux affaires, allons-nous? Notre système logiciel hérité utilise les éléments suivants LegacyVideoController
interface pour contrôler le système vidéo.
interface publique LegacyVideoController / ** * Lance la lecture après startTimeTicks * depuis le début de la vidéo * @param startTimeTicks temps en millisecondes * / public void startPlayback (long startTimeTicks);…
Le code client qui utilise ce contrôleur ressemble à ceci:
public void playBackVideo (long timeToStart, contrôleur LegacyVideoController) if (controller! = null) controller.startPlayback (timeToStart);
Il n'y a rien de nouveau ici, vraiment - cela arrive assez souvent. Les besoins des utilisateurs sont sujets à changement en permanence et notre système hérité doit désormais fonctionner avec un nouveau contrôleur vidéo, doté de l'interface suivante:
interface publique AdvancedVideoController / ** * Place le contrôleur tête après heure * depuis le début de la piste * heure @param time définit le temps requis pour la recherche * / public void recherche (heure); / ** * Lit le morceau * / public void play ();
En conséquence, le code client se brise car cette nouvelle interface n’est pas compatible..
Alors, comment traitons-nous cette interface modifiée sans changer notre code hérité? Vous connaissez la réponse maintenant, n'est-ce pas? Nous écrivons un simple adaptateur pour modifier son interface afin de l'adapter à l'existant comme ci-dessous:
Classe publique AdvancedVideoControllerAdapter implémente LegacyVideoController private AdvancedVideoController advancedvideoController; public AdvancedVideoControllerAdapter (AdvancedVideoController advancedVideoController) this.advancedVideoController = advancedVideoController; @Override public void startPlayback (long startTimeTicks) // Convertit longtemps en heure de la date et heure startTime = getTime (startTimeTicks); // Adapter advancedVideoController.seek (startTime); advancedVideoController.play ();
Cet adaptateur implémente l'interface cible, ce à quoi le client s'attend, il n'est donc pas nécessaire de modifier le code du client. Nous composons l'adaptateur avec une instance de l'interface adaptée.
Cette relation "a-a" permet à l'adaptateur de déléguer la demande du client à l'instance réelle..
Les adaptateurs aident également à découpler le code du client et la mise en œuvre.
Nous pouvons maintenant simplement envelopper le nouvel objet dans cet adaptateur et le faire sans modifier le code client car le nouvel objet est maintenant converti / adapté à la même interface..
AdvancedVideoController advancedController = controllerFactory.createController (); // adapter LegacyVideoController controllerAdapter = new AdvancedVideoControllerAdapter (advancedController); playBackVideo (20, controllerAdapter);
Un adaptateur peut être un simple passage ou suffisamment intelligent pour fournir des modules complémentaires en fonction de la complexité de l'interface à prendre en charge. De même, un adaptateur peut être utilisé pour envelopper plusieurs objets si l'interface cible est complexe et que la nouvelle fonctionnalité a été divisée en deux classes ou plus..
Bien que de nombreux modèles traitent de la création d'objets, un modèle spécifique en ressort. Aujourd’hui, nous allons examiner l’un des plus simples, mais encore mal compris: le modèle Singleton.
Comme son nom l'indique, le singleton a pour objectif de créer une instance unique de la classe et de lui donner un accès global. Des exemples peuvent être un niveau d'application Cache
, un pool d'objets de threads, de connexions, etc. Pour de telles entités, une seule et même instance doit suffire, sinon elles menacent la stabilité et nuisent au but de l'application.
Une implémentation simple en Java ressemblerait à ceci:
Classe publique ApplicationCache carte privéeattributMap; // Instance privée privée Instance statique ApplicationCache; // Méthode d'accès statique public statique ApplicationCache getInstance () if (instance == null) instance == new ApplicationCache (); instance de retour; // constructeur privé private ApplicationCache () attributMap = createCache (); // Initialise le cache
Dans notre exemple, la classe contient un membre statique du même type que celui de la classe, auquel on accède via une méthode statique. Nous utilisons Initialisation paresseuse ici, en retardant l'initialisation du cache jusqu'à ce qu'il soit réellement nécessaire au moment de l'exécution. Le constructeur est également rendu privé afin qu’une nouvelle instance de cette classe ne puisse pas être créée à l’aide de la commande Nouveau
opérateur. Pour récupérer le cache, nous appelons:
Cache ApplicationCache = ApplicationCache.getInstance (); // utilise le cache pour améliorer les performances
Cela fonctionne parfaitement tant que nous avons affaire à un modèle à un seul thread. Mais la vie, telle que nous la connaissons, n’est pas si simple. Dans un environnement multithread, vous devez synchroniser l'initialisation différée ou simplement la supprimer en créant le cache dès le chargement de la classe, en utilisant des blocs statiques ou en initialisant lors de la déclaration du cache..
Nous synchronisons l'initialisation différée pour nous assurer que le code d'initialisation n'est exécuté qu'une seule fois. Ce code fonctionne avec Java version 5.0 et ultérieure en raison des particularités associées à la mise en œuvre de synchronisé
et volatil
en Java.
Classe publique ApplicationCache carte privéeattributMap; // volatile de sorte que les écritures dans le désordre JVM ne se produisent pas privé instance volatile statique ApplicationCache; public statique ApplicationCache getInstance () // Vérifié une fois si (instance == null) // Synchronisé sur le verrou de niveau classe synchronisé (ApplicationCache.class) // Vérifié à nouveau si (instance == null) instance == nouveau ApplicationCache (); instance de retour; private ApplicationCache () attributMap = createCache (); // Initialise le cache
Nous rendons la variable d'instance volatile afin que la machine virtuelle Java empêche les écritures dans le désordre pour elle. Nous effectuons également une vérification double null (d'où le nom) par exemple lors de la synchronisation de l'initialisation, de sorte que toute séquence de 2 threads ou plus ne corrompe pas l'état ni n'entraîne la création de plusieurs instances du cache. Nous aurions pu à la place synchroniser l'intégralité de la méthode d'accès statique, mais cela aurait été excessif, car la synchronisation n'est nécessaire que jusqu'à ce que l'objet soit complètement initialisé. plus jamais en y accédant.
Un moyen plus simple serait de supprimer les avantages de l'initialisation lente, ce qui permettrait également d'obtenir un code plus propre:
Classe publique ApplicationCache carte privéeattributMap; // Initialisation de la déclaration While privée, instance statique ApplicationCache statique = new ApplicationCache (); public ApplicationCache statique getInstance () instance de retour; // constructeur privé private ApplicationCache () attributMap = createCache (); // Initialise le cache
Dès que la classe est chargée et que les variables sont initialisées, nous appelons le constructeur privé pour créer la seule et unique instance du cache. Nous perdons les avantages de l'initialisation paresseuse de l'instance, mais le code est beaucoup plus propre. Les deux méthodes sont thread-safe et vous pouvez choisir celle qui convient à votre environnement de projet.
En fonction de vos besoins, vous voudrez peut-être également vous protéger contre:
readResolve ()
méthode de l'API de sérialisationLe titre de notre tutoriel est un peu trompeur, je l’admets, car les modèles de conception ne tiennent pas compte des langues. Ils constituent simplement une collection des meilleures stratégies de conception développées pour contrer les problèmes récurrents rencontrés dans la conception de logiciels. Ni plus ni moins.
Par exemple, voici un aperçu de la manière dont nous pourrions mettre en œuvre un singleton
en Javascript. L'intention reste la même: contrôler la création de l'objet et maintenir un point d'accès global, mais la mise en œuvre diffère avec les constructions et la sémantique de chaque langage..
var applicationCache = function () // objet privé var instance; function initCache () return proxyUrl: "/bin/getCache.json", cachePurgeTime: 5000, autorisations: read: "tout le monde", write: "admin"; // Retour public getInstance: function () if (! Instance) instance = initCache (); instance de retour; , purgeCache: function () instance = null; ; ;
Pour citer un autre exemple, jQuery fait également un usage intensif du modèle de conception Façade, en éliminant la complexité d’un sous-système et en offrant une interface plus simple à l’utilisateur..
Tous les problèmes ne nécessitent pas l'utilisation d'un modèle de conception spécifique
Un mot de prudence est nécessaire: ne pas en abuser! Tous les problèmes ne nécessitent pas l'utilisation d'un modèle de conception spécifique. Vous devez analyser soigneusement la situation avant de vous installer sur un motif. L'apprentissage des modèles de conception aide également à comprendre d'autres bibliothèques telles que jQuery, Spring, etc., qui utilisent beaucoup de tels modèles..
J'espère qu'après avoir lu cet article, vous pourrez vous rapprocher de votre compréhension des modèles de conception. Si vous avez des questions ou souhaitez apprendre un modèle de conception supplémentaire, merci de me le faire savoir dans les commentaires ci-dessous, et je ferai de mon mieux pour répondre à vos questions.!