CodeGym /Java blogg /SlumpmÀssig /Vad Àr antimönster? LÄt oss titta pÄ nÄgra exempel (del 1...
John Squirrels
NivÄ
San Francisco

Vad Àr antimönster? LÄt oss titta pÄ nÄgra exempel (del 1)

Publicerad i gruppen
God dag till alla! HĂ€romdagen hade jag en anstĂ€llningsintervju, och jag fick nĂ„gra frĂ„gor om antimönster: vad de Ă€r, vilka typer det finns och vilka praktiska exempel det finns. Naturligtvis svarade jag pĂ„ frĂ„gan, men vĂ€ldigt ytligt, eftersom jag inte tidigare dykt djupt in i detta Ă€mne. Efter intervjun började jag leta runt pĂ„ nĂ€tet och fördjupade mig mer och mer i Ă€mnet. Vad Ă€r antimönster?  LĂ„t oss titta pĂ„ nĂ„gra exempel (del 1) - 1 Idag skulle jag vilja ge en kort översikt över de mest populĂ€ra anti-mönstren och granska nĂ„gra exempel. Jag hoppas att lĂ€sningen av detta kommer att ge dig den kunskap du behöver inom detta omrĂ„de. LĂ„t oss börja! Innan vi diskuterar vad ett antimönster Ă€r, lĂ„t oss komma ihĂ„g vad ett designmönster Ă€r. Ett designmönsterĂ€r en repeterbar arkitektonisk lösning för vanliga problem eller situationer som uppstĂ„r vid design av en applikation. Men idag talar vi inte om dem, utan snarare deras motsatser – antimönster. Ett antimönster Ă€r ett utbrett men ineffektivt, riskabelt och/eller improduktivt tillvĂ€gagĂ„ngssĂ€tt för att lösa en klass av vanliga problem. Detta Ă€r med andra ord ett mönster av misstag (Ă€ven ibland kallad en fĂ€lla). Som regel Ă€r antimönster indelade i följande typer:
  1. Arkitektoniska antimönster — Dessa antimönster uppstĂ„r nĂ€r strukturen i ett system utformas (vanligtvis av en arkitekt).
  2. Lednings-/organisationsantimönster — Dessa Ă€r antimönster i projektledning, som vanligtvis möter olika chefer (eller grupper av chefer).
  3. Utvecklingsantimönster — Dessa antimönster uppstĂ„r nĂ€r ett system implementeras av vanliga programmerare.
Hela utbudet av antimönster Àr mycket mer exotiskt, men vi kommer inte att övervÀga dem alla idag. För vanliga utvecklare skulle det vara för mycket. Till att börja med, lÄt oss betrakta ett ledningsantimönster som ett exempel.

1. Analytisk förlamning

Analys förlamninganses vara ett klassiskt management-antimönster. Det innebÀr att överanalysera situationen under planeringen, sÄ att inga beslut eller ÄtgÀrder vidtas, vilket i huvudsak förlamar utvecklingsprocessen. Detta hÀnder ofta nÀr mÄlet Àr att uppnÄ perfektion och övervÀga absolut allt under analysperioden. Detta antimönster kÀnnetecknas av att gÄ i cirklar (en helt ny sluten slinga), revidera och skapa detaljerade modeller, vilket i sin tur stör arbetsflödet. Till exempel, du försöker förutsÀga saker pÄ en nivÄ: men tÀnk om en anvÀndare plötsligt vill skapa en lista över anstÀllda baserat pÄ den fjÀrde och femte bokstÀverna i deras namn, inklusive listan över projekt som de spenderat mest arbetstimmar pÄ mellan nyÄr och internationella kvinnodagen under de senaste fyra Ären? I huvudsak Àr det' Àr för mycket analys. HÀr Àr nÄgra tips för att bekÀmpa analysförlamning:
  1. Du mÄste definiera ett lÄngsiktigt mÄl som en ledstjÀrna för beslutsfattande, sÄ att vart och ett av dina beslut flyttar dig nÀrmare mÄlet snarare Àn att fÄ dig att stagnera.
  2. Koncentrera dig inte pÄ bagateller (varför fatta ett beslut om en obetydlig detalj som om det vore det viktigaste beslutet i ditt liv?)
  3. SÀtt en tidsfrist för ett beslut.
  4. Försök inte att slutföra en uppgift perfekt – det Ă€r bĂ€ttre att göra det mycket bra.
Inget behov av att gÄ för djupt hÀr, sÄ vi kommer inte att övervÀga andra ledningsmÀssiga antimönster. DÀrför, utan nÄgon introduktion, gÄr vi vidare till nÄgra arkitektoniska antimönster, eftersom den hÀr artikeln med största sannolikhet kommer att lÀsas av framtida utvecklare snarare Àn chefer.

2. Gud objekt

Ett Gud-objekt Àr ett antimönster som beskriver en överdriven koncentration av alla möjliga funktioner och stora mÀngder disparata data (ett objekt som applikationen kretsar kring). Ta ett litet exempel:

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();
   }
}
HÀr ser vi en enorm klass som gör allt. Den innehÄller databasfrÄgor sÄvÀl som vissa data. Vi ser Àven fasadmetoden findAllWithoutPageEn, som inkluderar affÀrslogik. Ett sÄdant gudsobjekt blir enormt och besvÀrligt att underhÄlla pÄ rÀtt sÀtt. Vi mÄste brÄka med det i varje kod. MÄnga systemkomponenter förlitar sig pÄ det och Àr tÀtt kopplade till det. Det blir svÄrare och svÄrare att upprÀtthÄlla sÄdan kod. I sÄdana fall bör koden delas upp i separata klasser, som var och en endast har ett syfte. I det hÀr exemplet kan vi dela upp Gud-objektet i en Dao-klass:

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());
   }
  
                               ........
}
En klass som innehÄller data och metoder för att komma Ät data:

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;
   }
                    ....
Och det vore mer lÀmpligt att flytta metoden med affÀrslogik till en tjÀnst:

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

En singel Àr det enklaste mönstret. Det sÀkerstÀller att det i en enkeltrÄdad applikation kommer att finnas en enda instans av en klass, och den tillhandahÄller en global Ätkomstpunkt till detta objekt. Men Àr det ett mönster eller ett antimönster? LÄt oss titta pÄ nackdelarna med detta mönster:
  1. Global status NÀr vi kommer Ät instansen av klassen vet vi inte det aktuella tillstÄndet för denna klass. Vi vet inte vem som har Àndrat det eller nÀr. Staten kanske inte Àr nÄgot som vi förvÀntar oss. Med andra ord beror riktigheten av att arbeta med en singel pÄ ordningen pÄ Ätkomsterna till den. Detta innebÀr att delsystem Àr beroende av varandra och som ett resultat blir en design allvarligt mer komplex.

  2. En singleton bryter mot SOLID-principerna – principen om ett enda ansvar: förutom sina direkta uppgifter styr singelklassen ocksĂ„ antalet instanser.

  3. En vanlig klasss beroende av en singleton syns inte i klassens grÀnssnitt. Eftersom en singleton-instans vanligtvis inte skickas som ett metodargument, utan istÀllet erhÄlls direkt genom getInstance(), mÄste du komma in i implementeringen av varje metod för att identifiera klassens beroende av singletonen - bara titta pÄ en klasss publika kontrakt rÀcker inte.

    NÀrvaron av en singel minskar testbarheten för applikationen som helhet och klasserna som anvÀnder singeln i synnerhet. Först och frÀmst kan du inte ersÀtta singeln med ett skenobjekt. För det andra, om en singel har ett grÀnssnitt för att Àndra dess tillstÄnd, kommer testerna att bero pÄ varandra.

    Med andra ord, en singel ökar kopplingen, och allt som nÀmns ovan Àr inget annat Àn en konsekvens av ökad koppling.

    Och om du tÀnker efter kan du undvika att anvÀnda en singel. Till exempel Àr det fullt möjligt (och faktiskt nödvÀndigt) att anvÀnda olika typer av fabriker för att kontrollera antalet instanser av ett objekt.

    Den största faran ligger i ett försök att bygga en hel applikationsarkitektur baserad pÄ singeltoner. Det finns massor av underbara alternativ till detta tillvÀgagÄngssÀtt. Det viktigaste exemplet Àr Spring, nÀmligen dess IoC-behÄllare: de Àr en naturlig lösning pÄ problemet med att kontrollera skapandet av tjÀnster, eftersom de faktiskt Àr "fabriker pÄ steroider".

    MÄnga oÀndliga och oförsonliga debatter rasar nu om detta Àmne. Det Àr upp till dig att bestÀmma om en singel Àr ett mönster eller ett antimönster.

    Vi kommer inte att dröja vid det. IstÀllet gÄr vi vidare till det sista designmönstret för idag - poltergeist.

