JavaRush /Blog Java /Random-ES /¿Qué son los antipatrones? Veamos ejemplos (parte 1)

¿Qué son los antipatrones? Veamos ejemplos (parte 1)

Publicado en el grupo Random-ES
¿Qué son los antipatrones?  Veamos ejemplos (parte 1) - 1¡Buen día a todos! El otro día me entrevistaron y me hicieron una pregunta sobre los antipatrones: qué tipo de bestia es esta, cuáles son sus tipos y ejemplos en la práctica. Por supuesto respondí la pregunta, pero de manera muy superficial, ya que no profundicé mucho en el estudio de este tema. Después de la entrevista, comencé a buscar en Internet, sumergiéndome cada vez más en este tema. Hoy me gustaría hacer un breve repaso de los antipatrones más populares y sus ejemplos, cuya lectura puede aportaros los conocimientos necesarios sobre este tema. ¡Empecemos! Entonces, antes de discutir qué es un antipatrón, recordemos qué es un patrón. Un patrón es un diseño arquitectónico repetible para resolver problemas o situaciones comunes que surgen al diseñar una aplicación. Pero hoy no hablamos de ellos, sino de sus opuestos: los antipatrones. Un antipatrón es un enfoque común para resolver una clase de problemas comunes que son ineficaces, arriesgados o improductivos. En otras palabras, es un patrón de error (a veces también llamado trampa). ¿Qué son los antipatrones?  Veamos ejemplos (parte 1) - 2Como regla general, los antipatrones se dividen en los siguientes tipos:
  1. Antipatrones arquitectónicos : antipatrones arquitectónicos que surgen al diseñar la estructura de un sistema (generalmente por parte de un arquitecto).
  2. Antipatrón de gestión : antipatrones en el campo de la gestión, que suelen encontrar varios gerentes (o grupos de gerentes).
  3. Antipatrón de desarrollo : los antipatrones son problemas de desarrollo que surgen cuando los programadores comunes escriben un sistema.
El exotismo de los antipatrones es mucho más amplio, pero no los consideraremos hoy, ya que para los desarrolladores comunes esto resultará abrumador. Para empezar, tomemos un ejemplo de antipatrón en el campo de la gestión.

1. Parálisis analítica

La parálisis del análisis se considera un antipatrón organizacional clásico. Implica analizar excesivamente una situación al planificar de modo que no se tome ninguna decisión o acción, lo que esencialmente paraliza el desarrollo. Esto sucede a menudo cuando el objetivo es lograr la perfección y completar completamente el período de análisis. Este antipatrón se caracteriza por caminar en círculos (una especie de circuito cerrado), revisar y crear modelos detallados, lo que a su vez interfiere con el flujo de trabajo. Por ejemplo, estás tratando de predecir cosas como: ¿Qué pasa si el usuario de repente quiere crear una lista de empleados basada en la cuarta y quinta letra de su nombre, incluyendo en la lista los proyectos a los que dedicaron la mayor cantidad de horas de trabajo entre el ¿Año Nuevo y el 8 de marzo en los cuatro años anteriores? En esencia, se trata de un exceso de análisis. Un buen ejemplo de la vida real es cómo la parálisis del análisis llevó a Kodak a la quiebra . A continuación se ofrecen un par de pequeños consejos para combatir la parálisis del análisis:
  1. Es necesario definir un objetivo a largo plazo como un faro para la toma de decisiones, de modo que cada decisión que tomes te acerque a tu objetivo y no te obligue a marcar el tiempo.
  2. No te concentres en nimiedades (¿por qué tomar una decisión sobre un matiz menor como si fuera la última en tu vida?)
  3. Establezca una fecha límite para tomar una decisión.
  4. No intentes hacer una tarea a la perfección: es mejor hacerlo muy bien.
No profundizaremos demasiado ni consideraremos otros antipatrones de gestión ahora. Por lo tanto, sin preámbulos, pasemos a algunos antipatrones arquitectónicos, porque lo más probable es que este artículo sea leído por futuros desarrolladores, no por administradores.

2.Objeto de Dios

Objeto divino es un antipatrón que describe la concentración excesiva de demasiadas funciones dispares, almacenando una gran cantidad de datos diversos (el objeto alrededor del cual gira la aplicación). Pongamos un pequeño ejemplo:
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();
   }
}
Aquí vemos una especie de clase grande que hace todo a la vez. Contiene consultas a la Base de Datos, contiene algunos datos, también vemos un método de fachada findAllWithoutPageEncon lógica de negocios. Un objeto tan divino se vuelve enorme y difícil de sostener adecuadamente. Tenemos que modificarlo en cada fragmento de código: muchos nodos del sistema dependen de él y están estrechamente acoplados a él. Mantener dicho código es cada vez más difícil. En tales casos, es necesario dividirlo en clases separadas, cada una de las cuales tendrá un solo propósito (objetivo). En este ejemplo, puedes dividirlo en una clase 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());
   }

                               ........
}
Una clase que contiene datos y métodos para acceder a ellos:
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;
   }
                    ....
