CodeGym/Java blog/Tilfældig/Hvad er anti-mønstre? Lad os se på nogle eksempler (del 1...
John Squirrels
Niveau
San Francisco

Hvad er anti-mønstre? Lad os se på nogle eksempler (del 1)

Udgivet i gruppen
God dag til alle! Forleden var jeg til jobsamtale, og jeg fik nogle spørgsmål om anti-mønstre: hvad er det, hvilke typer findes der, og hvilke praktiske eksempler der er. Jeg svarede selvfølgelig på spørgsmålet, men meget overfladisk, da jeg ikke tidligere var dykket dybt ned i dette emne. Efter interviewet begyndte jeg at gennemsøge internettet og fordybede mig mere og mere i emnet. Hvad er anti-mønstre?  Lad os se på nogle eksempler (del 1) - 1 I dag vil jeg gerne give et kort overblik over de mest populære anti-mønstre og gennemgå nogle eksempler. Jeg håber, at læsning af dette vil give dig den viden, du har brug for på dette område. Lad os komme igang! Før vi diskuterer, hvad et anti-mønster er, lad os huske, hvad et designmønster er. Et designmønsterer en gentagelig arkitektonisk løsning til almindelige problemer eller situationer, der opstår, når man designer en applikation. Men i dag taler vi ikke om dem, men snarere deres modsætninger - anti-mønstre. Et anti-mønster er en udbredt, men ineffektiv, risikabel og/eller uproduktiv tilgang til at løse en klasse af almindelige problemer. Med andre ord er dette et mønster af fejl (også nogle gange kaldet en fælde). Som regel er anti-mønstre opdelt i følgende typer:
  1. Arkitektoniske anti-mønstre - Disse anti-mønstre opstår, når strukturen af ​​et system er designet (generelt af en arkitekt).
  2. Ledelses-/organisatoriske anti-mønstre - Disse er anti-mønstre i projektledelse, som normalt støder på forskellige ledere (eller grupper af ledere).
  3. Udviklings-anti-mønstre — Disse anti-mønstre opstår, når et system implementeres af almindelige programmører.
Hele udvalget af anti-mønstre er langt mere eksotiske, men vi vil ikke overveje dem alle i dag. For almindelige udviklere ville det være for meget. Lad os for det første betragte et ledelses-anti-mønster som et eksempel.

1. Analytisk lammelse

Analyse lammelsebetragtes som et klassisk management-anti-mønster. Det indebærer at overanalysere situationen under planlægningen, så der ikke træffes nogen beslutning eller handling, hvilket i det væsentlige lammer udviklingsprocessen. Dette sker ofte, når målet er at opnå perfektion og overveje absolut alt i analyseperioden. Dette anti-mønster er kendetegnet ved at gå i cirkler (en løbende lukket løkke), revidere og skabe detaljerede modeller, som igen forstyrrer arbejdsgangen. For eksempel forsøger du at forudsige ting på et niveau: men hvad nu hvis en bruger pludselig vil oprette en liste over medarbejdere baseret på det fjerde og femte bogstav i deres navn, inklusive listen over projekter, som de har brugt flest arbejdstimer på mellem nytår og kvindernes internationale kampdag de seneste fire år? I bund og grund er det' s for meget analyse. Her er et par tips til at bekæmpe analyselammelse:
  1. Du skal definere et langsigtet mål som et fyrtårn for beslutningstagning, så hver af dine beslutninger flytter dig tættere på målet i stedet for at få dig til at stagnere.
  2. Koncentrer dig ikke om bagateller (hvorfor tage en beslutning om en ubetydelig detalje, som om det var den vigtigste beslutning i dit liv?)
  3. Sæt en frist for en beslutning.
  4. Forsøg ikke at fuldføre en opgave perfekt - det er bedre at gøre det meget godt.
Ingen grund til at gå for dybt her, så vi vil ikke overveje andre ledelsesmæssige anti-mønstre. Derfor vil vi uden nogen introduktion gå videre til nogle arkitektoniske anti-mønstre, fordi denne artikel med stor sandsynlighed vil blive læst af fremtidige udviklere i stedet for ledere.

2. Gud protesterer

Et Gud-objekt er et anti-mønster, der beskriver en overdreven koncentration af alle mulige funktioner og store mængder forskellige data (et objekt, som applikationen kredser om). Tag et lille 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 kæmpe klasse, der gør alt. Den indeholder databaseforespørgsler samt nogle data. Vi ser også findAllWithoutPageEn facademetoden, som inkluderer forretningslogik. Sådan et Gudobjekt bliver enormt og akavet at vedligeholde ordentligt. Vi er nødt til at rode med det i hvert stykke kode. Mange systemkomponenter er afhængige af det og er tæt forbundet med det. Det bliver sværere og sværere at opretholde en sådan kode. I sådanne tilfælde bør koden opdeles i separate klasser, som hver kun har ét formål. I dette eksempel kan vi opdele 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, der indeholder data og metoder til at få adgang 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 mere hensigtsmæssigt at flytte metoden med forretningslogik 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ønster. Det sikrer, at der i en enkelt-trådsapplikation vil være en enkelt forekomst af en klasse, og den giver et globalt adgangspunkt til dette objekt. Men er det et mønster eller et anti-mønster? Lad os se på ulemperne ved dette mønster:
  1. Global tilstand Når vi tilgår forekomsten af ​​klassen, kender vi ikke den aktuelle tilstand for denne klasse. Vi ved ikke, hvem der har ændret det eller hvornår. Staten er måske ikke noget, som vi forventer. Med andre ord afhænger rigtigheden af ​​at arbejde med en singleton af rækkefølgen af ​​adgange til den. Det betyder, at delsystemer er afhængige af hinanden, og som følge heraf bliver et design for alvor mere komplekst.

  2. En singleton overtræder SOLID-principperne — enkeltansvarsprincippet: Ud over sine direkte pligter kontrollerer singleton-klassen også antallet af instanser.

  3. En almindelig klasses afhængighed af en singleton er ikke synlig i klassens grænseflade. Fordi en singleton-instans normalt ikke videregives som et metode-argument, men i stedet opnås direkte gennem getInstance(), er du nødt til at komme ind i implementeringen af ​​hver metode for at identificere klassens afhængighed af singletonen - bare at se på en klasses offentlige kontrakt er ikke nok.

    Tilstedeværelsen af ​​en singleton reducerer testbarheden af ​​applikationen som helhed og de klasser, der bruger singletonen i særdeleshed. Først og fremmest kan du ikke erstatte singletonen med et mock objekt. For det andet, hvis en singleton har en grænseflade til at ændre dens tilstand, vil testene afhænge af hinanden.

    Med andre ord, en singleton øger koblingen, og alt nævnt ovenfor er intet andet end en konsekvens af øget kobling.

    Og hvis du tænker over det, kan du undgå at bruge en singleton. For eksempel er det meget muligt (og faktisk nødvendigt) at bruge forskellige slags fabrikker til at kontrollere antallet af forekomster af et objekt.

    Den største fare ligger i et forsøg på at bygge en hel applikationsarkitektur baseret på singletons. Der er tonsvis af vidunderlige alternativer til denne tilgang. Det vigtigste eksempel er Spring, nemlig dets IoC-beholdere: de er en naturlig løsning på problemet med at kontrollere oprettelsen af ​​tjenester, da de faktisk er "fabrikker på steroider".

    Mange uendelige og uforsonlige debatter raser nu om dette emne. Det er op til dig at afgøre, om en singleton er et mønster eller et anti-mønster.

    Vi vil ikke dvæle ved det. I stedet går vi videre til det sidste designmønster for i dag - poltergeist.

4. Poltergeist

En poltergeist er et anti-mønster, der involverer en meningsløs klasse, der bruges til at kalde metoder fra en anden klasse eller blot tilføjer et unødvendigt lag af abstraktion. Dette anti-mønster manifesterer sig som kortlivede objekter, blottet for stat. Disse objekter bruges ofte til at initialisere andre, mere 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 har vi brug for et objekt, der blot er en mellemmand og uddelegerer sit arbejde til en anden? Vi fjerner det og overfører den lille funktionalitet, det havde, til objekter med lang levetid. Dernæst går vi videre til de mønstre, der har størst interesse for os (som almindelige udviklere), altså udviklings-anti-mønstre .

5. Hård kodning

Så vi er nået frem til dette forfærdelige ord: hård kodning. Essensen af ​​dette anti-mønster er, at koden er stærkt knyttet til en specifik hardwarekonfiguration og/eller systemmiljø. Dette komplicerer i høj grad porteringen af ​​koden til andre konfigurationer. Dette anti-mønster er tæt forbundet med magiske tal (disse anti-mønstre er ofte sammenflettet). 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;
}
Det gør ondt, gør det ikke? Her koder vi vores forbindelsesindstillinger hårdt. Som et resultat vil koden kun fungere korrekt med MySQL. For at ændre databasen skal vi dykke ned i koden og ændre alt manuelt. En god løsning ville være at lægge 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
En anden mulighed er at bruge konstanter.

