CodeGym/Blog Java/Aleatoriu/Ce sunt anti-modele? Să ne uităm la câteva exemple (Parte...
John Squirrels
Nivel
San Francisco

Ce sunt anti-modele? Să ne uităm la câteva exemple (Partea 1)

Publicat în grup
Ziua bună tuturor! Zilele trecute am avut un interviu de angajare și mi s-au pus câteva întrebări despre anti-pattern: ce sunt, ce tipuri există și ce exemple practice există. Bineînțeles, am răspuns la întrebare, dar foarte superficial, din moment ce nu mă scufundasem anterior în acest subiect. După interviu, am început să cercetez internetul și m-am cufundat din ce în ce mai mult în subiect. Ce sunt anti-modele?  Să ne uităm la câteva exemple (Partea 1) - 1 Astăzi aș dori să ofer o scurtă prezentare generală a celor mai populare anti-modele și să trec în revistă câteva exemple. Sper că citirea acestui articol vă va oferi cunoștințele de care aveți nevoie în acest domeniu. Să începem! Înainte de a discuta despre ce este un anti-model, să ne amintim ce este un model de design. Un model de designeste o soluție arhitecturală repetabilă pentru problemele comune sau situațiile care apar la proiectarea unei aplicații. Dar astăzi nu vorbim despre ele, ci mai degrabă despre contrariile lor - anti-tipare. Un anti-model este o abordare larg răspândită, dar ineficientă, riscantă și/sau neproductivă pentru rezolvarea unei clase de probleme comune. Cu alte cuvinte, acesta este un model de greșeli (uneori numit și o capcană). De regulă, anti-modelele sunt împărțite în următoarele tipuri:
  1. Anti-modele arhitecturale — Aceste anti-modele apar pe măsură ce structura unui sistem este proiectată (în general de către un arhitect).
  2. Anti-modele de management/organizație — Acestea sunt anti-modele în managementul proiectelor, întâlnite de obicei de diverși manageri (sau grupuri de manageri).
  3. Anti-modele de dezvoltare — Aceste anti-modele apar pe măsură ce un sistem este implementat de programatori obișnuiți.
Gama completă de anti-modare este mult mai exotică, dar nu le vom lua în considerare pe toate astăzi. Pentru dezvoltatorii obișnuiți, asta ar fi prea mult. Pentru început, să luăm ca exemplu un anti-model de management.

1. Paralizie analitică

Paralizie de analizăeste considerat un anti-pattern clasic de management. Presupune supraanaliza situației în timpul planificării, astfel încât să nu se ia nicio decizie sau acțiune, paralizând în esență procesul de dezvoltare. Acest lucru se întâmplă adesea atunci când scopul este atingerea perfecțiunii și luarea în considerare a absolut totul în perioada de analiză. Acest anti-model se caracterizează prin mersul în cerc (o buclă închisă normală), revizuirea și crearea de modele detaliate, care la rândul lor interferează cu fluxul de lucru. De exemplu, încercați să preziceți lucrurile la un nivel: dar ce se întâmplă dacă un utilizator dorește dintr-o dată să creeze o listă de angajați pe baza a patra și a cincea literă a numelui lor, inclusiv lista proiectelor pentru care și-a petrecut cele mai multe ore de lucru între Anul Nou și Ziua Internațională a Femeii în ultimii patru ani? În esență, este e prea multă analiză. Iată câteva sfaturi pentru combaterea paraliziei de analiză:
  1. Trebuie să definești un obiectiv pe termen lung ca un far pentru luarea deciziilor, astfel încât fiecare dintre deciziile tale să te apropie de obiectiv, mai degrabă decât să te determine să stagnezi.
  2. Nu vă concentrați pe fleacuri (de ce să luați o decizie cu privire la un detaliu nesemnificativ ca și cum ar fi cea mai importantă decizie din viața voastră?)
  3. Stabiliți un termen limită pentru o decizie.
  4. Nu încercați să finalizați o sarcină perfect - este mai bine să o faceți foarte bine.
Nu este nevoie să mergem prea adânc aici, așa că nu vom lua în considerare alte anti-tipare manageriale. Prin urmare, fără nicio introducere, vom trece la câteva anti-modele arhitecturale, deoarece acest articol este cel mai probabil să fie citit de viitorii dezvoltatori, mai degrabă decât de manageri.

2. Dumnezeu obiect

Un obiect Dumnezeu este un anti-model care descrie o concentrare excesivă de tot felul de funcții și cantități mari de date disparate (un obiect în jurul căruia se învârte aplicația). Luați un mic exemplu:
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();
   }
}
Aici vedem o clasă imensă care face totul. Conține interogări de bază de date, precum și unele date. Vedem și metoda de fațadă findAllWithoutPageEn, care include logica de afaceri. Un astfel de obiect al lui Dumnezeu devine enorm și greu de întreținut în mod corespunzător. Trebuie să ne încurcăm cu el în fiecare bucată de cod. Multe componente ale sistemului se bazează pe acesta și sunt strâns legate cu el. Devine din ce în ce mai greu să menții un astfel de cod. În astfel de cazuri, codul ar trebui să fie împărțit în clase separate, fiecare dintre acestea având un singur scop. În acest exemplu, putem împărți obiectul Dumnezeu într-o clasă 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());
   }

                               ........
}
O clasă care conține date și metode de accesare a datelor:
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;
   }
                    ....