Y sería más apropiado trasladar el método con lógica empresarial al servicio:
private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3.único

Un singleton es el patrón más simple que garantiza que habrá una única instancia de alguna clase en una aplicación de un solo subproceso y proporciona un punto de acceso global a ese objeto. Puedes leer más sobre esto aquí . ¿Pero es esto un patrón o un antipatrón? ¿Qué son los antipatrones?  Veamos ejemplos (parte 1) - 3Veamos las desventajas de esta plantilla:
  1. Estado global. Cuando accedemos a una instancia de una clase, no sabemos el estado actual de esa clase ni quién la cambió ni cuándo, y es posible que ese estado no sea el que esperábamos. En otras palabras, la corrección de trabajar con un singleton depende del orden de las llamadas, lo que hace que los subsistemas dependan entre sí y, como resultado, aumenta seriamente la complejidad del desarrollo.

  2. Singleton viola uno de los principios SOLID (principio de responsabilidad única): la clase Singleton, además de cumplir con sus responsabilidades inmediatas, también controla el número de sus instancias.

  3. La dependencia de una clase normal de un singleton no es visible en la interfaz de clase. Dado que, por lo general, una instancia de un singleton no se pasa en los parámetros de un método, sino que se obtiene directamente, a través de getInstance(), para identificar la dependencia de una clase en un singleton, es necesario profundizar en la implementación de cada método, simplemente viendo el público. contrato del objeto no es suficiente.

    La presencia de un singleton reduce la capacidad de prueba de la aplicación en general y de las clases que utilizan el singleton en particular. En primer lugar, no se puede colocar un objeto simulado en lugar de un singleton y, en segundo lugar, si un singleton tiene una interfaz para cambiar su estado, las pruebas dependerán unas de otras.

    En otras palabras, un singleton aumenta la conectividad, y todo lo anterior no es más que una consecuencia de una mayor conectividad.

    Y si lo piensas bien, se puede evitar el uso de un singleton. Por ejemplo, para controlar el número de instancias de un objeto, es muy posible (y necesario) utilizar varios tipos de fábricas.

    El mayor peligro reside en el intento de construir toda la arquitectura de la aplicación basada en singletons. Hay muchas alternativas excelentes a este enfoque. El ejemplo más importante es Spring, concretamente sus contenedores IoC: allí el problema de controlar la creación de servicios se resuelve de forma natural, ya que, de hecho, son "fábricas de esteroides".

    Ahora bien, hay mucho holívar sobre este tema, por lo que depende de usted decidir si un singleton es un patrón o un antipatrón.

    Y no nos detendremos en ello y pasaremos al último patrón de diseño de hoy: poltergeist.

4. Duende

Poltergeist es un antipatrón de clase no útil que se usa para llamar a métodos de otra clase o simplemente agrega una capa innecesaria de abstracción. El antipatrón se manifiesta en forma de objetos de corta duración y desprovistos de estado. Estos objetos se utilizan a menudo para inicializar otros objetos más duraderos.
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);
   }
}
¿Por qué necesitamos un objeto que sea sólo un intermediario y delega su trabajo en otra persona? Lo eliminamos y trasladamos la pequeña funcionalidad que implementa a objetos de larga duración. A continuación, pasamos a los patrones que nos interesan más (como desarrolladores comunes): antipatrones de desarrollo .

5.Código duro

Entonces llegamos a esta terrible palabra: código rígido. La esencia de este antipatrón es que el código está fuertemente ligado a una configuración de hardware y/o entorno de sistema específico, lo que hace que sea muy difícil migrarlo a otras configuraciones. Este antipatrón está estrechamente relacionado con los números mágicos (a menudo están entrelazados). Ejemplo:
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;
}
Clavado, ¿no? Aquí establecemos directamente la configuración de nuestra conexión; como resultado, el código funcionará correctamente solo con MySQL, y para cambiar la base de datos necesitará ingresar el código y cambiar todo manualmente. Una buena solución sería poner las configuraciones en un archivo separado:
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Otra opción es moverlo a constantes.

6. Ancla de barco