6. Bådanker

I forbindelse med anti-mønstre betyder et bådanker at beholde dele af systemet, som ikke længere bruges efter at have udført en vis optimering eller refactoring. Nogle dele af koden kan også opbevares "til fremtidig brug", hvis du pludselig får brug for dem. I bund og grund forvandler dette din kode til en skraldespand. 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 opdateringsmetode, der bruger en separat metode til at flette brugerdata fra databasen med de brugerdata, der er videregivet til metoden (hvis brugeren, der er videregivet til opdateringsmetoden, har et nulfelt, tages den gamle feltværdi fra databasen) . Antag så, at der er et nyt krav om, at posterne ikke må flettes med de gamle, men i stedet, selvom der er nulfelter, bruges de til at overskrive de gamle:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Det betyder, at mergeUser ikke længere bruges, men det ville være ærgerligt at slette det - hvad nu hvis denne metode (eller ideen med denne metode) kunne komme til nytte en dag? En sådan kode komplicerer kun systemer og introducerer forvirring, idet den i det væsentlige ikke har nogen praktisk værdi. Vi må ikke glemme, at sådan en kode med "døde brikker" vil være svære at give videre til en kollega, når du tager afsted til et andet projekt. Den bedste måde at håndtere bådankre på er at omfaktorere koden, dvs. slette dele af kode (hjerteskærende, jeg ved det). Når udviklingsplanen udarbejdes, er det desuden nødvendigt at tage højde for sådanne ankre (for at allokere tid til oprydning).

