JavaRush /Java Blog /Random-IT /Cosa sono gli antipattern? Diamo un'occhiata agli esempi ...

Cosa sono gli antipattern? Diamo un'occhiata agli esempi (parte 1)

Pubblicato nel gruppo Random-IT
Cosa sono gli antipattern?  Diamo un'occhiata agli esempi (parte 1) - 1Buongiorno a tutti! L'altro giorno sono stato intervistato e mi è stata posta una domanda sugli antipattern: che tipo di bestia è questa, quali sono i loro tipi ed esempi nella pratica. Naturalmente ho risposto alla domanda, ma in modo molto superficiale, poiché non ho approfondito lo studio di questa questione. Dopo l'intervista, ho iniziato a setacciare Internet, immergendomi sempre più in questo argomento. Oggi vorrei fare una breve rassegna degli antipattern più popolari e dei loro esempi, la cui lettura potrebbe darvi le conoscenze necessarie su questo tema. Iniziamo! Quindi, prima di discutere cos'è un antipattern, ricordiamo cos'è un pattern. Un modello è un progetto architettonico ripetibile per risolvere problemi o situazioni comuni che si presentano durante la progettazione di un'applicazione. Ma oggi non parliamo di loro, ma dei loro opposti: gli antimodelli. Un anti-pattern è un approccio comune per risolvere una classe di problemi comunemente riscontrati che è inefficace, rischioso o improduttivo. In altre parole, si tratta di un modello di errore (a volte chiamato anche trap). Cosa sono gli antipattern?  Diamo un'occhiata agli esempi (parte 1) - 2Di norma, gli antipattern sono suddivisi nei seguenti tipi:
  1. Antimodelli architettonici - antimodelli architettonici che emergono durante la progettazione della struttura di un sistema (solitamente da parte di un architetto).
  2. Management Anti Pattern - antipattern nel campo del management, che di solito incontrano diversi manager (o gruppi di manager).
  3. Antipattern di sviluppo : gli antipattern sono problemi di sviluppo che sorgono quando i normali programmatori scrivono un sistema.
L'esotismo degli antipattern è molto più ampio, ma non li considereremo oggi, poiché per gli sviluppatori ordinari sarà travolgente. Per cominciare, prendiamo un esempio di antipattern nel campo del management.

1. Paralisi analitica

La paralisi da analisi è considerata un classico anti-modello organizzativo. Si tratta di analizzare eccessivamente una situazione durante la pianificazione in modo che non venga intrapresa alcuna decisione o azione, paralizzando sostanzialmente lo sviluppo. Ciò accade spesso quando l'obiettivo è raggiungere la perfezione e completare il periodo di analisi. Questo anti-modello è caratterizzato dal camminare in tondo (una sorta di ciclo chiuso), rivedendo e creando modelli dettagliati, che a loro volta interferiscono con il flusso di lavoro. Ad esempio, stai cercando di prevedere cose del tipo: cosa succederebbe se l'utente volesse improvvisamente creare un elenco di dipendenti in base alla quarta e quinta lettera del loro nome, includendo nell'elenco i progetti a cui hanno dedicato più ore di lavoro tra il Capodanno e l'otto marzo dei quattro anni precedenti? In sostanza, questa è una sovrabbondanza di analisi. Un buon esempio di vita reale è il modo in cui la paralisi dell'analisi ha portato Kodak alla bancarotta . Ecco un paio di piccoli consigli per combattere la paralisi da analisi:
  1. Devi definire un obiettivo a lungo termine come un faro per il processo decisionale, in modo che ogni decisione che prendi ti avvicini al tuo obiettivo e non ti costringa a segnare il tempo.
  2. Non concentrarti sulle sciocchezze (perché prendere una decisione su una sfumatura minore come se fosse l’ultima della tua vita?)
  3. Fissare una scadenza per prendere una decisione.
  4. Non cercare di svolgere un compito perfettamente: è meglio farlo molto bene.
Non andremo troppo in profondità e non prenderemo in considerazione ora altri antipattern gestionali. Quindi, senza preamboli, passiamo ad alcuni antipattern architetturali, perché molto probabilmente questo articolo lo leggono i futuri sviluppatori, non i manager.

2.Oggetto di Dio

L'oggetto divino è un anti-pattern che descrive l'eccessiva concentrazione di troppe funzioni disparate, memorizzando una grande quantità di dati diversi (l'oggetto attorno al quale ruota l'applicazione). Facciamo un piccolo esempio:
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();
   }
}
Qui vediamo una sorta di grande classe che fa tutto in una volta. Contiene query al database, contiene alcuni dati, vediamo anche un metodo di facciata findAllWithoutPageEncon logica aziendale. Un oggetto così divino diventa enorme e goffo da sostenere adeguatamente. Dobbiamo modificarlo in ogni parte del codice: molti nodi del sistema fanno affidamento su di esso e sono strettamente collegati ad esso. Mantenere tale codice sta diventando sempre più difficile. In questi casi, è necessario dividerlo in classi separate, ciascuna delle quali avrà un solo scopo (obiettivo). In questo esempio, puoi suddividerlo in una 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());
   }

                               ........
}
Una classe contenente dati e metodi per accedervi:
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 sarebbe più opportuno spostare il metodo con logica di business nel servizio:
private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3.Singolo

Un singleton è il modello più semplice che garantisce che ci sarà una singola istanza di una classe in un'applicazione a thread singolo e fornisce un punto di accesso globale a quell'oggetto. Puoi leggere di più a riguardo qui . Ma si tratta di un modello o di un antimodello? Cosa sono gli antipattern?  Diamo un'occhiata agli esempi (parte 1) - 3Diamo un'occhiata agli svantaggi di questo modello:
  1. Stato globale. Quando accediamo a un'istanza di una classe, non conosciamo lo stato attuale di quella classe o chi l'ha modificata o quando, e quello stato potrebbe non essere quello che ci aspettiamo. In altre parole, la correttezza di lavorare con un singleton dipende dall'ordine delle chiamate ad esso, il che fa sì che i sottosistemi dipendano l'uno dall'altro e, di conseguenza, aumenta seriamente la complessità dello sviluppo.

  2. Singleton viola uno dei principi SOLID - Principio di responsabilità unica - la classe Singleton, oltre a svolgere le sue responsabilità immediate, controlla anche il numero delle sue istanze.

  3. La dipendenza di una classe normale da un singleton non è visibile nell'interfaccia della classe. Poiché solitamente un'istanza di un singleton non viene passata nei parametri di un metodo, ma viene ottenuta direttamente, tramite getInstance(), per identificare la dipendenza di una classe da un singleton, è necessario approfondire l'implementazione di ciascun metodo - semplicemente visualizzando il public il contratto dell'oggetto non è sufficiente.

    La presenza di un singleton riduce la testabilità dell'applicazione in generale e delle classi che utilizzano il singleton in particolare. In primo luogo, non è possibile inserire un oggetto Mock al posto di un singleton e, in secondo luogo, se un singleton ha un'interfaccia per modificare il proprio stato, i test dipenderanno l'uno dall'altro.

    In altre parole, un singleton aumenta la connettività e tutto quanto sopra non è altro che una conseguenza di una maggiore connettività.

    E se ci pensi, l'uso di un singleton può essere evitato. Ad esempio, per controllare il numero di istanze di un oggetto, è del tutto possibile (e necessario) utilizzare vari tipi di fabbriche.

    Il pericolo maggiore risiede nel tentativo di costruire l’intera architettura applicativa basata sui singleton. Esistono molte ottime alternative a questo approccio. L'esempio più importante è Spring, ovvero i suoi contenitori IoC: lì il problema del controllo della creazione dei servizi è risolto in modo naturale, poiché sono, di fatto, “fabbriche sotto steroidi”.

    Ora ci sono molti holivar su questo argomento, quindi spetta a te decidere se un singleton è un pattern o un antipattern.

    E non ci soffermeremo su questo e passeremo all'ultimo modello di progettazione di oggi: poltergeist.

4. Poltergeist

Poltergeist è un antipattern di classe non utile che viene utilizzato per chiamare metodi di un'altra classe o semplicemente aggiunge un livello di astrazione non necessario. L'antipattern si manifesta sotto forma di oggetti di breve durata privi di stato. Questi oggetti vengono spesso utilizzati per inizializzare altri oggetti più durevoli.
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);
   }
}
Perché abbiamo bisogno di un oggetto che sia solo un intermediario e deleghi il suo lavoro a qualcun altro? Lo eliminiamo e spostiamo le piccole funzionalità che implementa in oggetti di lunga durata. Successivamente, passiamo ai modelli che sono di maggiore interesse per noi (come sviluppatori ordinari): antipattern di sviluppo .

5.Codice rigido

Quindi siamo arrivati ​​a questa terribile parola: hardcode. L'essenza di questo antipattern è che il codice è fortemente legato a una specifica configurazione hardware e/o ambiente di sistema, il che rende molto difficile portarlo su altre configurazioni. Questo antipattern è strettamente correlato ai numeri magici (spesso sono intrecciati). Esempio:
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;
}
Inchiodato, non è vero? Qui impostiamo direttamente la configurazione della nostra connessione; di conseguenza il codice funzionerà correttamente solo con MySQL, e per modificare il database bisognerà entrare nel codice e modificare tutto manualmente. Una buona soluzione sarebbe quella di inserire le configurazioni in un file separato:
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Un'altra opzione è spostarlo in costanti.

6. Ancoraggio della barca

Un'ancora per barca nel contesto degli antipattern significa immagazzinare parti inutilizzate di un sistema che rimangono dopo qualche ottimizzazione o refactoring. Inoltre, alcune parti del codice potrebbero essere lasciate “per il futuro”, nel caso in cui dovessi riutilizzarle. Questo essenzialmente trasforma il codice in un cestino. Cosa sono gli antipattern?  Diamo un'occhiata agli esempi (parte 1) - 4Esempio:
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());
}
Abbiamo un metodo di aggiornamento che utilizza un metodo separato per unire i dati dell'utente dal database e quello venuto per l'aggiornamento (se la persona venuta per l'aggiornamento ha un campo vuoto, allora è scritto come quello vecchio dal database). E ad esempio, era richiesto che i record non venissero uniti con quelli vecchi, ma sovrascritti, anche se ci sono campi vuoti:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Di conseguenza mergeUsernon viene più utilizzato ed è un peccato cancellarlo: e se (o la sua idea) fosse ancora utile? Tale codice non fa altro che complicare e confondere i sistemi, essenzialmente senza fornire alcun valore pratico. Non dobbiamo dimenticare che tale codice con “pezzi morti” sarà difficile da trasferire a un collega quando partirai per un altro progetto. Il metodo migliore per gestire le ancore delle barche è il refactoring del codice, ovvero l'eliminazione di queste sezioni di codice (ahimè, ahimè). Inoltre, quando si pianifica lo sviluppo, è necessario tenere conto della presenza di tali ancoraggi (concedere il tempo per ripulire gli sterili).

7.Pozzo nero dell'oggetto

Per descrivere questo antipattern è necessario prima conoscere il pattern del pool di oggetti . Un pool di oggetti (pool di risorse) è un modello di progettazione generativa , un insieme di oggetti inizializzati e pronti per l'uso. Quando un'applicazione richiede un oggetto, questo non viene creato di nuovo, ma viene preso da questo pool. Quando un oggetto non è più necessario, non viene distrutto, ma restituito al pool. Utilizzato in genere per oggetti pesanti che richiedono un uso intensivo di risorse da creare ogni volta, ad esempio una connessione al database. Diamo un'occhiata a un piccolo e semplice esempio per amore di esempio. Quindi abbiamo una classe che rappresenta questo modello:
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);
   }
}
Presentiamo questa classe nella forma del pattern/antipattern singleton sopra descritto , cioè può esserci un solo oggetto di questo tipo, funziona su determinati oggetti Resource, per impostazione predefinita nel costruttore il pool è riempito con 4 copie; quando un oggetto del genere viene preso, viene tolto dalla riserva (se non c'è, viene creato e subito regalato), e alla fine c'è un metodo per rimettere l'oggetto. Gli oggetti Resourceappaiono così:
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;
   }
}
Qui abbiamo un piccolo oggetto contenente una mappa con i nomi dei pattern come chiave e i collegamenti ad essi come valore, oltre ai metodi per accedere alla mappa. Guardiamo main:
class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

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

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

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // ......Howим-то образом используем нашу мапу.....
       pool.releaseResource(thirdResource);
   }
}
Anche qui tutto è chiaro: prendiamo un oggetto del pool, estraiamo da esso un oggetto con risorse, ne prendiamo una mappa, facciamo qualcosa con esso e rimettiamo tutto nel pool per un ulteriore riutilizzo. Voila: qui hai il modello del pool di oggetti. Ma stavamo parlando di antipattern, no? Diamo un'occhiata a questo caso main:
Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// ......Howим-то образом используем нашу мапу.....
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
Anche in questo caso, viene preso un oggetto risorsa, viene presa la sua mappa con modelli e viene fatto qualcosa con esso, ma prima di salvare nuovamente nel pool di oggetti, la mappa viene pulita e riempita con dati incomprensibili che rendono questo oggetto Risorsa inadatto al riutilizzo. Una delle principali sfumature di un pool di oggetti è che dopo che un oggetto è stato restituito, deve essere riportato in uno stato adatto per un ulteriore riutilizzo. Se gli oggetti si trovano in uno stato errato o indefinito dopo essere stati restituiti al pool, questo costrutto è chiamato pozzo nero dell'oggetto. Qual è lo scopo di conservare oggetti che non sono riutilizzabili? In questa situazione, puoi rendere immutabile la mappa interna nel costruttore:
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);
}
(i tentativi e il desiderio di modificare il contenuto cadranno insieme a UnsupportedOperationException). Gli antipattern sono trappole in cui gli sviluppatori spesso cadono a causa di una grave mancanza di tempo, disattenzione, inesperienza o disagi da parte dei manager. La consueta mancanza di tempo e fretta può comportare grossi problemi per l'applicazione in futuro, quindi questi errori devono essere conosciuti ed evitati in anticipo. Cosa sono gli antipattern?  Diamo un'occhiata agli esempi (parte 1) - 6Con questo si chiude la prima parte dell'articolo: continua .
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION