CodeGym /Java-blogg /Tilfeldig /Hva er anti-mønstre? La oss se på noen eksempler (del 1)
John Squirrels
Nivå
San Francisco

Hva er anti-mønstre? La oss se på noen eksempler (del 1)

Publisert i gruppen
Ha en fin dag alle sammen! Her om dagen hadde jeg et jobbintervju, og jeg ble stilt noen spørsmål om antimønstre: hva de er, hvilke typer det finnes, og hvilke praktiske eksempler det finnes. Jeg svarte selvfølgelig på spørsmålet, men veldig overfladisk, siden jeg ikke tidligere hadde dykket dypt inn i dette emnet. Etter intervjuet begynte jeg å lete rundt på Internett og fordypet meg mer og mer i temaet. Hva er anti-mønstre?  La oss se på noen eksempler (del 1) - 1 I dag vil jeg gi en kort oversikt over de mest populære anti-mønstrene og gjennomgå noen eksempler. Jeg håper at det å lese dette vil gi deg kunnskapen du trenger på dette området. La oss komme i gang! Før vi diskuterer hva et antimønster er, la oss huske hva et designmønster er. Et designmønsterer en repeterbar arkitektonisk løsning for vanlige problemer eller situasjoner som oppstår ved utforming av en applikasjon. Men i dag snakker vi ikke om dem, men snarere deres motsetninger - anti-mønstre. Et anti-mønster er en utbredt, men ineffektiv, risikabel og/eller uproduktiv tilnærming til å løse en klasse med vanlige problemer. Med andre ord er dette et mønster av feil (også noen ganger kalt en felle). Som regel er anti-mønstre delt inn i følgende typer:
  1. Arkitektoniske anti-mønstre - Disse anti-mønstrene oppstår når strukturen til et system er designet (vanligvis av en arkitekt).
  2. Ledelses-/organisatoriske anti-mønstre - Dette er anti-mønstre i prosjektledelse, vanligvis møtt av ulike ledere (eller grupper av ledere).
  3. Utviklings-anti-mønstre — Disse anti-mønstrene oppstår når et system implementeres av vanlige programmerere.
Hele spekteret av anti-mønstre er langt mer eksotisk, men vi vil ikke vurdere dem alle i dag. For vanlige utviklere blir det for mye. For det første, la oss vurdere et ledelsesantimønster som et eksempel.

1. Analytisk lammelse

Analyse lammelseregnes som et klassisk antimønster for ledelse. Det innebærer å overanalysere situasjonen under planleggingen, slik at ingen beslutning eller handling blir tatt, noe som i hovedsak lammer utviklingsprosessen. Dette skjer ofte når målet er å oppnå perfeksjon og vurdere absolutt alt i løpet av analyseperioden. Dette anti-mønsteret er preget av å gå i sirkler (en løpende lukket sløyfe), revidere og lage detaljerte modeller, som igjen forstyrrer arbeidsflyten. For eksempel prøver du å forutsi ting på et nivå: men hva om en bruker plutselig ønsker å lage en liste over ansatte basert på den fjerde og femte bokstaven i navnet deres, inkludert listen over prosjekter de brukte mest arbeidstid på mellom nyttår og internasjonale kvinnedagen de siste fire årene? I hovedsak er det' er for mye analyse. Her er noen tips for å bekjempe analyselammelse:
  1. Du må definere et langsiktig mål som et fyrtårn for beslutningstaking, slik at hver av dine beslutninger flytter deg nærmere målet i stedet for å få deg til å stagnere.
  2. Ikke konsentrer deg om bagateller (hvorfor ta en beslutning om en ubetydelig detalj som om det var den viktigste avgjørelsen i livet ditt?)
  3. Sett en frist for vedtak.
  4. Ikke prøv å fullføre en oppgave perfekt - det er bedre å gjøre det veldig bra.
Ingen grunn til å gå for dypt her, så vi vil ikke vurdere andre ledelsesmessige antimønstre. Derfor, uten noen introduksjon, vil vi gå videre til noen arkitektoniske anti-mønstre, fordi denne artikkelen er mest sannsynlig å bli lest av fremtidige utviklere i stedet for ledere.

2. Gud objekt

Et Gud-objekt er et anti-mønster som beskriver en overdreven konsentrasjon av alle slags funksjoner og store mengder forskjellige data (et objekt som applikasjonen dreier seg om). Ta et lite eksempel:

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();
   }
}
Her ser vi en enorm klasse som gjør alt. Den inneholder databasespørringer samt noen data. Vi ser også findAllWithoutPageEn fasademetoden, som inkluderer forretningslogikk. Et slikt Gudobjekt blir enormt og vanskelig å vedlikeholde. Vi må rote med det i hver kodebit. Mange systemkomponenter er avhengige av det og er tett koblet til det. Det blir vanskeligere og vanskeligere å opprettholde slik kode. I slike tilfeller bør koden deles inn i separate klasser, som hver har bare ett formål. I dette eksemplet kan vi dele opp Gud-objektet i en Dao-klasse:

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 klasse som inneholder data og metoder for å få tilgang til dataene:

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;
   }
                    ....
Og det ville være mer hensiktsmessig å flytte metoden med forretningslogikk til en tjeneste:

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 singleton er det enkleste mønsteret. Det sikrer at det i en enkelt-tråds applikasjon vil være en enkelt forekomst av en klasse, og den gir et globalt tilgangspunkt til dette objektet. Men er det et mønster eller et antimønster? La oss se på ulempene med dette mønsteret:
  1. Global tilstand Når vi får tilgang til forekomsten av klassen, vet vi ikke den nåværende tilstanden til denne klassen. Vi vet ikke hvem som har endret det eller når. Staten er kanskje ikke noe som vi forventer. Med andre ord avhenger riktigheten av å jobbe med en singleton av rekkefølgen på tilganger til den. Dette betyr at delsystemer er avhengige av hverandre, og som et resultat blir et design alvorlig mer komplekst.

  2. En singleton bryter med SOLID-prinsippene – enkeltansvarsprinsippet: i tillegg til sine direkte plikter kontrollerer singleton-klassen også antall forekomster.

  3. En vanlig klasses avhengighet av en singleton er ikke synlig i klassens grensesnitt. Fordi en singleton-forekomst vanligvis ikke sendes som et metodeargument, men i stedet hentes direkte gjennom getInstance(), må du gå inn i implementeringen av hver metode for å identifisere klassens avhengighet av singletonen - bare se på en klasses offentlige kontrakt er ikke nok.

    Tilstedeværelsen av en singleton reduserer testbarheten til applikasjonen som helhet og klassene som bruker singletonen spesielt. For det første kan du ikke erstatte singletonen med et falskt objekt. For det andre, hvis en singleton har et grensesnitt for å endre tilstanden, vil testene avhenge av hverandre.

    Med andre ord, en singleton øker koblingen, og alt nevnt ovenfor er ikke annet enn en konsekvens av økt kobling.

    Og hvis du tenker deg om, kan du unngå å bruke en singleton. For eksempel er det fullt mulig (og faktisk nødvendig) å bruke ulike typer fabrikker for å kontrollere antall forekomster av et objekt.

    Den største faren ligger i et forsøk på å bygge en hel applikasjonsarkitektur basert på singletons. Det er tonnevis av fantastiske alternativer til denne tilnærmingen. Det viktigste eksemplet er Spring, nemlig dets IoC-beholdere: de er en naturlig løsning på problemet med å kontrollere opprettelsen av tjenester, siden de faktisk er "fabrikker på steroider".

    Mange uendelige og uforsonlige debatter raser nå om dette emnet. Det er opp til deg å bestemme om en singleton er et mønster eller anti-mønster.

    Vi vil ikke dvele ved det. I stedet går vi videre til det siste designmønsteret for i dag - poltergeist.

4. Poltergeist

En poltergeist er et antimønster som involverer en meningsløs klasse som brukes til å kalle metoder fra en annen klasse eller bare legger til et unødvendig lag av abstraksjon. Dette anti-mønsteret manifesterer seg som kortlivede objekter, blottet for stat. Disse objektene brukes ofte til å initialisere andre, mer permanente objekter.

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);
   }
}
Hvorfor trenger vi et objekt som bare er et mellomledd og delegerer arbeidet til noen andre? Vi eliminerer den og overfører den lille funksjonaliteten den hadde til gjenstander med lang levetid. Deretter går vi videre til mønstrene som er av mest interesse for oss (som vanlige utviklere), dvs. utviklingsantimønstre .

5. Hard koding

Så vi har kommet til dette forferdelige ordet: hard koding. Essensen av dette antimønsteret er at koden er sterkt knyttet til en spesifikk maskinvarekonfigurasjon og/eller systemmiljø. Dette kompliserer portering av koden til andre konfigurasjoner. Dette anti-mønsteret er nært forbundet med magiske tall (disse anti-mønstrene er ofte sammenvevd). Eksempel:

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;
}
Gjør det vondt, ikke sant? Her koder vi tilkoblingsinnstillingene våre hardt. Som et resultat vil koden bare fungere riktig med MySQL. For å endre databasen, må vi dykke ned i koden og endre alt manuelt. En god løsning ville være å legge konfigurasjonen i en egen 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
Et annet alternativ er å bruke konstanter.

6. Båtanker

I sammenheng med anti-mønstre betyr et båtanker å beholde deler av systemet som ikke lenger brukes etter å ha utført en viss optimering eller refaktorisering. Noen deler av koden kan også beholdes "for fremtidig bruk" i tilfelle du plutselig trenger dem. I hovedsak gjør dette koden din til en søppelbøtte. Eksempel:

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 oppdateringsmetode som bruker en egen metode for å slå sammen brukerdata fra databasen med brukerdataene som er sendt til metoden (hvis brukeren som ble sendt til oppdateringsmetoden har et nullfelt, blir den gamle feltverdien hentet fra databasen) . Anta så at det er et nytt krav om at postene ikke må slås sammen med de gamle, men i stedet, selv om det er null-felt, brukes de til å overskrive de gamle:

public User update(Long id, User request) {
   return userDAO.update(user);
}
Dette betyr at mergeUser ikke lenger brukes, men det ville være synd å slette det - hva om denne metoden (eller ideen om denne metoden) kan komme til nytte en dag? Slik kode kompliserer bare systemer og introduserer forvirring, og har i hovedsak ingen praktisk verdi. Vi må ikke glemme at slik kode med «døde brikker» vil være vanskelig å gi videre til en kollega når du drar til et annet prosjekt. Den beste måten å håndtere båtankre på er å refaktorere koden, dvs. slette deler av kode (hjerteskjærende, jeg vet). I tillegg, når du utarbeider utviklingsplanen, er det nødvendig å gjøre rede for slike ankere (for å avsette tid til opprydding).

7. Gjenstandsbrønn

For å beskrive dette antimønsteret, må du først gjøre deg kjent med objektbassengmønsteret . En objektpool (ressurspool) er et kreativt designmønster , et sett med initialiserte og klare til bruk objekter. Når en applikasjon trenger et objekt, blir det hentet fra dette bassenget i stedet for å bli gjenskapt. Når en gjenstand ikke lenger er nødvendig, blir den ikke ødelagt. I stedet føres den tilbake til bassenget. Dette mønsteret brukes vanligvis for tunge objekter som er tidkrevende å lage hver gang de trengs, for eksempel når du kobler til en database. La oss se på et lite og enkelt eksempel. Her er en klasse som representerer dette mønsteret:

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);
   }
}
Denne klassen presenteres i form av ovennevnte singleton- mønster/anti-mønster, dvs. at det kun kan være ett objekt av denne typen. Den bruker visse Resourcegjenstander. Som standard fyller konstruktøren bassenget med 4 forekomster. Når du får et objekt, fjernes det fra bassenget (hvis det ikke er noe tilgjengelig objekt, opprettes det og returneres umiddelbart). Og på slutten har vi en metode for å sette gjenstanden tilbake. Ressursobjekter ser slik ut:

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;
   }
}
Her har vi et lite objekt som inneholder et kart med designmønsternavn som nøkkel og tilsvarende Wikipedia-lenker som verdi, samt metoder for å få tilgang til kartet. La oss ta en titt på hoveddelen:

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);
   }
}
Alt her er klart nok: vi henter et bassengobjekt, henter et objekt med ressurser fra bassenget, henter kartet fra Ressursobjekt, gjør noe med det, og legger alt dette på plass i bassenget for videre gjenbruk. Voila, dette er objektbassengdesignmønsteret. Men vi snakket om anti-mønstre, ikke sant? La oss vurdere følgende tilfelle i hovedmetoden:

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);
Her får vi igjen et ressursobjekt, vi får kartet over mønstrene, og vi gjør noe med kartet. Men før kartet lagres tilbake til utvalget av objekter, blir det tømt og deretter fylt med ødelagte data, noe som gjør ressursobjektet uegnet for gjenbruk. En av hoveddetaljene til en objektpool er at når et objekt returneres, må det gjenopprettes til en tilstand som er egnet for videre gjenbruk. Hvis gjenstander som returneres til bassenget forblir i en feil eller udefinert tilstand, kalles designen vår en objektbrønn. Er det fornuftig å lagre gjenstander som ikke er egnet for gjenbruk? I denne situasjonen kan vi gjøre det interne kartet uforanderlig i konstruktøren:

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);
}
Forsøk og ønsket om å endre kartets innhold vil forsvinne takket være UnsupportedOperationException de vil generere. Anti-mønstre er feller som utviklere møter ofte på grunn av akutt mangel på tid, uforsiktighet, uerfarenhet eller press fra prosjektledere. Rusing, som er vanlig, kan føre til store problemer for applikasjonen i fremtiden, så du må vite om disse feilene og unngå dem på forhånd. Dette avslutter første del av artikkelen. Fortsettelse følger...
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION