CodeGym /Java Blog /Random-IT /Cosa sono gli anti-pattern? Diamo un'occhiata ad alcuni e...
John Squirrels
Livello 41
San Francisco

Cosa sono gli anti-pattern? Diamo un'occhiata ad alcuni esempi (Parte 1)

Pubblicato nel gruppo Random-IT
Buongiorno a tutti! L'altro giorno ho fatto un colloquio di lavoro e mi sono state poste alcune domande sugli anti-pattern: cosa sono, quali tipi esistono e quali esempi pratici ci sono. Certo, ho risposto alla domanda, ma in modo molto superficiale, poiché non mi ero mai immerso in profondità in questo argomento. Dopo l'intervista, ho iniziato a setacciare Internet e mi sono immerso sempre di più nell'argomento. Cosa sono gli anti-pattern?  Vediamo alcuni esempi (Parte 1) - 1 Oggi vorrei fornire una breve panoramica degli anti-pattern più popolari e rivedere alcuni esempi. Spero che la lettura di questo ti darà le conoscenze di cui hai bisogno in questo settore. Iniziamo! Prima di discutere cos'è un anti-pattern, ricordiamo cos'è un design pattern. Un modello di progettazioneè una soluzione architetturale ripetibile per problemi o situazioni comuni che sorgono durante la progettazione di un'applicazione. Ma oggi non stiamo parlando di loro, ma piuttosto dei loro opposti: anti-schemi. Un anti-pattern è un approccio diffuso ma inefficace, rischioso e/o improduttivo per risolvere una classe di problemi comuni. In altre parole, questo è uno schema di errori (a volte chiamato anche trappola). Di norma, gli anti-pattern sono suddivisi nei seguenti tipi:
  1. Anti-pattern architettonici : questi anti-pattern sorgono quando viene progettata la struttura di un sistema (generalmente da un architetto).
  2. Anti-pattern gestionali/organizzativi : si tratta di anti-pattern nella gestione dei progetti, solitamente incontrati da vari manager (o gruppi di manager).
  3. Anti-pattern di sviluppo : questi anti-pattern sorgono quando un sistema viene implementato da normali programmatori.
L'intera gamma di anti-pattern è molto più esotica, ma oggi non li considereremo tutti. Per gli sviluppatori ordinari, sarebbe troppo. Per cominciare, consideriamo un anti-pattern di gestione come esempio.

1. Paralisi analitica

Paralisi dell'analisiè considerato un classico anti-pattern gestionale. Implica un'analisi eccessiva della situazione durante la pianificazione, in modo che non venga presa alcuna decisione o azione, paralizzando essenzialmente il processo di sviluppo. Questo accade spesso quando l'obiettivo è raggiungere la perfezione e considerare assolutamente tutto durante il periodo di analisi. Questo anti-pattern è caratterizzato dal camminare in cerchio (un ciclo chiuso ordinario), rivedere e creare modelli dettagliati, che a sua volta interferisce con il flusso di lavoro. Ad esempio, stai cercando di prevedere le cose a livello: ma cosa succede se un utente desidera improvvisamente creare un elenco di dipendenti basato sulla quarta e quinta lettera del loro nome, incluso l'elenco dei progetti su cui ha trascorso la maggior parte delle ore lavorative tra Capodanno e la Giornata internazionale della donna negli ultimi quattro anni? In sostanza, e' è troppa analisi. Ecco alcuni suggerimenti per combattere la paralisi da analisi:
  1. Devi definire un obiettivo a lungo termine come faro per il processo decisionale, in modo che ciascuna delle tue decisioni ti avvicini all'obiettivo piuttosto che farti ristagnare.
  2. Non concentrarti sulle sciocchezze (perché prendere una decisione su un dettaglio insignificante come se fosse la decisione più importante della tua vita?)
  3. Fissare un termine per una decisione.
  4. Non cercare di completare perfettamente un compito: è meglio farlo molto bene.
Non c'è bisogno di approfondire qui, quindi non prenderemo in considerazione altri anti-pattern manageriali. Pertanto, senza alcuna introduzione, passeremo ad alcuni anti-pattern architetturali, poiché è molto probabile che questo articolo venga letto da futuri sviluppatori piuttosto che da manager.

2. Dio oggetto