Un ancla de barco en el contexto de antipatrones significa almacenar partes no utilizadas de un sistema que quedan después de alguna optimización o refactorización. Además, algunas partes del código podrían dejarse “para el futuro”, en caso de que tengas que volver a utilizarlas. Básicamente, esto convierte el código en un bote de basura. ¿Qué son los antipatrones?  Veamos ejemplos (parte 1) - 4Ejemplo:
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());
}
Tenemos un método de actualización que utiliza un método separado para fusionar los datos del usuario de la base de datos y el que vino para la actualización (si la persona que vino para la actualización tiene un campo vacío, entonces se escribe como el anterior de la base de datos). Y, por ejemplo, existía el requisito de que los registros no se fusionaran con los antiguos, sino que se sobrescribieran, incluso si había campos vacíos:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Como resultado, mergeUserya no se utiliza y es una pena eliminarlo: ¿y si (o su idea) sigue siendo útil? Este tipo de código sólo complica y confunde los sistemas y, en esencia, no proporciona ningún valor práctico. No debemos olvidar que ese código con “piezas muertas” será difícil de transferir a un colega cuando se vaya a otro proyecto. El mejor método para lidiar con las anclas de los barcos es la refactorización del código, es decir, eliminar estas secciones de código (ay, ay). Además, al planificar el desarrollo, es necesario tener en cuenta la aparición de dichos anclajes (deje tiempo para limpiar los relaves).

7.Pozo negro de objetos

Para describir este antipatrón, primero debe familiarizarse con el patrón del grupo de objetos . Un grupo de objetos (grupo de recursos) es un patrón de diseño generativo , un conjunto de objetos inicializados y listos para su uso. Cuando una aplicación requiere un objeto, no se crea de nuevo, sino que se toma de este grupo. Cuando un objeto ya no es necesario, no se destruye, sino que se devuelve al grupo. Normalmente se utiliza para objetos pesados ​​que requieren muchos recursos para crear cada vez, como una conexión de base de datos. Veamos un ejemplo pequeño y sencillo a modo de ejemplo. Entonces tenemos una clase que representa este patrón:
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);
   }
}
Presentamos esta clase en la forma del patrón/antipatrón singleton descrito anteriormente , es decir, solo puede haber un objeto de este tipo, opera sobre ciertos objetos Resource, por defecto en el constructor el grupo se llena con 4 copias; cuando se toma un objeto de este tipo, se elimina del grupo (si no está allí, se crea y se regala inmediatamente), y al final hay un método para devolver el objeto. Los objetos Resourcese ven así:
public class Resource {
   private Map<String, String> patterns;
   public Resource() {
       patterns = new HashMap<>();
       patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
       patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
       patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
       patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
   }
   public Map<String, String> getPatterns() {
       return patterns;
   }
   public void setPatterns(Map<String, String> patterns) {
       this.patterns = patterns;
   }
}
Aquí tenemos un pequeño objeto que contiene un mapa con los nombres de los patrones como clave y enlaces a ellos como valor, así como métodos para acceder al mapa. Miremos main:
class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

       Resource firstResource = pool.acquireResource();
       Map<String, String> firstPatterns = firstResource.getPatterns();
       // ......Cómoим-то образом используем нашу мапу.....
       pool.releaseResource(firstResource);

       Resource secondResource = pool.acquireResource();
       Map<String, String> secondPatterns = firstResource.getPatterns();
       // ......Cómoим-то образом используем нашу мапу.....
       pool.releaseResource(secondResource);

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // ......Cómoим-то образом используем нашу мапу.....
       pool.releaseResource(thirdResource);
   }
}
Aquí todo también está claro: tomamos un objeto del grupo, extraemos un objeto con recursos, le quitamos un mapa, hacemos algo con él y lo volvemos a colocar todo en el grupo para su posterior reutilización. Voila: aquí tienes el patrón del grupo de objetos. Pero estábamos hablando de antipatrones, ¿no? Veamos este caso main:
Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// ......Cómoим-то образом используем нашу мапу.....
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
Aquí nuevamente, se toma un objeto de recurso, se toma su mapa con patrones y se hace algo con él, pero antes de volver a guardarlo en el grupo de objetos, el mapa se limpia y se llena con datos incomprensibles que hacen que este objeto de recurso no sea apto para su reutilización. Uno de los principales matices de un grupo de objetos es que una vez que se devuelve un objeto, debe devolverse a un estado adecuado para su posterior reutilización. Si los objetos se encuentran en un estado incorrecto o indefinido después de ser devueltos al grupo, esta construcción se denomina pozo negro de objetos. ¿Cuál es el punto de almacenar objetos que no son reutilizables? En esta situación, puedes hacer que el mapa interno sea inmutable en el constructor:
public Resource() {
   patterns = new HashMap<>();
   patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
   patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
   patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
   patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
   patterns = Collections.unmodifiableMap(patterns);
}
(Los intentos y deseos de cambiar el contenido caerán junto con UnsupportedOperationException). Los antipatrones son trampas en las que los desarrolladores suelen caer debido a una grave falta de tiempo, falta de atención, inexperiencia o patadas de los gerentes. La habitual falta de tiempo y prisas puede provocar grandes problemas para la aplicación en el futuro, por lo que estos errores deben conocerse y evitarse con antelación. ¿Qué son los antipatrones?  Veamos ejemplos (parte 1) - 6Con esto finaliza la primera parte del artículo: continuará .
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION