CodeGym/Blogue Java/Random-PT/O que são antipadrões? Vejamos alguns exemplos (Parte 1)
John Squirrels
Nível 41
San Francisco

O que são antipadrões? Vejamos alguns exemplos (Parte 1)

Publicado no grupo Random-PT
Bom Dia a todos! Outro dia tive uma entrevista de emprego e me fizeram algumas perguntas sobre antipadrões: o que são, que tipos existem e que exemplos práticos existem. Claro, respondi à pergunta, mas muito superficialmente, já que não havia me aprofundado anteriormente neste tema. Após a entrevista, comecei a vasculhar a Internet e mergulhei cada vez mais no assunto. O que são antipadrões?  Vejamos alguns exemplos (Parte 1) - 1 Hoje, gostaria de fornecer uma breve visão geral dos antipadrões mais populares e revisar alguns exemplos. Espero que a leitura dê a você o conhecimento necessário nessa área. Vamos começar! Antes de discutirmos o que é um antipadrão, vamos relembrar o que é um padrão de projeto. Um padrão de designé uma solução arquitetônica repetível para problemas ou situações comuns que surgem ao projetar um aplicativo. Mas hoje não estamos falando sobre eles, mas sim sobre seus opostos — antipadrões. Um antipadrão é uma abordagem difundida, mas ineficaz, arriscada e/ou improdutiva para resolver uma classe de problemas comuns. Em outras palavras, esse é um padrão de erros (às vezes também chamado de armadilha). Como regra, os antipadrões são divididos nos seguintes tipos:
  1. Antipadrões arquitetônicos — Esses antipadrões surgem quando a estrutura de um sistema é projetada (geralmente por um arquiteto).
  2. Antipadrões de gerenciamento/organização — Esses são antipadrões no gerenciamento de projetos, geralmente encontrados por vários gerentes (ou grupos de gerentes).
  3. Antipadrões de desenvolvimento — Esses antipadrões surgem quando um sistema é implementado por programadores comuns.
A gama completa de antipadrões é muito mais exótica, mas não vamos considerá-los todos hoje. Para desenvolvedores comuns, isso seria demais. Para começar, vamos considerar um antipadrão de gerenciamento como exemplo.

1. Paralisia analítica

Paralisia de análiseé considerado um antipadrão de gerenciamento clássico. Envolve uma análise excessiva da situação durante o planejamento, de modo que nenhuma decisão ou ação seja tomada, paralisando essencialmente o processo de desenvolvimento. Isso geralmente acontece quando o objetivo é atingir a perfeição e considerar absolutamente tudo durante o período de análise. Esse antipadrão é caracterizado por andar em círculos (um loop fechado comum), revisar e criar modelos detalhados, o que, por sua vez, interfere no fluxo de trabalho. Por exemplo, você está tentando prever as coisas em um nível: mas e se um usuário de repente quiser criar uma lista de funcionários com base na quarta e na quinta letras de seus nomes, incluindo a lista de projetos nos quais eles passaram mais horas de trabalho? entre o Ano Novo e o Dia Internacional da Mulher nos últimos quatro anos? Em essência, é' é muita análise. Aqui estão algumas dicas para combater a paralisia da análise:
  1. Você precisa definir um objetivo de longo prazo como um farol para a tomada de decisões, de modo que cada uma de suas decisões o aproxime do objetivo, em vez de causar estagnação.
  2. Não se concentre em ninharias (por que tomar uma decisão sobre um detalhe insignificante como se fosse a decisão mais importante da sua vida?)
  3. Estabeleça um prazo para uma decisão.
  4. Não tente completar uma tarefa perfeitamente — é melhor fazê-la muito bem.
Não há necessidade de ir muito fundo aqui, então não vamos considerar outros antipadrões gerenciais. Portanto, sem nenhuma introdução, passaremos para alguns antipadrões de arquitetura, porque este artigo provavelmente será lido por futuros desenvolvedores em vez de gerentes.

2. Objeto de Deus

Um objeto God é um antipadrão que descreve uma concentração excessiva de todos os tipos de funções e grandes quantidades de dados díspares (um objeto em torno do qual o aplicativo gira). Veja um pequeno exemplo:
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();
   }
}
Aqui a gente vê uma turma enorme que faz de tudo. Ele contém consultas de banco de dados, bem como alguns dados. Também vemos o método de fachada findAllWithoutPageEn, que inclui a lógica de negócios. Tal objeto de Deus torna-se enorme e difícil de manter adequadamente. Temos que mexer com isso em cada parte do código. Muitos componentes do sistema dependem dele e estão fortemente acoplados a ele. Torna-se cada vez mais difícil manter esse código. Nesses casos, o código deve ser dividido em classes separadas, cada uma com apenas uma finalidade. Neste exemplo, podemos dividir o objeto Deus em uma 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());
   }

                               ........
}
Uma classe contendo dados e métodos para acessar os dados:
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;
   }
                    ....