Și ar fi mai potrivit să mutați metoda cu logica de afaceri într-un serviciu:
private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3. Singleton

Un singleton este cel mai simplu model. Acesta asigură că într-o aplicație cu un singur thread va exista o singură instanță a unei clase și oferă un punct de acces global la acest obiect. Dar este un model sau un anti-model? Să ne uităm la dezavantajele acestui model:
  1. Stare globală Când accesăm instanța clasei, nu știm starea curentă a acestei clase. Nu știm cine a schimbat-o sau când. Statul poate să nu seamănă cu ceea ce ne așteptăm. Cu alte cuvinte, corectitudinea lucrului cu un singleton depinde de ordinea acceselor la acesta. Aceasta înseamnă că subsistemele sunt dependente unul de celălalt și, ca urmare, un design devine serios mai complex.

  2. Un singleton încalcă principiile SOLID — principiul responsabilității unice: pe lângă sarcinile sale directe, clasa singleton controlează și numărul de instanțe.

  3. Dependența unei clase obișnuite de un singleton nu este vizibilă în interfața clasei. Deoarece o instanță singleton nu este de obicei transmisă ca argument de metodă, ci este obținută direct prin getInstance(), trebuie să intrați în implementarea fiecărei metode pentru a identifica dependența clasei de singleton - doar uitându-vă la publicul unei clase. contractul nu este suficient.

    Prezența unui singleton reduce capacitatea de testare a aplicației în ansamblu și a claselor care folosesc singletonul în special. În primul rând, nu puteți înlocui singleton-ul cu un obiect simulat. În al doilea rând, dacă un singleton are o interfață pentru schimbarea stării sale, atunci testele vor depinde unul de celălalt.

    Cu alte cuvinte, un singleton crește cuplarea și tot ceea ce s-a menționat mai sus nu este altceva decât o consecință a cuplării crescute.

    Și dacă te gândești bine, poți evita să folosești un singleton. De exemplu, este foarte posibil (și într-adevăr necesar) să folosiți diferite tipuri de fabrici pentru a controla numărul de instanțe ale unui obiect.

    Cel mai mare pericol constă în încercarea de a construi o întreagă arhitectură de aplicație bazată pe singletons. Există o mulțime de alternative minunate la această abordare. Cel mai important exemplu este Spring, și anume containerele sale IoC: sunt o soluție firească la problema controlului creării de servicii, întrucât sunt de fapt „fabrici de steroizi”.

    Multe dezbateri interminabile și ireconciliabile fac acum furie pe acest subiect. Depinde de tine să decizi dacă un singleton este un model sau anti-model.

    Nu vom zăbovi asupra ei. În schimb, vom trece la ultimul model de design pentru astăzi - poltergeist.

4. Poltergeist

Un poltergeist este un anti-model care implică o clasă inutilă care este folosită pentru a apela metode din altă clasă sau pur și simplu adaugă un strat inutil de abstractizare. Acest anti-model se manifestă ca obiecte de scurtă durată, lipsite de stat. Aceste obiecte sunt adesea folosite pentru a inițializa alte obiecte mai permanente.
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);
   }
}
De ce avem nevoie de un obiect care să fie doar un intermediar și să-și delege munca altcuiva? Îl eliminăm și transferăm puțina funcționalitate pe care o avea la obiecte cu viață lungă. În continuare, trecem la modelele care ne interesează cel mai mult (ca dezvoltatori obișnuiți), adică anti-patternele de dezvoltare .

5. Codare hard

Așa că am ajuns la acest cuvânt groaznic: hard coding. Esența acestui anti-model este că codul este puternic legat de o anumită configurație hardware și/sau de mediu de sistem. Acest lucru complică foarte mult portarea codului în alte configurații. Acest anti-model este strâns asociat cu numerele magice (aceste anti-modeluri sunt adesea împletite). Exemplu:
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;
}
Doare, nu-i așa? Aici ne codificăm setările de conexiune. Ca rezultat, codul va funcționa corect numai cu MySQL. Pentru a schimba baza de date, va trebui să ne scufundăm în cod și să schimbăm totul manual. O soluție bună ar fi să puneți configurația într-un fișier separat:
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
O altă opțiune este utilizarea constantelor.

6. Ancoră pentru bărci