4. Poltergeist

En poltergeist Àr ett antimönster som involverar en meningslös klass som anvÀnds för att anropa metoder frÄn en annan klass eller helt enkelt lÀgger till ett onödigt lager av abstraktion. Detta antimönster manifesterar sig som kortlivade objekt, utan tillstÄnd. Dessa objekt anvÀnds ofta för att initiera andra, mer permanenta objekt.

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);
   }
}
Varför behöver vi ett objekt som bara Àr en mellanhand och delegerar sitt arbete till nÄgon annan? Vi tar bort den och överför den lilla funktionaliteten den hade till lÄnglivade föremÄl. DÀrefter gÄr vi vidare till de mönster som Àr mest intressanta för oss (som vanliga utvecklare), dvs utvecklingsantimönster .

5. HĂ„rd kodning

SÄ vi har kommit fram till det hÀr fruktansvÀrda ordet: hÄrd kodning. KÀrnan i detta antimönster Àr att koden Àr starkt knuten till en specifik hÄrdvarukonfiguration och/eller systemmiljö. Detta komplicerar avsevÀrt porteringen av koden till andra konfigurationer. Detta antimönster Àr nÀra förknippat med magiska siffror (dessa antimönster Àr ofta sammanflÀtade). Exempel:

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;
}
Gör det ont, eller hur? HÀr hÄrdkodar vi vÄra anslutningsinstÀllningar. Som ett resultat kommer koden endast att fungera korrekt med MySQL. För att Àndra databasen mÄste vi dyka in i koden och Àndra allt manuellt. En bra lösning skulle vara att lÀgga konfigurationen i en separat fil:

spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Ett annat alternativ Àr att anvÀnda konstanter.

6. BĂ„tankare

I samband med antimönster innebÀr ett bÄtankare att behÄlla delar av systemet som inte lÀngre anvÀnds efter att ha utfört viss optimering eller omfaktorering. Vissa delar av koden kan ocksÄ behÄllas "för framtida anvÀndning" ifall du plötsligt skulle behöva dem. I huvudsak förvandlar detta din kod till en soptunna. Exempel:

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());
}
Vi har en uppdateringsmetod som anvÀnder en separat metod för att slÄ samman anvÀndardata frÄn databasen med anvÀndardata som skickas till metoden (om anvÀndaren som skickats till uppdateringsmetoden har ett nollfÀlt, sÄ tas det gamla fÀltvÀrdet frÄn databasen) . Anta sedan att det finns ett nytt krav att posterna inte fÄr slÄs samman med de gamla, utan istÀllet, Àven om det finns nollfÀlt, anvÀnds de för att skriva över de gamla:

public User update(Long id, User request) {
   return userDAO.update(user);
}
Det betyder att mergeUser inte lĂ€ngre anvĂ€nds, men det skulle vara synd att ta bort den — tĂ€nk om den hĂ€r metoden (eller idĂ©n med denna metod) kan komma till anvĂ€ndning nĂ„gon gĂ„ng? SĂ„dan kod komplicerar bara system och skapar förvirring, och har i princip inget praktiskt vĂ€rde. Vi fĂ„r inte glömma att sĂ„dan kod med "döda pjĂ€ser" kommer att vara svĂ„ra att föra vidare till en kollega nĂ€r man ska ivĂ€g till ett annat projekt. Det bĂ€sta sĂ€ttet att hantera bĂ„tankare Ă€r att omfaktorisera koden, dvs radera kodavsnitt (hjĂ€rtskĂ€rande, jag vet). Vid utarbetandet av utvecklingsschemat Ă€r det dessutom nödvĂ€ndigt att ta hĂ€nsyn till sĂ„dana ankare (för att avsĂ€tta tid för stĂ€dning).

