CodeGym/Blog Java/Random-ES/¿Qué son los antipatrones? Veamos algunos ejemplos (Parte...
John Squirrels
Nivel 41
San Francisco

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

Publicado en el grupo Random-ES
¡Buen día a todos! El otro día tuve una entrevista de trabajo, y me hicieron algunas preguntas sobre los antipatrones: qué son, qué tipos hay y qué ejemplos prácticos hay. Por supuesto, respondí la pregunta, pero muy superficialmente, ya que no había profundizado en este tema anteriormente. Después de la entrevista, comencé a buscar en Internet y me sumergí cada vez más en el tema. ¿Qué son los antipatrones?  Veamos algunos ejemplos (Parte 1) - 1 Hoy me gustaría brindar una breve descripción de los antipatrones más populares y revisar algunos ejemplos. Espero que leyendo esto le dará el conocimiento que necesita en esta área. ¡Empecemos! Antes de discutir qué es un antipatrón, recordemos qué es un patrón de diseño. un patrón de diseñoes una solución arquitectónica repetible para problemas o situaciones comunes que surgen al diseñar una aplicación. Pero hoy no estamos hablando de ellos, sino de sus opuestos: antipatrones. Un antipatrón es un enfoque generalizado pero ineficaz, arriesgado y/o improductivo para resolver una clase de problemas comunes. En otras palabras, este es un patrón de errores (a veces también llamado trampa). Como regla general, los antipatrones se dividen en los siguientes tipos:
  1. Antipatrones arquitectónicos : estos antipatrones surgen a medida que se diseña la estructura de un sistema (generalmente por un arquitecto).
  2. Anti-patrones de gestión/organización : estos son anti-patrones en la gestión de proyectos, generalmente encontrados por varios gerentes (o grupos de gerentes).
  3. Antipatrones de desarrollo : estos antipatrones surgen cuando los programadores comunes implementan un sistema.
La gama completa de antipatrones es mucho más exótica, pero no los consideraremos todos hoy. Para los desarrolladores ordinarios, eso sería demasiado. Para empezar, consideremos un antipatrón de gestión como ejemplo.

1. Parálisis analítica

Análisis parálisisse considera un clásico antipatrón de gestión. Implica sobreanalizar la situación durante la planificación, de modo que no se tome ninguna decisión o acción, lo que esencialmente paraliza el proceso de desarrollo. Esto sucede a menudo cuando el objetivo es alcanzar la perfección y considerar absolutamente todo durante el período de análisis. Este antipatrón se caracteriza por caminar en círculos (un circuito cerrado común y corriente), revisar y crear modelos detallados, lo que a su vez interfiere con el flujo de trabajo. Por ejemplo, está tratando de predecir cosas a un nivel: pero ¿qué pasa si un usuario de repente quiere crear una lista de empleados basada en la cuarta y quinta letras de su nombre, incluida la lista de proyectos en los que pasó la mayor parte de las horas de trabajo? entre el Año Nuevo y el Día Internacional de la Mujer en los últimos cuatro años? En esencia, es s demasiado análisis. Aquí hay algunos consejos para combatir la parálisis del análisis:
  1. Debe definir una meta a largo plazo como un faro para la toma de decisiones, de modo que cada una de sus decisiones lo acerque a la meta en lugar de estancarlo.
  2. No te concentres en nimiedades (¿por qué tomar una decisión sobre un detalle insignificante como si fuera la decisión más importante de tu vida?)
  3. Establezca una fecha límite para una decisión.
  4. No intentes completar una tarea a la perfección, es mejor hacerlo muy bien.
No es necesario profundizar demasiado aquí, por lo que no consideraremos otros antipatrones gerenciales. Por lo tanto, sin ninguna introducción, pasaremos a algunos antipatrones arquitectónicos, porque es más probable que este artículo sea leído por futuros desarrolladores en lugar de administradores.

2. Dios objeto

Un objeto Dios es un antipatrón que describe una concentración excesiva de todo tipo de funciones y grandes cantidades de datos dispares (un objeto alrededor del cual gira la aplicación). Toma 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 clase enorme que hace de todo. Contiene consultas a la base de datos, así como algunos datos. También vemos el método de fachada findAllWithoutPageEn, que incluye lógica empresarial. Tal objeto de Dios se vuelve enorme y difícil de mantener adecuadamente. Tenemos que jugar con él en cada pieza de código. Muchos componentes del sistema dependen de él y están estrechamente acoplados con él. Cada vez es más difícil mantener dicho código. En tales casos, el código debe dividirse en clases separadas, cada una de las cuales tendrá un solo propósito. En este ejemplo, podemos dividir el objeto Dios 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 los datos:
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 adecuado trasladar el método con lógica de negocio a un 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. Hijo único

Un singleton es el patrón más simple. Garantiza que en una aplicación de subproceso único habrá una única instancia de una clase y proporciona un punto de acceso global a este objeto. Pero, ¿es un patrón o un antipatrón? Veamos las desventajas de este patrón:
  1. Estado global Cuando accedemos a la instancia de la clase, no sabemos el estado actual de esta clase. No sabemos quién lo ha cambiado ni cuándo. El estado puede no ser nada parecido a lo que esperamos. En otras palabras, la corrección de trabajar con un singleton depende del orden de los accesos al mismo. Esto significa que los subsistemas dependen unos de otros y, como resultado, un diseño se vuelve mucho más complejo.

  2. Un singleton viola los principios SOLID, el principio de responsabilidad única: además de sus funciones directas, la clase singleton también controla el número de instancias.

  3. La dependencia de una clase ordinaria en un singleton no es visible en la interfaz de la clase. Debido a que una instancia de singleton generalmente no se pasa como un argumento de método, sino que se obtiene directamente a través de getInstance(), debe ingresar a la implementación de cada método para identificar la dependencia de la clase en el singleton, simplemente mirando el público de una clase. contrato no es suficiente.

    La presencia de un singleton reduce la capacidad de prueba de la aplicación en su conjunto y de las clases que utilizan el singleton en particular. En primer lugar, no puede reemplazar el singleton con un objeto simulado. 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 el acoplamiento, y todo lo mencionado anteriormente no es más que una consecuencia del aumento del acoplamiento.

    Y si lo piensa, puede evitar usar un singleton. Por ejemplo, es bastante posible (y de hecho necesario) usar varios tipos de fábricas para controlar el número de instancias de un objeto.

    El mayor peligro radica en intentar construir una arquitectura de aplicación completa basada en singletons. Hay toneladas de maravillosas alternativas a este enfoque. El ejemplo más importante es Spring, es decir, sus contenedores IoC: son una solución natural al problema de controlar la creación de servicios, ya que en realidad son "fábricas de esteroides".

    Muchos debates interminables e irreconciliables se están librando ahora sobre este tema. Depende de usted decidir si un singleton es un patrón o un antipatrón.

    No nos detendremos en ello. En su lugar, pasaremos al último patrón de diseño de hoy: poltergeist.

4. duende

Un poltergeist es un antipatrón que involucra una clase sin sentido que se usa para llamar a métodos de otra clase o simplemente agrega una capa innecesaria de abstracción. Este antipatrón se manifiesta como objetos efímeros, desprovistos de estado. Estos objetos se utilizan a menudo para inicializar otros objetos más permanentes.
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 solo un intermediario y delegue su trabajo a otra persona? Lo eliminamos y trasladamos la poca funcionalidad que tenía a objetos longevos. A continuación, pasamos a los patrones que más nos interesan (como desarrolladores ordinarios), es decir, los antipatrones de desarrollo .

5. Codificación dura

Así que hemos llegado a esta terrible palabra: codificación dura. 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íficos. Esto complica enormemente la transferencia del código a otras configuraciones. Este antipatrón está estrechamente relacionado con los números mágicos (estos antipatrones suelen estar 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;
}
Duele, ¿no? Aquí codificamos nuestra configuración de conexión. Como resultado, el código funcionará correctamente solo con MySQL. Para cambiar la base de datos, necesitaremos sumergirnos en el código y cambiar todo manualmente. Una buena solución sería poner la configuración en un archivo aparte:
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 usar constantes.

6. Ancla de barco

En el contexto de los antipatrones, un ancla de barco significa mantener partes del sistema que ya no se usan después de realizar alguna optimización o refactorización. Además, algunas partes del código podrían conservarse "para uso futuro" en caso de que las necesite repentinamente. Esencialmente, esto convierte su código en un basurero. Ejemplo:
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 usa un método separado para fusionar los datos del usuario de la base de datos con los datos del usuario pasados ​​al método (si el usuario pasado al método de actualización tiene un campo nulo, entonces el valor del campo anterior se toma de la base de datos) . Entonces supongamos que hay un nuevo requisito de que los registros no deben fusionarse con los antiguos, sino que, incluso si hay campos nulos, se utilizan para sobrescribir los antiguos:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Esto significa que mergeUser ya no se usa, pero sería una lástima eliminarlo. ¿Qué pasa si este método (o la idea de este método) podría ser útil algún día? Dicho código solo complica los sistemas e introduce confusión, ya que esencialmente no tiene valor práctico. No debemos olvidar que dicho código con "piezas muertas" será difícil de pasar a un colega cuando se vaya a otro proyecto. La mejor manera de lidiar con las anclas de los barcos es refactorizar el código, es decir, eliminar secciones del código (desgarrador, lo sé). Además, al preparar el cronograma de desarrollo, es necesario tener en cuenta dichos anclajes (para asignar tiempo para ordenar).

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 de creación , un conjunto de objetos inicializados y listos para usar. Cuando una aplicación necesita un objeto, se toma de este grupo en lugar de volver a crearlo. Cuando un objeto ya no se necesita, no se destruye. En su lugar, se devuelve a la piscina. Este patrón generalmente se usa para objetos pesados ​​​​que requieren mucho tiempo para crearse cada vez que se necesitan, como cuando se conecta a una base de datos. Veamos un pequeño y sencillo ejemplo. Aquí hay 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);
   }
}
Esta clase se presenta en forma del patrón/antipatrón singleton anterior, es decir, sólo puede haber un objeto de este tipo. Utiliza ciertos Resourceobjetos. De forma predeterminada, el constructor llena el grupo con 4 instancias. Cuando obtiene un objeto, se elimina del grupo (si no hay ningún objeto disponible, se crea uno y se devuelve inmediatamente). Y al final, tenemos un método para devolver el objeto. Los objetos de recursos se ven así:
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;
   }
}
Aquí tenemos un pequeño objeto que contiene un mapa con nombres de patrones de diseño como clave y los enlaces de Wikipedia correspondientes como valor, así como métodos para acceder al mapa. Echemos un vistazo a la principal:
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);
   }
}
Todo aquí es lo suficientemente claro: obtenemos un objeto de grupo, obtenemos un objeto con recursos del grupo, obtenemos el mapa del objeto de recurso, hacemos algo con él y ponemos todo esto en su lugar en el grupo para su posterior reutilización. Listo, este es el patrón de diseño del grupo de objetos. Pero estábamos hablando de antipatrones, ¿no? Consideremos el siguiente caso en el método principal:
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);
Aquí, de nuevo, obtenemos un objeto Resource, obtenemos su mapa de patrones y hacemos algo con el mapa. Pero antes de volver a guardar el mapa en el grupo de objetos, se borra y luego se rellena con datos dañados, lo que hace que el objeto Resource no sea apto para su reutilización. Uno de los detalles principales de un conjunto de objetos es que cuando se devuelve un objeto, debe restaurarse a un estado adecuado para su posterior reutilización. Si los objetos devueltos a la piscina permanecen en un estado incorrecto o indefinido, nuestro diseño se denomina pozo negro de objetos. ¿Tiene algún sentido almacenar objetos que no son aptos para su reutilización? En esta situación, podemos hacer que el mapa interno sea inmutable en el constructor:
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);
}
Los intentos y el deseo de cambiar el contenido del mapa se desvanecerán gracias a la UnsupportedOperationException que generarán. Los antipatrones son trampas que los desarrolladores encuentran con frecuencia debido a una aguda falta de tiempo, descuido, inexperiencia o presión de los gerentes de proyecto. Las prisas, que son comunes, pueden generar grandes problemas para la aplicación en el futuro, por lo que debe conocer estos errores y evitarlos con anticipación. Con esto concluye la primera parte del artículo. Continuará...
Comentarios
  • Populares
  • Nuevas
  • Antiguas
Debes iniciar sesión para dejar un comentario
Esta página aún no tiene comentarios