În contextul anti-patternelor, o ancoră de barcă înseamnă păstrarea unor părți ale sistemului care nu mai sunt utilizate după efectuarea unor optimizari sau refactorizări. De asemenea, unele părți ale codului ar putea fi păstrate „pentru utilizare ulterioară” doar în cazul în care aveți nevoie brusc de ele. În esență, acest lucru vă transformă codul într-un coș de gunoi. Exemplu:
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());
}
Avem o metodă de actualizare care utilizează o metodă separată pentru a îmbina datele utilizatorului din baza de date cu datele utilizatorului transmise metodei (dacă utilizatorul trecut la metoda de actualizare are un câmp nul, atunci valoarea câmpului vechi este preluată din baza de date) . Apoi să presupunem că există o nouă cerință conform căreia înregistrările nu trebuie îmbinate cu cele vechi, ci în schimb, chiar dacă există câmpuri nule, acestea sunt folosite pentru a le suprascrie pe cele vechi:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Aceasta înseamnă că mergeUser nu mai este folosit, dar ar fi păcat să-l ștergeți - ce se întâmplă dacă această metodă (sau ideea acestei metode) ar putea fi utilă într-o zi? Un astfel de cod doar complică sistemele și introduce confuzie, neavând practic valoare practică. Nu trebuie să uităm că un astfel de cod cu „piese moarte” va fi greu de transmis unui coleg atunci când pleci la alt proiect. Cel mai bun mod de a face față ancorelor de bărci este să refactorezi codul, adică să ștergi secțiuni de cod (sfâșietor, știu). În plus, la pregătirea programului de dezvoltare, este necesar să se țină seama de astfel de ancore (pentru a aloca timp pentru aranjare).

7. Păstrarea obiectelor

Pentru a descrie acest anti-model, mai întâi trebuie să vă familiarizați cu modelul pool-ului de obiecte . Un pool de obiecte (pool de resurse) este un model de design creațional , un set de obiecte inițializate și gata de utilizare. Când o aplicație are nevoie de un obiect, acesta este preluat din acest pool, în loc să fie recreat. Când un obiect nu mai este necesar, acesta nu este distrus. În schimb, este returnat la piscină. Acest model este de obicei folosit pentru obiecte grele care necesită mult timp pentru a fi create de fiecare dată când sunt necesare, cum ar fi atunci când se conectează la o bază de date. Să ne uităm la un exemplu mic și simplu. Iată o clasă care reprezintă acest model:
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);
   }
}
Această clasă este prezentată sub forma modelului/anti-modelului singleton de mai sus , adică poate exista un singur obiect de acest tip. Folosește anumite Resourceobiecte. În mod implicit, constructorul umple pool-ul cu 4 instanțe. Când obțineți un obiect, acesta este eliminat din pool (dacă nu există un obiect disponibil, unul este creat și imediat returnat). Și la sfârșit, avem o metodă de a pune obiectul înapoi. Obiectele de resurse arată astfel:
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;
   }
}
Aici avem un obiect mic care conține o hartă cu nume de modele de design ca cheie și link-uri Wikipedia corespunzătoare ca valoare, precum și metode de accesare a hărții. Să aruncăm o privire 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);
   }
}
Totul aici este suficient de clar: obținem un obiect pool, obținem un obiect cu resurse din pool, obținem harta de la obiect Resource, facem ceva cu el și punem toate acestea la locul său în pool pentru reutilizare ulterioară. Voila, acesta este modelul de design al piscinei de obiecte. Dar vorbeam despre anti-tipare, nu? Să luăm în considerare următorul caz în metoda 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);
Aici, din nou, obținem un obiect Resource, obținem harta lui de modele și facem ceva cu harta. Dar înainte de a salva harta înapoi în grupul de obiecte, aceasta este șters și apoi populată cu date corupte, făcând obiectul Resurse inadecvat pentru reutilizare. Unul dintre principalele detalii ale unui pool de obiecte este că atunci când un obiect este returnat, acesta trebuie să revină la o stare adecvată pentru reutilizare ulterioară. Dacă obiectele returnate în bazin rămân într-o stare incorectă sau nedefinită, atunci designul nostru se numește un canal de obiecte. Are vreun sens să depozitezi obiecte care nu sunt potrivite pentru reutilizare? În această situație, putem face harta internă imuabilă în 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);
}
Încercările și dorința de a schimba conținutul hărții vor dispărea datorită UnsupportedOperationException pe care o vor genera. Anti-modelele sunt capcane pe care dezvoltatorii le întâlnesc frecvent din cauza lipsei acute de timp, a neglijenței, a lipsei de experiență sau a presiunii din partea managerilor de proiect. Grabita, care este obișnuită, poate duce la probleme mari pentru aplicație în viitor, așa că trebuie să știți despre aceste erori și să le evitați în avans. Aceasta încheie prima parte a articolului. Va urma...
Comentarii
  • Popular
  • Nou
  • Vechi
Trebuie să fii conectat pentru a lăsa un comentariu
Această pagină nu are încă niciun comentariu