7. Genstandsbrøndbrønd

For at beskrive dette anti-mønster skal du først stifte bekendtskab med objektpuljemønsteret . En objektpulje (ressourcepulje) er et kreativt designmønster , et sæt initialiserede og klar-til-brug objekter. Når en applikation har brug for et objekt, tages det fra denne pulje i stedet for at blive genskabt. Når en genstand ikke længere er nødvendig, bliver den ikke ødelagt. I stedet føres den tilbage til poolen. Dette mønster bruges normalt til tunge objekter, der er tidskrævende at oprette, hver gang der er brug for dem, såsom når der oprettes forbindelse til en database. Lad os se på et lille og enkelt eksempel. Her er en klasse, der repræsenterer dette 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);
   }
}
Denne klasse præsenteres i form af ovenstående singleton- mønster/anti-mønster, dvs. der kan kun være ét objekt af denne type. Den bruger visse Resourcegenstande. Som standard fylder konstruktøren puljen med 4 forekomster. Når du får et objekt, fjernes det fra puljen (hvis der ikke er et tilgængeligt objekt, oprettes et og returneres straks). Og til sidst har vi en metode til at sætte objektet tilbage. Ressourceobjekter ser sådan ud:
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 lille objekt indeholdende et kort med designmønsternavne som nøgle og tilsvarende Wikipedia-links som værdi, samt metoder til at få adgang til kortet. Lad os tage et kig på hoved:
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 puljeobjekt, henter et objekt med ressourcer fra poolen, henter kortet fra ressourceobjektet, gør noget med det og sætter alt dette på plads i poolen til videre genbrug. Voila, dette er objektpooldesignmønsteret. Men vi talte om anti-mønstre, ikke? Lad os overveje følgende tilfælde 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 igen et ressourceobjekt, vi får dets kort over mønstre, og vi gør noget med kortet. Men før du gemmer kortet tilbage til puljen af ​​objekter, bliver det ryddet og derefter udfyldt med korrupte data, hvilket gør ressourceobjektet uegnet til genbrug. En af hoveddetaljerne i en objektpulje er, at når et objekt returneres, skal det gendanne til en tilstand, der er egnet til yderligere genbrug. Hvis objekter, der returneres til puljen, forbliver i en forkert eller udefineret tilstand, kaldes vores design en objektafløbsbrønd. Giver det nogen mening at opbevare genstande, der ikke er egnet til genbrug? I denne situation kan vi gøre det interne kort uforanderligt 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øg og ønsket om at ændre kortets indhold vil forsvinde takket være UnsupportedOperationException, de vil generere. Anti-mønstre er fælder, som udviklere ofte støder på på grund af akut mangel på tid, skødesløshed, uerfarenhed eller pres fra projektledere. Rusing, som er almindeligt, kan føre til store problemer for applikationen i fremtiden, så du skal kende til disse fejl og undgå dem på forhånd. Dette afslutter første del af artiklen. Fortsættes...
Kommentarer
  • Populær
  • Ny
  • Gammel
Du skal være logget ind for at skrive en kommentar
Denne side har ingen kommentarer endnu