Un oggetto Dio è un anti-pattern che descrive un'eccessiva concentrazione di tutti i tipi di funzioni e grandi quantità di dati disparati (un oggetto attorno al quale ruota l'applicazione). Prendi 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 classe enorme che fa tutto. Contiene query di database e alcuni dati. Vediamo anche il metodo di facciata findAllWithoutPageEn, che include la logica di business. Un tale oggetto di Dio diventa enorme e scomodo da mantenere adeguatamente. Dobbiamo scherzare con esso in ogni pezzo di codice. Molti componenti del sistema fanno affidamento su di esso e sono strettamente collegati ad esso. Diventa sempre più difficile mantenere tale codice. In tali casi, il codice dovrebbe essere suddiviso in classi separate, ognuna delle quali avrà un solo scopo. In questo esempio, possiamo suddividere l'oggetto Dio 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 accedere ai dati:

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ù appropriato spostare il metodo con la logica aziendale su un 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. Garantisce che in un'applicazione a thread singolo ci sarà una singola istanza di una classe e fornisce un punto di accesso globale a questo oggetto. Ma è uno schema o un anti-schema? Diamo un'occhiata agli svantaggi di questo modello:
  1. Stato globale Quando accediamo all'istanza della classe, non conosciamo lo stato corrente di questa classe. Non sappiamo chi l'ha cambiato o quando. Lo stato potrebbe non essere come ci aspettiamo. In altre parole, la correttezza di lavorare con un singleton dipende dall'ordine degli accessi ad esso. Ciò significa che i sottosistemi dipendono l'uno dall'altro e, di conseguenza, un progetto diventa seriamente più complesso.

  2. Un singleton viola i principi SOLID - il principio di responsabilità singola: oltre ai suoi doveri diretti, la classe singleton controlla anche il numero di istanze.

  3. La dipendenza di una classe ordinaria da un singleton non è visibile nell'interfaccia della classe. Poiché un'istanza singleton di solito non viene passata come argomento del metodo, ma viene invece ottenuta direttamente tramite getInstance(), è necessario entrare nell'implementazione di ciascun metodo per identificare la dipendenza della classe dal singleton, semplicemente osservando il pubblico di una classe contratto non basta.

    La presenza di un singleton riduce la testabilità dell'applicazione nel suo complesso e delle classi che utilizzano il singleton in particolare. Prima di tutto, non puoi sostituire il singleton con un finto oggetto. In secondo luogo, se un singleton ha un'interfaccia per modificare il suo stato, i test dipenderanno l'uno dall'altro.

    In altre parole, un singleton aumenta l'accoppiamento e tutto ciò che è stato menzionato sopra non è altro che una conseguenza dell'aumento dell'accoppiamento.

    E se ci pensi, puoi evitare di usare un singleton. Ad esempio, è del tutto possibile (e in effetti necessario) utilizzare vari tipi di factory per controllare il numero di istanze di un oggetto.

    Il pericolo maggiore risiede nel tentativo di costruire un'intera architettura applicativa basata su singleton. Ci sono tantissime meravigliose alternative a questo approccio. L'esempio più importante è Spring, ovvero i suoi container IoC: sono una soluzione naturale al problema del controllo della creazione dei servizi, visto che sono in realtà delle "fabbriche sotto steroidi".

    Molti dibattiti interminabili e inconciliabili stanno ora infuriando su questo argomento. Sta a te decidere se un singleton è uno schema o un anti-schema.

    Non ci soffermeremo. Invece, passeremo all'ultimo modello di progettazione per oggi: poltergeist.

4. Poltergeist

Un poltergeist è un anti-pattern che coinvolge una classe inutile che viene utilizzata per chiamare metodi di un'altra classe o semplicemente aggiunge uno strato di astrazione non necessario. Questo anti-modello si manifesta come oggetti di breve durata, privi di stato. Questi oggetti vengono spesso utilizzati per inizializzare altri oggetti più permanenti.

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 trasferiamo la poca funzionalità che aveva su oggetti longevi. Successivamente, passiamo ai pattern che ci interessano di più (in quanto sviluppatori ordinari), ovvero gli anti-pattern di sviluppo .

5. Codifica difficile

Quindi siamo arrivati ​​a questa terribile parola: hard coding. L'essenza di questo anti-pattern è che il codice è fortemente legato a una specifica configurazione hardware e/o ambiente di sistema. Ciò complica notevolmente il porting del codice su altre configurazioni. Questo anti-modello è strettamente associato ai numeri magici (questi anti-pattern sono spesso 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;
}
Fa male, vero? Qui codifichiamo le nostre impostazioni di connessione. Di conseguenza, il codice funzionerà correttamente solo con MySQL. Per modificare il database, dovremo immergerci nel codice e modificare tutto manualmente. Una buona soluzione sarebbe mettere la configurazione 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 è usare le costanti.

6. Ancora della barca

