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
  • Populär
  • Ny
  • Gammal
Du måste vara inloggad för att lämna en kommentar
Den här sidan har inga kommentarer än