E seria mais adequado mover o método com lógica de negócio para um serviço:
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

Um singleton é o padrão mais simples. Ele garante que em um aplicativo de thread único haverá uma única instância de uma classe e fornece um ponto de acesso global a esse objeto. Mas é um padrão ou um antipadrão? Vejamos as desvantagens desse padrão:
  1. Estado global Quando acessamos a instância da classe, não sabemos o estado atual desta classe. Não sabemos quem o mudou ou quando. O estado pode não ser nada parecido com o que esperamos. Em outras palavras, a correção de trabalhar com um singleton depende da ordem de acesso a ele. Isso significa que os subsistemas dependem uns dos outros e, como resultado, um projeto se torna muito mais complexo.

  2. Um singleton viola os princípios SOLID — o princípio da responsabilidade única: além de suas funções diretas, a classe singleton também controla o número de instâncias.

  3. A dependência de uma classe comum em um singleton não é visível na interface da classe. Como uma instância singleton geralmente não é passada como um argumento de método, mas, em vez disso, é obtida diretamente por meio de getInstance(), você precisa entrar na implementação de cada método para identificar a dependência da classe em relação ao singleton — apenas observando o public da classe contrato não é suficiente.

    A presença de um singleton reduz a capacidade de teste do aplicativo como um todo e das classes que usam o singleton em particular. Em primeiro lugar, você não pode substituir o singleton por um objeto fictício. Em segundo lugar, se um singleton tiver uma interface para alterar seu estado, os testes dependerão um do outro.

    Em outras palavras, um singleton aumenta o acoplamento, e tudo o que foi mencionado acima nada mais é do que uma consequência do aumento do acoplamento.

    E se você pensar sobre isso, você pode evitar usar um singleton. Por exemplo, é bem possível (e de fato necessário) usar vários tipos de fábricas para controlar o número de instâncias de um objeto.

    O maior perigo está na tentativa de construir toda uma arquitetura de aplicativo baseada em singletons. Existem inúmeras alternativas maravilhosas para essa abordagem. O exemplo mais importante é o Spring, nomeadamente os seus contentores IoC: são uma solução natural para o problema do controlo da criação de serviços, uma vez que são na verdade "fábricas com esteróides".

    Muitos debates intermináveis ​​e irreconciliáveis ​​estão agora travando sobre este assunto. Cabe a você decidir se um singleton é um padrão ou antipadrão.

    Não vamos nos demorar nisso. Em vez disso, passaremos para o último padrão de projeto de hoje — poltergeist.

4. Poltergeist

Um poltergeist é um antipadrão envolvendo uma classe sem sentido que é usada para chamar métodos de outra classe ou simplesmente adiciona uma camada desnecessária de abstração. Esse antipadrão se manifesta como objetos de vida curta, desprovidos de estado. Esses objetos geralmente são usados ​​para inicializar outros objetos mais 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 que precisamos de um objeto que é apenas um intermediário e delega seu trabalho a outra pessoa? Eliminamo-lo e transferimos a pouca funcionalidade que tinha para objetos de vida longa. Em seguida, passamos para os padrões que são de maior interesse para nós (como desenvolvedores comuns), ou seja, antipadrões de desenvolvimento .

5. Codificação rígida

Então chegamos a esta terrível palavra: hard coding. A essência desse antipadrão é que o código está fortemente vinculado a uma configuração de hardware e/ou ambiente de sistema específico. Isso complica muito a portabilidade do código para outras configurações. Esse antipadrão está intimamente associado a números mágicos (esses antipadrões geralmente estão interligados). Exemplo:
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;
}
Dói, não é? Aqui nós codificamos nossas configurações de conexão. Como resultado, o código funcionará corretamente apenas com o MySQL. Para alterar o banco de dados, precisaremos mergulhar no código e alterar tudo manualmente. Uma boa solução seria colocar a configuração em um arquivo 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
Outra opção é usar constantes.

6. Âncora do barco

No contexto dos antipadrões, uma âncora de barco significa manter partes do sistema que não são mais usadas após a execução de alguma otimização ou refatoração. Além disso, algumas partes do código podem ser mantidas "para uso futuro" caso você precise delas repentinamente. Essencialmente, isso transforma seu código em uma lata de lixo. Exemplo:
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());
}
Temos um método de atualização que usa um método separado para mesclar os dados do usuário do banco de dados com os dados do usuário passados ​​para o método (se o usuário passado para o método de atualização tiver um campo nulo, o valor do campo antigo é retirado do banco de dados) . Então, suponha que haja um novo requisito de que os registros não sejam mesclados com os antigos, mas, em vez disso, mesmo que haja campos nulos, eles sejam usados ​​para sobrescrever os antigos:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Isso significa que mergeUser não é mais usado, mas seria uma pena excluí-lo — e se esse método (ou a ideia desse método) puder ser útil algum dia? Tal código apenas complica os sistemas e introduz confusão, não tendo essencialmente nenhum valor prático. Não podemos esquecer que esse código com "pedaços mortos" será difícil de passar para um colega quando você partir para outro projeto. A melhor maneira de lidar com âncoras de barco é refatorar o código, ou seja, excluir seções do código (de partir o coração, eu sei). Além disso, ao preparar o cronograma de desenvolvimento, é necessário levar em conta essas âncoras (para alocar tempo para arrumar).

7. Fossa de objetos

Para descrever esse antipadrão, primeiro você precisa se familiarizar com o padrão de pool de objetos . Um pool de objetos (pool de recursos) é um padrão de design criacional , um conjunto de objetos inicializados e prontos para uso. Quando um aplicativo precisa de um objeto, ele é retirado desse pool em vez de ser recriado. Quando um objeto não é mais necessário, ele não é destruído. Em vez disso, ele é retornado ao pool. Esse padrão geralmente é usado para objetos pesados ​​que consomem tempo para serem criados toda vez que são necessários, como ao se conectar a um banco de dados. Vejamos um pequeno e simples exemplo. Aqui está uma classe que representa esse padrão:
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);
   }
}
Essa classe é apresentada na forma do padrão/antipadrão singleton acima, ou seja, só pode haver um objeto desse tipo. Ele usa certos Resourceobjetos. Por padrão, o construtor preenche o pool com 4 instâncias. Quando você obtém um objeto, ele é removido do pool (se não houver nenhum objeto disponível, um é criado e imediatamente retornado). E no final, temos um método para colocar o objeto de volta. Os objetos de recurso têm esta aparência:
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;
   }
}
Aqui temos um pequeno objeto contendo um mapa com nomes de padrões de design como a chave e os links correspondentes da Wikipédia como o valor, bem como métodos para acessar o mapa. Vamos dar uma olhada no 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);
   }
}
Tudo aqui é bastante claro: pegamos um objeto pool, pegamos um objeto com recursos do pool, pegamos o mapa do objeto Resource, fazemos algo com ele e colocamos tudo isso em seu lugar no pool para reutilização posterior. Voila, este é o padrão de design do pool de objetos. Mas estávamos falando sobre antipadrões, certo? Vamos considerar o seguinte caso no método 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);
Aqui, novamente, obtemos um objeto Resource, obtemos seu mapa de padrões e fazemos algo com o mapa. Porém, antes de salvar o mapa de volta no pool de objetos, ele é limpo e preenchido com dados corrompidos, tornando o objeto Recurso inadequado para reutilização. Um dos principais detalhes de um pool de objetos é que, quando um objeto é retornado, ele deve ser restaurado para um estado adequado para posterior reutilização. Se os objetos devolvidos ao pool permanecerem em um estado incorreto ou indefinido, nosso design será chamado de cesspool de objetos. Faz sentido armazenar objetos que não são adequados para reutilização? Nesta situação, podemos tornar o mapa interno imutável no construtor:
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);
}
As tentativas e o desejo de alterar o conteúdo do mapa desaparecerão graças ao UnsupportedOperationException que eles gerarão. Os antipadrões são armadilhas que os desenvolvedores encontram com frequência devido a uma aguda falta de tempo, descuido, inexperiência ou pressão dos gerentes de projeto. A pressa, que é comum, pode trazer grandes problemas para o aplicativo no futuro, então você precisa saber sobre esses erros e evitá-los com antecedência. Isso conclui a primeira parte do artigo. Continua...
Comentários
  • Populares
  • Novas
  • Antigas
Você precisa acessar para deixar um comentário
Esta página ainda não tem nenhum comentário