Nel contesto degli anti-pattern, un'ancora per barche significa mantenere parti del sistema che non vengono più utilizzate dopo aver eseguito alcune ottimizzazioni o refactoring. Inoltre, alcune parti di codice potrebbero essere conservate "per uso futuro" nel caso in cui ne avessi improvvisamente bisogno. In sostanza, questo trasforma il tuo codice in una pattumiera. Esempio:

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 con i dati dell'utente passati al metodo (se l'utente passato al metodo di aggiornamento ha un campo nullo, il vecchio valore del campo viene preso dal database) . Supponiamo quindi che ci sia un nuovo requisito per cui i record non devono essere uniti a quelli vecchi, ma invece, anche se ci sono campi nulli, vengono utilizzati per sovrascrivere quelli vecchi:

public User update(Long id, User request) {
   return userDAO.update(user);
}
Ciò significa che mergeUser non è più utilizzato, ma sarebbe un peccato eliminarlo: e se questo metodo (o l'idea di questo metodo) potesse tornare utile un giorno? Tale codice complica solo i sistemi e introduce confusione, non avendo sostanzialmente alcun valore pratico. Non dobbiamo dimenticare che un tale codice con "pezzi morti" sarà difficile da trasmettere a un collega quando parti per un altro progetto. Il modo migliore per gestire le ancore delle barche è il refactoring del codice, ovvero eliminare sezioni di codice (straziante, lo so). Inoltre, quando si prepara il programma di sviluppo, è necessario tenere conto di tali ancore (per allocare il tempo per il riordino).

7. Pozzo nero dell'oggetto

Per descrivere questo anti-pattern, devi prima familiarizzare con il modello del pool di oggetti . Un pool di oggetti (pool di risorse) è un modello di progettazione creazionale , un insieme di oggetti inizializzati e pronti per l'uso. Quando un'applicazione necessita di un oggetto, viene prelevato da questo pool anziché essere ricreato. Quando un oggetto non è più necessario, non viene distrutto. Invece, viene restituito al pool. Questo modello viene in genere utilizzato per oggetti pesanti che richiedono molto tempo per essere creati ogni volta che sono necessari, ad esempio durante la connessione a un database. Diamo un'occhiata a un piccolo e semplice esempio. Ecco 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);
   }
}
Questa classe è presentata sotto forma del pattern/anti-pattern singleton di cui sopra , cioè può esserci un solo oggetto di questo tipo. Usa determinati Resourceoggetti. Per impostazione predefinita, il costruttore riempie il pool con 4 istanze. Quando ottieni un oggetto, viene rimosso dal pool (se non ci sono oggetti disponibili, ne viene creato uno e immediatamente restituito). E alla fine, abbiamo un metodo per rimettere a posto l'oggetto. Gli oggetti risorsa hanno questo aspetto:

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;
   }
}
Qui abbiamo un piccolo oggetto contenente una mappa con i nomi dei design pattern come chiave e i corrispondenti link di Wikipedia come valore, così come i metodi per accedere alla mappa. Diamo un'occhiata a 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);
   }
}
Tutto qui è abbastanza chiaro: otteniamo un oggetto pool, otteniamo un oggetto con risorse dal pool, otteniamo la mappa dall'oggetto Resource, facciamo qualcosa con esso e mettiamo tutto questo al suo posto nel pool per un ulteriore riutilizzo. Voilà, questo è il modello di progettazione del pool di oggetti. Ma stavamo parlando di anti-pattern, giusto? Consideriamo il seguente caso nel metodo principale:

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);
Qui, di nuovo, otteniamo un oggetto Resource, otteniamo la sua mappa di modelli e facciamo qualcosa con la mappa. Ma prima di salvare nuovamente la mappa nel pool di oggetti, viene cancellata e quindi popolata con dati danneggiati, rendendo l'oggetto Risorsa inadatto al riutilizzo. Uno dei dettagli principali di un pool di oggetti è che quando un oggetto viene restituito, deve essere ripristinato in uno stato adatto per un ulteriore riutilizzo. Se gli oggetti restituiti al pool rimangono in uno stato errato o indefinito, il nostro progetto viene chiamato pozzo nero degli oggetti. Ha senso conservare oggetti che non sono adatti al riutilizzo? In questa situazione, possiamo rendere immutabile la mappa interna nel costruttore:

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);
}
I tentativi e la voglia di modificare i contenuti della mappa svaniranno grazie alle UnsupportedOperationException che genereranno. Gli anti-pattern sono trappole che gli sviluppatori incontrano frequentemente a causa di un'acuta mancanza di tempo, disattenzione, inesperienza o pressioni da parte dei project manager. La fretta, che è comune, può portare a grossi problemi per l'applicazione in futuro, quindi è necessario conoscere questi errori ed evitarli in anticipo. Con questo si conclude la prima parte dell'articolo. Continua...
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION