CodeGym/Java blog/Véletlen/Mik azok az anti-minták? Nézzünk néhány példát (1. rész)
John Squirrels
Szint
San Francisco

Mik azok az anti-minták? Nézzünk néhány példát (1. rész)

Megjelent a csoportban
Jó napot mindenkinek! A minap volt egy állásinterjúm, és néhány kérdést tettek fel az anti-mintákkal kapcsolatban: mik ezek, milyen típusok vannak, és milyen gyakorlati példák vannak. Természetesen válaszoltam a kérdésre, de nagyon felületesen, hiszen korábban nem merültem bele mélyen ebbe a témába. Az interjú után elkezdtem böngészni az internetet, és egyre jobban belemerültem a témába. Mik azok az anti-minták?  Nézzünk néhány példát (1. rész) – 1 Ma szeretnék rövid áttekintést nyújtani a legnépszerűbb anti-mintákról, és áttekinteni néhány példát. Remélem, hogy ennek elolvasása megadja a szükséges ismereteket ezen a területen. Kezdjük el! Mielőtt megvitatnánk, mi az anti-minta, emlékezzünk vissza, mi is az a tervezési minta. Tervezési mintaegy megismételhető építészeti megoldás az alkalmazások tervezése során felmerülő gyakori problémákra vagy helyzetekre. De ma nem róluk beszélünk, hanem inkább az ellentétükről – anti-mintázatokról. Az anti-minta egy széles körben elterjedt, de hatástalan, kockázatos és/vagy terméketlen megközelítés a gyakori problémák egy osztályának megoldására. Más szóval, ez a hibák mintája (néha csapdának is nevezik). Az anti-mintákat általában a következő típusokra osztják:
  1. Építészeti anti-mintázatok – Ezek az anti-mintázatok akkor keletkeznek, amikor egy rendszer szerkezetét megtervezik (általában egy építész).
  2. Vezetési/szervezeti anti-minták – Ezek a projektmenedzsment anti-mintái, amelyekkel általában különböző menedzserek (vagy menedzsercsoportok) találkoznak.
  3. Fejlesztési anti-minták – Ezek az anti-minták akkor keletkeznek, amikor egy rendszert a hétköznapi programozók implementálnak.
Az anti-minták teljes skálája sokkal egzotikusabb, de ma nem vesszük figyelembe mindegyiket. A hétköznapi fejlesztőknek ez túl sok lenne. Kezdetnek tekintsünk egy vezetési antimintát példaként.

1. Analitikai bénulás

Elemzési bénulásklasszikus irányítási antimintának számít. Ez magában foglalja a helyzet túlzott elemzését a tervezés során, hogy ne kerüljön sor döntésre vagy cselekvésre, ami lényegében megbénítja a fejlesztési folyamatot. Ez gyakran megtörténik, amikor a tökéletesség elérése a cél, és abszolút mindent figyelembe kell venni az elemzési időszakban. Ezt az anti-mintázatot a körökben járás (egy bejáratott zárt hurok), a részletes modellek átdolgozása és létrehozása jellemzi, ami viszont zavarja a munkafolyamatot. Például egy szinten próbál megjósolni dolgokat: de mi van akkor, ha a felhasználó hirtelen szeretne létrehozni egy listát az alkalmazottakról nevének negyedik és ötödik betűje alapján, beleértve azon projektek listáját, amelyeken a legtöbb munkaórát töltötte. Újév és a Nemzetközi Nőnap között az elmúlt négy évben? Lényegében ez' túl sok elemzés. Íme néhány tipp az elemzési bénulás leküzdésére:
  1. Meg kell határoznia egy hosszú távú célt, amely a döntéshozatal irányadója, hogy minden döntése közelebb mozdítson a célhoz, nehogy stagnálást okozzon.
  2. Ne koncentrálj apróságokra (miért dönts egy jelentéktelen részletről úgy, mintha az életed legfontosabb döntése lenne?)
  3. Állítson be határidőt a döntés meghozatalára.
  4. Ne próbáljon meg tökéletesen elvégezni egy feladatot – jobb, ha nagyon jól csinálja.
Itt nem kell túl mélyre menni, ezért nem veszünk figyelembe más menedzseri antimintákat. Ezért minden bevezetés nélkül áttérünk néhány építészeti anti-mintára, mert ezt a cikket nagy valószínűséggel a jövőbeli fejlesztők olvassák, nem pedig a menedzserek.

2. Isten kifogása

Az Isten objektum egy anti-minta, amely mindenféle funkció túlzott koncentrációját és nagy mennyiségű, egymástól eltérő adatot írja le (olyan objektum, amely körül az alkalmazás forog). Vegyünk egy kis példát:
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();
   }
}
Itt egy hatalmas osztályt látunk, amely mindent megtesz. Adatbázis-lekérdezéseket, valamint néhány adatot tartalmaz. Látjuk a findAllWithoutPageEn homlokzati metódust is, amely magában foglalja az üzleti logikát is. Egy ilyen Isten-tárgy hatalmassá válik, és kényelmetlenné válik megfelelően karbantartani. Minden kódrészletben vacakolnunk kell vele. Számos rendszerelem támaszkodik rá, és szorosan kapcsolódik hozzá. Egyre nehezebbé válik az ilyen kód karbantartása. Ilyen esetekben a kódot külön osztályokra kell felosztani, amelyek mindegyikének csak egy célja lesz. Ebben a példában feloszthatjuk az Isten objektumot egy Dao osztályra:
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());
   }

                               ........
}
Egy osztály, amely adatokat és módszereket tartalmaz az adatok elérésére:
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;
   }
                    ....
És célszerűbb lenne az üzleti logikával rendelkező metódust áthelyezni egy szolgáltatásba:
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

A singleton a legegyszerűbb minta. Biztosítja, hogy egy egyszálú alkalmazásban egy osztály egyetlen példánya legyen, és globális hozzáférési pontot biztosít ehhez az objektumhoz. De ez minta vagy antiminta? Nézzük meg ennek a mintának a hátrányait:
  1. Globális állapot Amikor hozzáférünk az osztály példányához, nem ismerjük ennek az osztálynak az aktuális állapotát. Nem tudjuk, ki és mikor változtatta meg. Lehet, hogy az állam nem olyan, mint amire számítunk. Más szóval, a szinglivel való munkavégzés helyessége a hozzáférési sorrendtől függ. Ez azt jelenti, hogy az alrendszerek egymásra vannak utalva, és ennek eredményeként a tervezés komolyan bonyolultabbá válik.

  2. A singleton sérti a SOLID elveket – az egyetlen felelősség elvét: a Singleton osztály a közvetlen feladatai mellett a példányok számát is szabályozza.

  3. Egy közönséges osztály szinglitől való függése nem látható az osztály felületén. Mivel a singleton példányt általában nem adják át metódus argumentumként, hanem közvetlenül a getInstance()-en keresztül kapják meg, minden metódus megvalósításába kell belemenni, hogy azonosítsa az osztály függőségét a szinglitől – csak egy osztály nyilvános oldalát tekintve. a szerződés nem elég.

    Az egyszó jelenléte csökkenti az alkalmazás egészének tesztelhetőségét, és különösen a szinglitonust használó osztályok tesztelhetőségét. Először is, nem helyettesítheti a szingulettet egy álobjektummal. Másodszor, ha egy singletonnak van interfésze az állapot megváltoztatására, akkor a tesztek egymástól függenek.

    Más szóval, egy szingli növeli a csatolást, és minden, amit fentebb említettünk, nem más, mint a megnövekedett csatolás következménye.

    És ha jobban belegondol, elkerülheti a szingli használatát. Például teljesen lehetséges (és valóban szükséges) különféle gyárak használata egy objektum példányszámának szabályozására.

    A legnagyobb veszély abban rejlik, ha egy teljes alkalmazás-architektúrát akarnak felépíteni egy szingliton alapuló architektúrára. Rengeteg csodálatos alternatíva létezik ennek a megközelítésnek. A legfontosabb példa a Spring, mégpedig annak IoC konténerei: természetes megoldást jelentenek a szolgáltatások létrehozásának ellenőrzésére, hiszen valójában "szteroidgyárak".

    Sok véget nem érő és kibékíthetetlen vita folyik most erről a témáról. Rajtad múlik, hogy eldöntsd, hogy a szingli minta vagy mintaellenes.

    Nem fogunk elidőzni rajta. Ehelyett áttérünk a mai utolsó tervezési mintára – a poltergeistre.

