JavaRush /Blog Java /Random-FR /Modèles de conception en Java
Viacheslav
Niveau 3

Modèles de conception en Java

Publié dans le groupe Random-FR
Les modèles ou modèles de conception constituent une partie souvent négligée du travail d'un développeur, ce qui rend le code difficile à maintenir et à adapter aux nouvelles exigences. Je vous suggère de regarder de quoi il s'agit et comment il est utilisé dans le JDK. Naturellement, tous les modèles de base, sous une forme ou une autre, nous entourent depuis longtemps. Voyons-les dans cette revue.
Modèles de conception en Java - 1
Contenu:

Modèles

L'une des exigences les plus courantes pour les postes vacants est la « connaissance des modèles ». Tout d’abord, il convient de répondre à une question simple : « Qu’est-ce qu’un modèle de conception ? » Pattern est traduit de l’anglais par « modèle ». C'est-à-dire qu'il s'agit d'un certain modèle selon lequel nous faisons quelque chose. La même chose est vraie en programmation. Il existe certaines bonnes pratiques et approches établies pour résoudre les problèmes courants. Chaque programmeur est un architecte. Même lorsque vous créez seulement quelques classes, voire une, cela dépend de vous, de la durée pendant laquelle le code peut survivre à des exigences changeantes et de la commodité avec laquelle il peut être utilisé par d'autres. Et c'est là que la connaissance des modèles sera utile, car... Cela vous permettra de comprendre rapidement la meilleure façon d’écrire du code sans le réécrire. Comme vous le savez, les programmeurs sont des gens paresseux et il est plus facile de bien écrire quelque chose tout de suite que de le refaire plusieurs fois.) Les modèles peuvent aussi ressembler à des algorithmes. Mais ils ont une différence. L'algorithme se compose d'étapes spécifiques qui décrivent les actions nécessaires. Les modèles décrivent uniquement l'approche, mais ne décrivent pas les étapes de mise en œuvre. Les modèles sont différents, parce que... résoudre différents problèmes. On distingue généralement les catégories suivantes :
  • Génératif

    Ces modèles résolvent le problème de la flexibilité de la création d'objets

  • De construction

    Ces modèles résolvent le problème de la création efficace de connexions entre les objets

  • Comportemental

    Ces modèles résolvent le problème de l'interaction efficace entre les objets

Pour considérer des exemples, je suggère d'utiliser le compilateur de code en ligne repl.it.
Modèles de conception en Java - 2

Modèles de création

Commençons par le début du cycle de vie des objets – avec la création des objets. Les modèles génératifs aident à créer des objets plus facilement et offrent de la flexibilité dans ce processus. L'un des plus connus est « Builder ». Ce modèle vous permet de créer des objets complexes étape par étape. En Java, l'exemple le plus connu est StringBuilder:
class Main {
  public static void main(String[] args) {
    StringBuilder builder = new StringBuilder();
    builder.append("Hello");
    builder.append(',');
    builder.append("World!");
    System.out.println(builder.toString());
  }
}
Une autre approche bien connue pour créer un objet consiste à déplacer la création vers une méthode distincte. Cette méthode devient en quelque sorte une fabrique d’objets. C'est pourquoi le modèle est appelé « Méthode d'usine ». En Java, par exemple, son effet est visible dans la classe java.util.Calendar. La classe elle-même Calendarest abstraite, et pour la créer la méthode est utilisée getInstance:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime());
    System.out.println(calendar.getClass().getCanonicalName());
  }
}
Cela est souvent dû au fait que la logique derrière la création d’objets peut être complexe. Par exemple, dans le cas ci-dessus, nous accédons à la classe de base Calendaret une classe est créée GregorianCalendar. Si nous regardons le constructeur, nous pouvons voir que différentes implémentations sont créées en fonction des conditions Calendar. Mais parfois, une seule méthode d’usine ne suffit pas. Parfois, vous devez créer différents objets pour qu’ils s’emboîtent. Un autre modèle nous y aidera - « Usine abstraite ». Et puis nous devons créer différentes usines en un seul endroit. En même temps, l'avantage est que les détails de mise en œuvre ne sont pas importants pour nous, c'est-à-dire peu importe l'usine spécifique que nous obtenons. L'essentiel est qu'il crée les bonnes implémentations. Super exemple :
Modèles de conception en Java - 3
Autrement dit, en fonction de l'environnement (système d'exploitation), nous recevrons une certaine usine qui créera des éléments compatibles. Comme alternative à l'approche consistant à créer par quelqu'un d'autre, nous pouvons utiliser le modèle " Prototype ". Son essence est simple : de nouveaux objets sont créés à l'image et à la ressemblance d'objets déjà existants, c'est-à-dire selon leur prototype. En Java, tout le monde a rencontré ce modèle - il s'agit de l'utilisation d'une interface java.lang.Cloneable:
class Main {
  public static void main(String[] args) {
    class CloneObject implements Cloneable {
      @Override
      protected Object clone() throws CloneNotSupportedException {
        return new CloneObject();
      }
    }
    CloneObject obj = new CloneObject();
    try {
      CloneObject pattern = (CloneObject) obj.clone();
    } catch (CloneNotSupportedException e) {
      //Do something
    }
  }
}
Comme vous pouvez le constater, l'appelant ne sait pas comment fonctionne le fichier clone. Autrement dit, la création d’un objet basé sur un prototype relève de la responsabilité de l’objet lui-même. Ceci est utile car cela ne lie pas l'utilisateur à l'implémentation de l'objet modèle. Eh bien, le tout dernier de cette liste est le modèle « Singleton ». Son objectif est simple : fournir une seule instance de l'objet pour l'ensemble de l'application. Ce modèle est intéressant car il montre souvent des problèmes de multithreading. Pour un examen plus approfondi, consultez ces articles :
Modèles de conception en Java - 4

Modèles structurels

Avec la création d’objets, c’est devenu plus clair. Il est désormais temps d’examiner les modèles structurels. Leur objectif est de construire des hiérarchies de classes et leurs relations faciles à prendre en charge. L'un des premiers modèles les plus connus est le « adjoint » (proxy). Le proxy a la même interface que l'objet réel, cela ne fait donc aucune différence pour le client de travailler via le proxy ou directement. L'exemple le plus simple est java.lang.reflect.Proxy :
import java.util.*;
import java.lang.reflect.*;
class Main {
  public static void main(String[] arguments) {
    final Map<String, String> original = new HashMap<>();
    InvocationHandler proxy = (obj, method, args) -> {
      System.out.println("Invoked: " + method.getName());
      return method.invoke(original, args);
    };
    Map<String, String> proxyInstance = (Map) Proxy.newProxyInstance(
        original.getClass().getClassLoader(),
        original.getClass().getInterfaces(),
        proxy);
    proxyInstance.put("key", "value");
    System.out.println(proxyInstance.get("key"));
  }
}
Comme vous pouvez le voir, dans l'exemple que nous avons original - c'est celui HashMapqui implémente l'interface Map. Nous créons ensuite un proxy qui remplace celui d'origine HashMappour la partie client, qui appelle les méthodes putet get, en ajoutant notre propre logique lors de l'appel. Comme nous pouvons le voir, l’interaction dans le modèle se produit via les interfaces. Mais parfois, un substitut ne suffit pas. Et puis le motif " Décorateur " peut être utilisé. Un décorateur est également appelé emballage ou emballage. Proxy et décorateur sont très similaires, mais si vous regardez l'exemple, vous verrez la différence :
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    List<String> list = new ArrayList<>();
    List<String> decorated = Collections.checkedList(list, String.class);
    decorated.add("2");
    list.add("3");
    System.out.println(decorated);
  }
}
Contrairement à un proxy, un décorateur s'enroule autour de quelque chose qui est transmis en entrée. Un proxy peut à la fois accepter ce qui doit être proxy et également gérer la vie de l'objet proxy (par exemple, créer un objet proxy). Il existe un autre modèle intéressant - « Adaptateur ». C'est similaire à un décorateur - le décorateur prend un objet en entrée et renvoie un wrapper sur cet objet. La différence est que le but n’est pas de changer la fonctionnalité, mais d’adapter une interface à une autre. Java en a un exemple très clair :
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    String[] array = {"One", "Two", "Three"};
    List<String> strings = Arrays.asList(array);
    strings.set(0, "1");
    System.out.println(Arrays.toString(array));
  }
}
En entrée nous avons un tableau. Ensuite, nous créons un adaptateur qui amène le tableau à l'interface List. Lorsque nous travaillons avec, nous travaillons en fait avec un tableau. Par conséquent, ajouter des éléments ne fonctionnera pas, car... Le tableau d'origine ne peut pas être modifié. Et dans ce cas, nous obtiendrons UnsupportedOperationException. La prochaine approche intéressante pour développer une structure de classe est le modèle composite . Il est intéressant dans la mesure où un certain ensemble d'éléments utilisant une interface sont disposés dans une certaine hiérarchie arborescente. En appelant une méthode sur un élément parent, nous obtenons un appel à cette méthode sur tous les éléments enfants nécessaires. Un excellent exemple de ce modèle est l'interface utilisateur (que ce soit java.awt ou JSF) :
import java.awt.*;
class Main {
  public static void main(String[] arguments) {
    Container container = new Container();
    Component component = new java.awt.Component(){};
    System.out.println(component.getComponentOrientation().isLeftToRight());
    container.add(component);
    container.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
    System.out.println(component.getComponentOrientation().isLeftToRight());
  }
}
Comme nous pouvons le voir, nous avons ajouté un composant au conteneur. Et puis nous avons demandé au conteneur d'appliquer la nouvelle orientation des composants. Et le conteneur, sachant de quels composants il se compose, a délégué l'exécution de cette commande à tous les composants enfants. Un autre motif intéressant est le motif « Bridge ». On l'appelle ainsi parce qu'il décrit une connexion ou un pont entre deux hiérarchies de classes différentes. L'une de ces hiérarchies est considérée comme une abstraction et l'autre comme une implémentation. Ceci est mis en évidence car l'abstraction elle-même n'effectue pas d'actions, mais délègue cette exécution à l'implémentation. Ce modèle est souvent utilisé lorsqu'il existe des classes « contrôle » et plusieurs types de classes « plateforme » (par exemple, Windows, Linux, etc.). Avec cette approche, l'une de ces hiérarchies (abstraction) recevra une référence à des objets d'une autre hiérarchie (implémentation) et leur déléguera le travail principal. Étant donné que toutes les implémentations suivront une interface commune, elles pourront être interchangées au sein de l’abstraction. En Java, un exemple clair de ceci estjava.awt :
Modèles de conception en Java - 5
Pour plus d'informations, consultez l'article « Modèles dans Java AWT ». Parmi les motifs structurels, je voudrais également noter le motif « Façade ». Son essence est de cacher la complexité de l'utilisation des bibliothèques/frameworks derrière cette API derrière une interface pratique et concise. Par exemple, vous pouvez utiliser JSF ou EntityManager de JPA comme exemple. Il existe également un autre modèle appelé « Flyweight ». Son essence est que si différents objets ont le même état, il peut alors être généralisé et stocké non pas dans chaque objet, mais au même endroit. Et puis chaque objet pourra référencer une partie commune, ce qui réduira les coûts mémoire pour le stockage. Ce modèle implique souvent la pré-mise en cache ou la maintenance d'un pool d'objets. Fait intéressant, nous connaissons également ce modèle depuis le tout début :
Modèles de conception en Java - 6
Par la même analogie, un pool de chaînes peut être inclus ici. Vous pouvez lire l'article sur ce sujet : « Flyweight Design Pattern ».
Modèles de conception en Java - 7

Modèles comportementaux

Nous avons donc compris comment créer des objets et comment organiser les connexions entre les classes. La chose la plus intéressante qui reste est d'offrir une flexibilité dans la modification du comportement des objets. Et les modèles de comportement nous y aideront. L'un des modèles les plus fréquemment mentionnés est le modèle « Stratégie ». C'est ici que commence l'étude des modèles dans le livre « Head First. Design Patterns ». En utilisant le modèle « Stratégie », nous pouvons stocker à l'intérieur d'un objet comment nous allons effectuer l'action, c'est-à-dire l'objet à l'intérieur stocke une stratégie qui peut être modifiée pendant l'exécution du code. Il s'agit d'un modèle que nous utilisons souvent lorsque nous utilisons un comparateur :
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Comparator<String> comparator = Comparator.comparingInt(String::length);
    Set dataSet = new TreeSet(comparator);
    dataSet.addAll(data);
    System.out.println("Dataset : " + dataSet);
  }
}
Avant nous - TreeSet. Il a le comportement de TreeSetmaintenir l'ordre des éléments, c'est-à-dire les trie (puisqu'il s'agit d'un SortedSet). Ce comportement a une stratégie par défaut, que l'on voit dans le JavaDoc : le tri en "ordre naturel" (pour les chaînes, il s'agit d'un ordre lexicographique). Cela se produit si vous utilisez un constructeur sans paramètre. Mais si on veut changer de stratégie, on peut passer Comparator. Dans cet exemple, nous pouvons créer notre ensemble en tant que new TreeSet(comparator), puis l'ordre de stockage des éléments (stratégie de stockage) passera à celui spécifié dans le comparateur. Fait intéressant, il existe presque le même modèle appelé « État ». Le modèle « État » dit que si nous avons un comportement dans l'objet principal qui dépend de l'état de cet objet, alors nous pouvons décrire l'état lui-même comme un objet et modifier l'objet d'état. Et déléguez les appels de l'objet principal à l'état. Un autre modèle que nous connaissons grâce à l'étude des bases mêmes du langage Java est le modèle « Command ». Ce modèle de conception suggère que différentes commandes peuvent être représentées comme différentes classes. Ce modèle est très similaire au modèle Stratégie. Mais dans le modèle Stratégie, nous redéfinissons la manière dont une action spécifique serait effectuée (par exemple, trier dans TreeSet). Dans le modèle « Commande », nous redéfinissons quelle action sera effectuée. La commande pattern nous accompagne tous les jours lorsque nous utilisons des threads :
import java.util.*;
class Main {
  public static void main(String[] args) {
    Runnable command = () -> {
      System.out.println("Command action");
    };
    Thread th = new Thread(command);
    th.start();
  }
}
Comme vous pouvez le voir, command définit une action ou une commande qui sera exécutée dans un nouveau thread. Il convient également de considérer le modèle « Chaîne de responsabilité ». Ce modèle est également très simple. Ce modèle indique que si quelque chose doit être traité, vous pouvez alors rassembler les gestionnaires dans une chaîne. Par exemple, ce modèle est souvent utilisé dans les serveurs Web. A l'entrée, le serveur reçoit une demande de l'utilisateur. Cette demande transite ensuite par la chaîne de traitement. Cette chaîne de gestionnaires comprend des filtres (par exemple, ne pas accepter les requêtes d'une liste noire d'adresses IP), des gestionnaires d'authentification (autoriser uniquement les utilisateurs autorisés), un gestionnaire d'en-tête de requête, un gestionnaire de mise en cache, etc. Mais il existe un exemple plus simple et plus compréhensible en Java java.util.logging:
import java.util.logging.*;
class Main {
  public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());
    ConsoleHandler consoleHandler = new ConsoleHandler(){
		@Override
            public void publish(LogRecord record) {
                System.out.println("LogRecord обработан");
            }
        };
    logger.addHandler(consoleHandler);
    logger.info("test");
  }
}
Comme vous pouvez le constater, les gestionnaires sont ajoutés à la liste des gestionnaires de loggers. Lorsqu'un enregistreur reçoit un message à traiter, chacun de ces messages passe par une chaîne de gestionnaires (depuis logger.getHandlers) pour cet enregistreur. Un autre modèle que nous voyons quotidiennement est “ Itérateur ”. Son essence est de séparer une collection d'objets (c'est-à-dire une classe représentant une structure de données. Par exemple List) et de parcourir cette collection.
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Iterator<String> iterator = data.iterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}
Comme vous pouvez le constater, l'itérateur ne fait pas partie de la collection, mais est représenté par une classe distincte qui parcourt la collection. L'utilisateur de l'itérateur peut même ne pas savoir sur quelle collection il itère, c'est-à-dire quelle collection visite-t-il ? Le modèle Visiteur mérite également d'être pris en compte . Le modèle de visiteur est très similaire au modèle d’itérateur. Ce modèle vous aide à contourner la structure des objets et à effectuer des actions sur ces objets. Ils diffèrent plutôt par leur concept. L'itérateur parcourt la collection de sorte que le client qui l'utilise ne se soucie pas de ce que contient la collection, seuls les éléments de la séquence sont importants. Le visiteur signifie qu'il existe une certaine hiérarchie ou structure des objets que nous visitons. Par exemple, nous pouvons utiliser un traitement de répertoire séparé et un traitement de fichier séparé. Java a une implémentation prête à l'emploi de ce modèle sous la formejava.nio.file.FileVisitor :
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
class Main {
  public static void main(String[] args) {
    SimpleFileVisitor visitor = new SimpleFileVisitor() {
      @Override
      public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
        System.out.println("File:" + file.toString());
        return FileVisitResult.CONTINUE;
      }
    };
    Path pathSource = Paths.get(System.getProperty("java.io.tmpdir"));
    try {
      Files.walkFileTree(pathSource, visitor);
    } catch (AccessDeniedException e) {
      // skip
    } catch (IOException e) {
      // Do something
    }
  }
}
Parfois, certains objets ont besoin de réagir aux changements survenus dans d'autres objets, et le modèle « Observateur » nous aidera alors . Le moyen le plus pratique consiste à fournir un mécanisme d'abonnement qui permet à certains objets de surveiller et de répondre aux événements se produisant dans d'autres objets. Ce modèle est souvent utilisé chez divers auditeurs et observateurs qui réagissent à différents événements. A titre d'exemple simple, on peut rappeler l'implémentation de ce pattern dès la première version du JDK :
import java.util.*;
class Main {
  public static void main(String[] args) {
    Observer observer = (obj, arg) -> {
      System.out.println("Arg: " + arg);
    };
    Observable target = new Observable(){
      @Override
      public void notifyObservers(Object arg) {
        setChanged();
        super.notifyObservers(arg);
      }
    };
    target.addObserver(observer);
    target.notifyObservers("Hello, World!");
  }
}
Il existe un autre modèle de comportement utile - « Médiateur ». C'est utile car dans les systèmes complexes, cela permet de supprimer la connexion entre différents objets et de déléguer toutes les interactions entre les objets à un objet, qui est un intermédiaire. L'une des applications les plus frappantes de ce modèle est Spring MVC, qui utilise ce modèle. Vous pouvez en savoir plus à ce sujet ici : " Spring : Mediator Pattern ". Vous pouvez souvent voir la même chose dans les exemplesjava.util.Timer :
import java.util.*;
class Main {
  public static void main(String[] args) {
    Timer mediator = new Timer("Mediator");
    TimerTask command = new TimerTask() {
      @Override
      public void run() {
        System.out.println("Command pattern");
        mediator.cancel();
      }
    };
    mediator.schedule(command, 1000);
  }
}
L'exemple ressemble davantage à un modèle de commande. Et l'essence du modèle "Mediator" est cachée dans la mise en œuvre de Timer'a. À l'intérieur du minuteur, il y a une file d'attente des tâches TaskQueue, il y a un fil de discussion TimerThread. Nous, en tant que clients de cette classe, n'interagissons pas avec eux, mais interagissons avec Timerl'objet qui, en réponse à notre appel à ses méthodes, accède aux méthodes des autres objets dont il est l'intermédiaire. Extérieurement, cela peut ressembler beaucoup à "Façade". Mais la différence est que lorsqu'une façade est utilisée, les composants ne savent pas que la façade existe et se parlent. Et lorsque "Mediator" est utilisé, les composants connaissent et utilisent l'intermédiaire, mais ne se contactent pas directement. Il vaut la peine de considérer le modèle « Template Method », qui ressort clairement de son nom. L'essentiel est que le code est écrit de telle manière que les utilisateurs du code (développeurs) disposent d'un modèle d'algorithme dont les étapes peuvent être redéfinies. Cela permet aux utilisateurs de code de ne pas écrire l'intégralité de l'algorithme, mais de réfléchir uniquement à la manière d'effectuer correctement l'une ou l'autre étape de cet algorithme. Par exemple, Java possède une classe abstraite AbstractListqui définit le comportement d'un itérateur par List. Cependant, l'itérateur lui-même utilise des méthodes feuilles telles que : get, set, remove. Le comportement de ces méthodes est déterminé par le développeur des descendants AbstractList. Ainsi, l'itérateur dans AbstractList- est un modèle pour l'algorithme d'itération sur une feuille. Et les développeurs d'implémentations spécifiques AbstractListmodifient le comportement de cette itération en définissant le comportement d'étapes spécifiques. Le dernier des modèles que nous analysons est le modèle « Snapshot » (Momento). Son essence est de préserver un certain état d'un objet avec la capacité de restaurer cet état. L'exemple le plus reconnaissable du JDK est la sérialisation d'objets, c'est-à-dire java.io.Serializable. Regardons un exemple :
import java.io.*;
import java.util.*;
class Main {
  public static void main(String[] args) throws IOException {
    ArrayList<String> list = new ArrayList<>();
    list.add("test");
    // Save State
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
      out.writeObject(list);
    }
    // Load state
    byte[] bytes = stream.toByteArray();
    InputStream inputStream = new ByteArrayInputStream(bytes);
    try (ObjectInputStream in = new ObjectInputStream(inputStream)) {
      List<String> listNew = (List<String>) in.readObject();
      System.out.println(listNew.get(0));
    } catch (ClassNotFoundException e) {
      // Do something. Can't find class fpr saved state
    }
  }
}
Modèles de conception en Java - 8

Conclusion

Comme nous l’avons vu lors de l’examen, il existe une grande variété de modèles. Chacun d'eux résout son propre problème. Et la connaissance de ces modèles peut vous aider à comprendre à temps comment écrire votre système pour qu'il soit flexible, maintenable et résistant au changement. Et enfin, quelques liens pour approfondir : #Viacheslav
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION