CodeGym/Blog Java/Random-FR/Qu'est-ce qu'un anti-modèle ? Regardons quelques exemples...
John Squirrels
Niveau 41
San Francisco

Qu'est-ce qu'un anti-modèle ? Regardons quelques exemples (Partie 1)

Publié dans le groupe Random-FR
membres
Bonne journée à tous! L'autre jour, j'ai eu un entretien d'embauche, et on m'a posé quelques questions sur les anti-modèles : ce qu'ils sont, quels types il y a et quels exemples pratiques il y a. Bien sûr, j'ai répondu à la question, mais très superficiellement, car je n'avais pas approfondi ce sujet auparavant. Après l'entretien, j'ai commencé à parcourir Internet et je me suis immergé de plus en plus dans le sujet. Qu'est-ce qu'un anti-modèle ?  Regardons quelques exemples (Partie 1) - 1 Aujourd'hui, je voudrais donner un bref aperçu des anti-modèles les plus populaires et passer en revue quelques exemples. J'espère que cette lecture vous donnera les connaissances dont vous avez besoin dans ce domaine. Commençons! Avant de discuter de ce qu'est un anti-pattern, rappelons ce qu'est un design pattern. Un modèle de conceptionest une solution architecturale reproductible pour les problèmes ou situations courants qui surviennent lors de la conception d'une application. Mais aujourd'hui, nous ne parlons pas d'eux, mais plutôt de leurs contraires - les anti-modèles. Un anti-modèle est une approche répandue mais inefficace, risquée et/ou improductive pour résoudre une classe de problèmes courants. En d'autres termes, il s'agit d'un schéma d'erreurs (aussi parfois appelé un piège). En règle générale, les anti-modèles sont divisés en types suivants :
  1. Anti-modèles architecturaux — Ces anti-modèles apparaissent lorsque la structure d'un système est conçue (généralement par un architecte).
  2. Anti-modèles de gestion/organisationnels — Il s'agit d'anti-modèles en gestion de projet, généralement rencontrés par divers managers (ou groupes de managers).
  3. Anti-modèles de développement — Ces anti-modèles apparaissent lorsqu'un système est implémenté par des programmeurs ordinaires.
La gamme complète des anti-modèles est beaucoup plus exotique, mais nous ne les considérerons pas tous aujourd'hui. Pour les développeurs ordinaires, ce serait trop. Pour commencer, considérons un anti-modèle de gestion comme exemple.

1. Paralysie analytique

Paralysie de l'analyseest considéré comme un anti-modèle de gestion classique. Cela implique une analyse excessive de la situation lors de la planification, de sorte qu'aucune décision ou action ne soit prise, paralysant essentiellement le processus de développement. Cela se produit souvent lorsque l'objectif est d'atteindre la perfection et de considérer absolument tout pendant la période d'analyse. Cet anti-modèle se caractérise par la marche en rond (une boucle fermée ordinaire), la révision et la création de modèles détaillés, ce qui à son tour interfère avec le flux de travail. Par exemple, vous essayez de prédire les choses à un certain niveau : mais que se passe-t-il si un utilisateur souhaite soudainement créer une liste d'employés basée sur les quatrième et cinquième lettres de son nom, y compris la liste des projets sur lesquels il a passé le plus d'heures de travail ? entre le Nouvel An et la Journée internationale de la femme au cours des quatre dernières années ? En gros, c'est c'est trop d'analyse. Voici quelques conseils pour lutter contre la paralysie de l'analyse :
  1. Vous devez définir un objectif à long terme comme balise pour la prise de décision, de sorte que chacune de vos décisions vous rapproche de l'objectif plutôt que de vous faire stagner.
  2. Ne vous concentrez pas sur des bagatelles (pourquoi prendre une décision sur un détail insignifiant comme si c'était la décision la plus importante de votre vie ?)
  3. Fixez un délai pour une décision.
  4. N'essayez pas d'accomplir une tâche parfaitement, il vaut mieux la faire très bien.
Inutile d'aller trop loin ici, nous ne considérerons donc pas d'autres anti-modèles managériaux. Par conséquent, sans aucune introduction, nous allons passer à quelques anti-modèles architecturaux, car cet article est plus susceptible d'être lu par de futurs développeurs plutôt que par des gestionnaires.

2. Objet divin

Un objet Dieu est un anti-modèle qui décrit une concentration excessive de toutes sortes de fonctions et de grandes quantités de données disparates (un objet autour duquel tourne l'application). Prenons un petit exemple :
public class SomeUserGodObject {
   private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
   private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";
   private static final String FIND_ALL_CUSTOMERS = "SELECT id, u.email, u.phone, u.first_name_en, u.middle_name_en, u.last_name_en, u.created_date" +
           "  WHERE u.id IN (SELECT up.user_id FROM user_permissions up WHERE up.permission_id = ?)";
   private static final String FIND_BY_EMAIL = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_dateFROM users WHERE email = ?";
   private static final String LIMIT_OFFSET = " LIMIT ? OFFSET ?";
   private static final String ORDER = " ORDER BY ISNULL(last_name_en), last_name_en, ISNULL(first_name_en), first_name_en, ISNULL(last_name_ru), " +
           "last_name_ru, ISNULL(first_name_ru), first_name_ru";
   private static final String CREATE_USER_EN = "INSERT INTO users(id, phone, email, first_name_en, middle_name_en, last_name_en, created_date) " +
           "VALUES (?, ?, ?, ?, ?, ?, ?)";
   private static final String FIND_ID_BY_LANG_CODE = "SELECT id FROM languages WHERE lang_code = ?";
                                  ........
   private final JdbcTemplate jdbcTemplate;
   private Map<String, String> firstName;
   private Map<String, String> middleName;
   private Map<String, String> lastName;
   private List<Long> permission;
                                   ........
   @Override
   public List<User> findAllEnCustomers(Long permissionId) {
       return jdbcTemplate.query( FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
   }
   @Override
   public List<User> findAllEn() {
       return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
   }
   @Override
   public Optional<List<User>> findAllEnByEmail(String email) {
       var query = FIND_ALL_USERS_EN + FIND_BY_EMAIL + ORDER;
       return Optional.ofNullable(jdbcTemplate.query(query, userRowMapper(), email));
   }
                              .............
   private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
       switch (type) {
           case USERS:
               return findAllEnUsers(permissionId);
           case CUSTOMERS:
               return findAllEnCustomers(permissionId);
           default:
               return findAllEn();
       }
   }
                              ..............private RowMapper<User> userRowMapperEn() {
       return (rs, rowNum) ->
               User.builder()
                       .id(rs.getLong("id"))
                       .email(rs.getString("email"))
                       .accessFailed(rs.getInt("access_counter"))
                       .createdDate(rs.getObject("created_date", LocalDateTime.class))
                       .firstName(rs.getString("first_name_en"))
                       .middleName(rs.getString("middle_name_en"))
                       .lastName(rs.getString("last_name_en"))
                       .phone(rs.getString("phone"))
                       .build();
   }
}
Ici, nous voyons une énorme classe qui fait tout. Il contient des requêtes de base de données ainsi que certaines données. Nous voyons également la méthode de façade findAllWithoutPageEn, qui inclut la logique métier. Un tel objet Dieu devient énorme et difficile à entretenir correctement. Nous devons jouer avec cela dans chaque morceau de code. De nombreux composants du système en dépendent et sont étroitement liés à celui-ci. Il devient de plus en plus difficile de maintenir un tel code. Dans de tels cas, le code doit être divisé en classes distinctes, chacune d'entre elles n'ayant qu'un seul objectif. Dans cet exemple, nous pouvons scinder l'objet God en une classe Dao :
public class UserDaoImpl {
   private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
   private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";

                                   ........
   private final JdbcTemplate jdbcTemplate;

                                   ........
   @Override
   public List<User> findAllEnCustomers(Long permissionId) {
       return jdbcTemplate.query(FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
   }
   @Override
   public List<User> findAllEn() {
       return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
   }

                               ........
}
Une classe contenant des données et des méthodes pour accéder aux données :
public class UserInfo {
   private Map<String, String> firstName;..
   public Map<String, String> getFirstName() {
       return firstName;
   }
   public void setFirstName(Map<String, String> firstName) {
       this.firstName = firstName;
   }
                    ....
Et il serait plus approprié de déplacer la méthode avec la logique métier vers un service :
private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3. Célibataire

Un singleton est le modèle le plus simple. Il garantit que dans une application à thread unique, il y aura une seule instance d'une classe, et il fournit un point d'accès global à cet objet. Mais est-ce un modèle ou un anti-modèle ? Regardons les inconvénients de ce modèle :
  1. Etat global Lorsque nous accédons à l'instance de la classe, nous ne connaissons pas l'état actuel de cette classe. Nous ne savons pas qui l'a changé ni quand. L'état peut ne pas être quelque chose comme ce que nous attendons. En d'autres termes, l'exactitude de travailler avec un singleton dépend de l'ordre d'accès à celui-ci. Cela signifie que les sous-systèmes dépendent les uns des autres et, par conséquent, une conception devient sérieusement plus complexe.

  2. Un singleton viole les principes SOLID - le principe de responsabilité unique : en plus de ses fonctions directes, la classe singleton contrôle également le nombre d'instances.

  3. La dépendance d'une classe ordinaire à un singleton n'est pas visible dans l'interface de la classe. Étant donné qu'une instance de singleton n'est généralement pas transmise en tant qu'argument de méthode, mais qu'elle est obtenue directement via getInstance (), vous devez entrer dans l'implémentation de chaque méthode afin d'identifier la dépendance de la classe vis-à-vis du singleton - il suffit de regarder le public d'une classe le contrat ne suffit pas.

    La présence d'un singleton réduit la testabilité de l'application dans son ensemble et des classes qui utilisent le singleton en particulier. Tout d'abord, vous ne pouvez pas remplacer le singleton par un objet factice. Deuxièmement, si un singleton a une interface pour changer son état, alors les tests dépendront les uns des autres.

    En d'autres termes, un singleton augmente le couplage, et tout ce qui est mentionné ci-dessus n'est rien de plus qu'une conséquence d'un couplage accru.

    Et si vous y réfléchissez, vous pouvez éviter d'utiliser un singleton. Par exemple, il est tout à fait possible (et même nécessaire) d'utiliser différents types de fabriques pour contrôler le nombre d'instances d'un objet.

    Le plus grand danger réside dans une tentative de construire une architecture d'application entière basée sur des singletons. Il existe des tonnes d'alternatives merveilleuses à cette approche. L'exemple le plus marquant est Spring, à savoir ses conteneurs IoC : ils sont une solution naturelle au problème de contrôle de la création de services, puisqu'il s'agit en fait d'"usines sous stéroïdes".

    De nombreux débats interminables et inconciliables font désormais rage à ce sujet. C'est à vous de décider si un singleton est un motif ou un anti-motif.

    Nous ne nous y attarderons pas. Au lieu de cela, nous allons passer au dernier modèle de conception pour aujourd'hui - poltergeist.

4. Poltergeist

Un poltergeist est un anti-modèle impliquant une classe inutile qui est utilisée pour appeler des méthodes d'une autre classe ou ajoute simplement une couche inutile d'abstraction. Cet anti-modèle se manifeste comme des objets éphémères, dépourvus d'état. Ces objets sont souvent utilisés pour initialiser d'autres objets plus permanents.
public class UserManager {
   private UserService service;
   public UserManager(UserService userService) {
       service = userService;
   }
   User createUser(User user) {
       return service.create(user);
   }
   Long findAllUsers(){
       return service.findAll().size();
   }
   String findEmailById(Long id) {
       return service.findById(id).getEmail();}
   User findUserByEmail(String email) {
       return service.findByEmail(email);
   }
   User deleteUserById(Long id) {
       return service.delete(id);
   }
}
Pourquoi avons-nous besoin d'un objet qui n'est qu'un intermédiaire et délègue son travail à quelqu'un d'autre ? Nous l'éliminons et transférons le peu de fonctionnalités qu'il avait à des objets à longue durée de vie. Ensuite, nous passons aux modèles qui nous intéressent le plus (en tant que développeurs ordinaires), c'est-à-dire les anti-modèles de développement .

5. Codage dur

Nous sommes donc arrivés à ce mot terrible : codage en dur. L'essence de cet anti-modèle est que le code est fortement lié à une configuration matérielle spécifique et/ou à un environnement système. Cela complique grandement le portage du code vers d'autres configurations. Cet anti-modèle est étroitement associé aux nombres magiques (ces anti-modèles sont souvent entrelacés). Exemple:
public Connection buildConnection() throws Exception {
   Class.forName("com.mysql.cj.jdbc.Driver");
   connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8&characterSetResults=UTF-8&serverTimezone=UTC", "user01", "12345qwert");
   return connection;
}
Ça fait mal, n'est-ce pas ? Ici, nous codons en dur nos paramètres de connexion. Par conséquent, le code ne fonctionnera correctement qu'avec MySQL. Pour changer la base de données, nous devrons plonger dans le code et tout changer manuellement. Une bonne solution serait de mettre la configuration dans un fichier séparé :
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Une autre option consiste à utiliser des constantes.

6. Ancre de bateau

Dans le contexte des anti-modèles, une ancre de bateau signifie conserver les parties du système qui ne sont plus utilisées après avoir effectué une optimisation ou une refactorisation. De plus, certaines parties du code pourraient être conservées "pour une utilisation future" au cas où vous en auriez soudainement besoin. Essentiellement, cela transforme votre code en poubelle. Exemple:
public User update(Long id, User request) {
   User user = mergeUser(findById(id), request);
   return userDAO.update(user);
}
private User mergeUser(User findUser, User requestUser) {
   return new User(
           findUser.getId(),
           requestUser.getEmail() != null ? requestUser.getEmail() : findUser.getEmail(),
           requestUser.getFirstName() != null ? requestUser.getFirstName() : findUser.getFirstNameRu(),
           requestUser.getMiddleName() != null ? requestUser.getMiddleName() : findUser.getMiddleNameRu(),
           requestUser.getLastName() != null ? requestUser.getLastName() : findUser.getLastNameEn(),
           requestUser.getPhone() != null ? requestUser.getPhone() : findUser.getPhone());
}
Nous avons une méthode de mise à jour qui utilise une méthode distincte pour fusionner les données utilisateur de la base de données avec les données utilisateur transmises à la méthode (si l'utilisateur transmis à la méthode de mise à jour a un champ nul, l'ancienne valeur de champ est extraite de la base de données) . Supposons ensuite qu'il existe une nouvelle exigence selon laquelle les enregistrements ne doivent pas être fusionnés avec les anciens, mais à la place, même s'il existe des champs nuls, ils sont utilisés pour écraser les anciens :
public User update(Long id, User request) {
   return userDAO.update(user);
}
Cela signifie que mergeUser n'est plus utilisé, mais il serait dommage de le supprimer — et si cette méthode (ou l'idée de cette méthode) pouvait être utile un jour ? Un tel code ne fait que compliquer les systèmes et introduit la confusion, n'ayant essentiellement aucune valeur pratique. Il ne faut pas oublier qu'un tel code avec des "morceaux morts" sera difficile à transmettre à un collègue lorsque vous partirez pour un autre projet. La meilleure façon de traiter les ancres de bateau est de refactoriser le code, c'est-à-dire de supprimer des sections de code (déchirant, je sais). De plus, lors de la préparation du planning de développement, il est nécessaire de tenir compte de ces ancres (pour allouer du temps de rangement).

7. Puisard d'objets

Pour décrire cet anti-modèle, vous devez d'abord vous familiariser avec le modèle de pool d'objets . Un pool d'objets (pool de ressources) est un design pattern créationnel , un ensemble d'objets initialisés et prêts à l'emploi. Lorsqu'une application a besoin d'un objet, celui-ci est extrait de ce pool au lieu d'être recréé. Lorsqu'un objet n'est plus nécessaire, il n'est pas détruit. Au lieu de cela, il est renvoyé dans la piscine. Ce modèle est généralement utilisé pour les objets lourds qui prennent du temps à créer chaque fois qu'ils sont nécessaires, comme lors de la connexion à une base de données. Prenons un petit exemple simple. Voici une classe qui représente ce modèle :
class ReusablePool {
   private static ReusablePool pool;
   private List<Resource> list = new LinkedList<>();
   private ReusablePool() {
       for (int i = 0; i < 3; i++)
           list.add(new Resource());
   }
   public static ReusablePool getInstance() {
       if (pool == null) {
           pool = new ReusablePool();
       }
       return pool;
   }
   public Resource acquireResource() {
       if (list.size() == 0) {
           return new Resource();
       } else {
           Resource r = list.get(0);
           list.remove(r);
           return r;
       }
   }
   public void releaseResource(Resource r) {
       list.add(r);
   }
}
Cette classe se présente sous la forme du motif/anti-motif singleton ci-dessus , c'est-à-dire qu'il ne peut y avoir qu'un seul objet de ce type. Il utilise certains Resourceobjets. Par défaut, le constructeur remplit le pool avec 4 instances. Lorsque vous obtenez un objet, il est supprimé du pool (s'il n'y a pas d'objet disponible, un est créé et immédiatement renvoyé). Et à la fin, nous avons une méthode pour remettre l'objet. Les objets ressources ressemblent à ceci :
public class Resource {
   private Map<String, String> patterns;
   public Resource() {
       patterns = new HashMap<>();
       patterns.put("proxy", "https://en.wikipedia.org/wiki/Proxy_pattern");
       patterns.put("bridge", "https://en.wikipedia.org/wiki/Bridge_pattern");
       patterns.put("facade", "https://en.wikipedia.org/wiki/Facade_pattern");
       patterns.put("builder", "https://en.wikipedia.org/wiki/Builder_pattern");
   }
   public Map<String, String> getPatterns() {
       return patterns;
   }
   public void setPatterns(Map<String, String> patterns) {
       this.patterns = patterns;
   }
}
Ici, nous avons un petit objet contenant une carte avec des noms de modèles de conception comme clé et des liens Wikipédia correspondants comme valeur, ainsi que des méthodes pour accéder à la carte. Jetons un coup d'œil à main:
class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

       Resource firstResource = pool.acquireResource();
       Map<String, String> firstPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(firstResource);

       Resource secondResource = pool.acquireResource();
       Map<String, String> secondPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(secondResource);

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // use our map somehow...
       pool.releaseResource(thirdResource);
   }
}
Tout ici est assez clair : nous obtenons un objet de pool, obtenons un objet avec des ressources du pool, récupérons la carte de l'objet Resource, faisons quelque chose avec et mettons tout cela à sa place dans le pool pour une réutilisation ultérieure. Voila, c'est le modèle de conception du pool d'objets. Mais nous parlions d'anti-modèles, n'est-ce pas ? Considérons le cas suivant dans la méthode main :
Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// use our map somehow...
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
Là encore, nous obtenons un objet Resource, nous obtenons sa carte de modèles et nous faisons quelque chose avec la carte. Mais avant de réenregistrer la carte dans le pool d'objets, elle est effacée puis remplie de données corrompues, ce qui rend l'objet Resource impropre à la réutilisation. L'un des principaux détails d'un pool d'objets est que lorsqu'un objet est renvoyé, il doit être restauré dans un état adapté à une réutilisation ultérieure. Si les objets retournés au pool restent dans un état incorrect ou indéfini, alors notre conception s'appelle un cloaque d'objets. Est-il judicieux de stocker des objets qui ne peuvent pas être réutilisés ? Dans cette situation, nous pouvons rendre la carte interne immuable dans le constructeur :
public Resource() {
   patterns = new HashMap<>();
   patterns.put("proxy", "https://en.wikipedia.org/wiki/Proxy_pattern");
   patterns.put("bridge", "https://en.wikipedia.org/wiki/Bridge_pattern");
   patterns.put("facade", "https://en.wikipedia.org/wiki/Facade_pattern");
   patterns.put("builder", "https://en.wikipedia.org/wiki/Builder_pattern");
   patterns = Collections.unmodifiableMap(patterns);
}
Les tentatives et le désir de modifier le contenu de la carte s'estomperont grâce à l'exception UnsupportedOperationException qu'ils généreront. Les anti-patterns sont des pièges que les développeurs rencontrent fréquemment en raison d'un manque de temps aigu, d'une négligence, d'une inexpérience ou de la pression des chefs de projet. La précipitation, qui est courante, peut entraîner de gros problèmes pour l'application à l'avenir, vous devez donc connaître ces erreurs et les éviter à l'avance. Ceci conclut la première partie de l'article. À suivre...
Commentaires
  • Populaires
  • Nouveau
  • Anciennes
Tu dois être connecté(e) pour laisser un commentaire
Cette page ne comporte pas encore de commentaires