4. Poltergeist

A poltergeist egy értelmetlen osztályt magában foglaló anti-minta, amelyet egy másik osztály metódusainak hívására használnak, vagy egyszerűen csak szükségtelen absztrakciós réteget adnak hozzá. Ez az antimintázat rövid életű, állapot nélküli tárgyakként nyilvánul meg. Ezeket az objektumokat gyakran használják más, állandóbb objektumok inicializálására.
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);
   }
}
Miért van szükségünk olyan objektumra, amely csak közvetítő, és a munkáját másra ruházza? Kiküszöböljük, és azt a csekély funkcionalitást, amivel rendelkezett, átvisszük a hosszú élettartamú tárgyakra. Ezután áttérünk azokra a mintákra, amelyek minket (mint hétköznapi fejlesztőket) a leginkább érdekelnek, azaz a fejlesztési anti-mintákkal .

5. Kemény kódolás

Elérkeztünk tehát ehhez a szörnyű szóhoz: kemény kódolás. Ennek az anti-mintázatnak az a lényege, hogy a kód erősen kötődik egy adott hardverkonfigurációhoz és/vagy rendszerkörnyezethez. Ez nagymértékben megnehezíti a kód más konfigurációkra való áthelyezését. Ez az anti-minta szorosan kapcsolódik a mágikus számokhoz (ezek az anti-mintázatok gyakran összefonódnak). Példa:
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;
}
Fáj, nem? Itt keményen kódoljuk kapcsolati beállításainkat. Ennek eredményeként a kód csak a MySQL-lel fog megfelelően működni. Az adatbázis megváltoztatásához bele kell merülnünk a kódba, és mindent kézzel kell módosítanunk. Jó megoldás az lenne, ha a konfigurációt egy külön fájlba helyezné:
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Egy másik lehetőség az állandók használata.

6. Csónakhorgony

Az anti-mintázattal összefüggésben a hajórögzítés azt jelenti, hogy a rendszer bizonyos optimalizálása vagy átalakítása után már nem használt részeit megtartják. Ezenkívül a kód egyes részei megőrizhetők „későbbi használatra”, arra az esetre, ha hirtelen szüksége lenne rájuk. Lényegében ez a kódját szeméttárolóvá változtatja. Példa:
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());
}
Van egy frissítési metódusunk, amely egy külön módszerrel egyesíti az adatbázisból származó felhasználói adatokat a metódusnak átadott felhasználói adatokkal (ha a frissítési metódushoz átadott felhasználó null mezővel rendelkezik, akkor a régi mező értéke az adatbázisból származik) . Akkor tegyük fel, hogy van egy új követelmény, hogy a rekordokat nem szabad összevonni a régiekkel, hanem akkor is, ha null mezők vannak, akkor is felülírják a régieket:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Ez azt jelenti, hogy a mergeUser-t már nem használják, de kár lenne törölni – mi van, ha ez a módszer (vagy a módszer ötlete) egyszer jól jön? Az ilyen kód csak bonyolítja a rendszereket és zavart okoz, gyakorlatilag nincs gyakorlati értéke. Nem szabad megfeledkeznünk arról, hogy az ilyen „halott darabokat” tartalmazó kódot nehéz lesz átadni egy kollégának, amikor egy másik projektbe indul. A hajóhorgonyok kezelésének legjobb módja a kód átalakítása, azaz kódrészletek törlése (tudom, szívszorító). Ezenkívül a fejlesztési ütemterv elkészítésekor számolni kell az ilyen horgonyokkal (a rendbetételre való idő beosztása).

7. Tárgyi pöcegödör

Ennek az anti-mintázatnak a leírásához először meg kell ismerkednie az objektumkészlet mintával. Az objektumkészlet (erőforráskészlet) egy kreatív tervezési minta , inicializált és használatra kész objektumok halmaza. Ha egy alkalmazásnak szüksége van egy objektumra, akkor azt ebből a készletből veszik el, nem pedig újra létrehozzák. Amikor egy tárgyra már nincs szükség, nem semmisül meg. Ehelyett visszakerül a medencébe. Ezt a mintát általában nehéz objektumokhoz használják, amelyek létrehozása időigényes minden alkalommal, amikor szükség van rájuk, például adatbázishoz való csatlakozáskor. Nézzünk egy kicsi és egyszerű példát. Itt van egy osztály, amely ezt a mintát képviseli:
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);
   }
}
Ez az osztály a fenti singleton minta/anti-minta formájában jelenik meg , azaz csak egy ilyen típusú objektum lehet. Bizonyos objektumokat használ Resource. Alapértelmezés szerint a konstruktor 4 példányban tölti ki a készletet. Amikor megkap egy objektumot, az eltávolításra kerül a készletből (ha nincs elérhető objektum, létrejön egy, és azonnal visszaadja). És a végén van egy módszerünk az objektum visszahelyezésére. Az erőforrás objektumok így néznek ki:
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;
   }
}
Itt van egy kis objektumunk, amely egy térképet tartalmaz kulcsként tervezési mintanevekkel, értékként pedig a megfelelő Wikipédia-hivatkozásokkal, valamint a térkép elérésének módszereit. Nézzük a főt:
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);
   }
}
Itt minden elég világos: kapunk egy pool objektumot, kapunk egy objektumot erőforrásokkal a készletből, megkapjuk a térképet az erőforrásobjektumból, csinálunk vele valamit, és mindezt a helyére tesszük a medencében további újrafelhasználás céljából. Voila, ez az objektum medence tervezési mintája. De mi az anti-mintázatokról beszéltünk, igaz? Tekintsük a következő esetet a fő módszerben:
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);
Itt ismét kapunk egy Resource objektumot, megkapjuk a mintatérképét, és csinálunk valamit a térképpel. Mielőtt azonban visszamentené a térképet az objektumok készletébe, a rendszer törli, majd feltölti a sérült adatokkal, így az erőforrás-objektum alkalmatlanná válik az újrafelhasználásra. Az objektumkészlet egyik fő részlete, hogy egy objektum visszaküldésekor vissza kell állítania a további újrafelhasználásra alkalmas állapotot. Ha a készletbe visszaküldött objektumok hibás vagy meghatározatlan állapotban maradnak, akkor a tervezésünket objektumtárolónak nevezzük. Van értelme az újrafelhasználásra alkalmatlan tárgyak tárolásának? Ebben a helyzetben a belső térképet megváltoztathatatlanná tehetjük a konstruktorban:
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);
}
Az általuk generált UnsupportedOperationException kivételnek köszönhetően a kísérletek és a vágy a térkép tartalmának megváltoztatására elhalványul. Az anti-minták olyan csapdák, amelyekkel a fejlesztők gyakran találkoznak akut időhiány, gondatlanság, tapasztalatlanság vagy a projektmenedzserek nyomása miatt. A gyakori rohanás nagy problémákat okozhat az alkalmazásnak a jövőben, ezért tudnia kell ezekről a hibákról, és előre el kell kerülnie őket. Ezzel zárul a cikk első része. Folytatjuk...
Hozzászólások
  • Népszerű
  • Új
  • Régi
Hozzászólás írásához be kell jelentkeznie
Ennek az oldalnak még nincsenek megjegyzései