7. Objekt avloppsbrunn

För att beskriva detta antimönster mÄste du först bekanta dig med objektpoolsmönstret . En objektpool (resurspool) Àr ett kreativt designmönster , en uppsÀttning initierade och fÀrdiga att anvÀnda objekt. NÀr ett program behöver ett objekt tas det frÄn denna pool istÀllet för att Äterskapas. NÀr ett föremÄl inte lÀngre behövs förstörs det inte. IstÀllet Äterförs den till poolen. Detta mönster anvÀnds vanligtvis för tunga objekt som Àr tidskrÀvande att skapa varje gÄng de behövs, till exempel nÀr du ansluter till en databas. LÄt oss titta pÄ ett litet och enkelt exempel. HÀr Àr en klass som representerar detta mönster:

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);
   }
}
Denna klass presenteras i form av ovanstÄende singleton- mönster/anti-mönster, dvs det kan bara finnas ett objekt av denna typ. Den anvÀnder vissa ResourceföremÄl. Som standard fyller konstruktören poolen med 4 instanser. NÀr du fÄr ett objekt tas det bort frÄn poolen (om det inte finns nÄgot tillgÀngligt objekt skapas ett och returneras omedelbart). Och i slutet har vi en metod för att sÀtta tillbaka föremÄlet. Resursobjekt ser ut sÄ hÀr:

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;
   }
}
HÀr har vi ett litet objekt som innehÄller en karta med designmönsternamn som nyckel och motsvarande Wikipedia-lÀnkar som vÀrde, samt metoder för att komma Ät kartan. LÄt oss ta en titt pÄ huvuddelen:

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);
   }
}
Allt hÀr Àr tydligt nog: vi fÄr ett poolobjekt, hÀmtar ett objekt med resurser frÄn poolen, hÀmtar kartan frÄn Resursobjekt, gör nÄgot med det och lÀgger allt detta pÄ sin plats i poolen för vidare ÄteranvÀndning. Voila, detta Àr designmönstret för objektpoolen. Men vi pratade om antimönster, eller hur? LÄt oss övervÀga följande fall i huvudmetoden:

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);
HĂ€r fĂ„r vi Ă„terigen ett resursobjekt, vi fĂ„r dess karta över mönster och vi gör nĂ„got med kartan. Men innan kartan sparas tillbaka till objektpoolen rensas den och fylls sedan i med korrupta data, vilket gör resursobjektet olĂ€mpligt för Ă„teranvĂ€ndning. En av huvuddetaljerna för en objektpool Ă€r att nĂ€r ett objekt returneras mĂ„ste det Ă„terstĂ€llas till ett tillstĂ„nd som Ă€r lĂ€mpligt för vidare Ă„teranvĂ€ndning. Om objekt som returneras till poolen förblir i ett felaktigt eller odefinierat tillstĂ„nd, kallas vĂ„r design en objektavloppsbrunn. Är det nĂ„gon mening med att förvara föremĂ„l som inte Ă€r lĂ€mpliga för Ă„teranvĂ€ndning? I den hĂ€r situationen kan vi göra den interna kartan oförĂ€nderlig i konstruktorn:

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);
}
Försök och önskan att Àndra kartans innehÄll kommer att blekna bort tack vare UnsupportedOperationException som de kommer att generera. Antimönster Àr fÀllor som utvecklare stöter pÄ ofta pÄ grund av akut tidsbrist, slarv, oerfarenhet eller press frÄn projektledare. Att rusa, vilket Àr vanligt, kan leda till stora problem för applikationen i framtiden, sÄ du mÄste kÀnna till dessa fel och undvika dem i förvÀg. Detta avslutar den första delen av artikeln. FortsÀttning följer...
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION