CodeGym/Java Blog/Willekeurig/Wat zijn antipatronen? Laten we een paar voorbeelden beki...
John Squirrels
Niveau 41
San Francisco

Wat zijn antipatronen? Laten we een paar voorbeelden bekijken (deel 1)

Gepubliceerd in de groep Willekeurig
Goededag aan iedereen! Laatst had ik een sollicitatiegesprek en er werden mij enkele vragen gesteld over antipatronen: wat ze zijn, welke soorten er zijn en welke praktische voorbeelden er zijn. Natuurlijk heb ik de vraag beantwoord, maar erg oppervlakkig, aangezien ik nog niet eerder diep in dit onderwerp was gedoken. Na het interview begon ik het internet af te struinen en verdiepte ik me steeds meer in het onderwerp. Wat zijn antipatronen?  Laten we enkele voorbeelden bekijken (deel 1) - 1 Vandaag wil ik een kort overzicht geven van de meest populaire antipatronen en enkele voorbeelden bekijken. Ik hoop dat je door dit te lezen de kennis krijgt die je op dit gebied nodig hebt. Laten we beginnen! Voordat we bespreken wat een anti-patroon is, laten we ons herinneren wat een ontwerppatroon is. Een ontwerppatroonis een herhaalbare architectonische oplossing voor veelvoorkomende problemen of situaties die zich voordoen bij het ontwerpen van een applicatie. Maar vandaag hebben we het niet over hen, maar eerder over hun tegenpolen - antipatronen. Een anti-patroon is een wijdverspreide maar ineffectieve, risicovolle en/of onproductieve benadering om een ​​klasse van veelvoorkomende problemen op te lossen. Met andere woorden, dit is een patroon van fouten (soms ook wel een valkuil genoemd). In de regel zijn antipatronen onderverdeeld in de volgende typen:
  1. Architecturale antipatronen - Deze antipatronen ontstaan ​​wanneer de structuur van een systeem wordt ontworpen (meestal door een architect).
  2. Management/organisatorische anti-patronen — Dit zijn anti-patronen in projectmanagement, waar verschillende managers (of groepen managers) meestal mee te maken hebben.
  3. Anti-ontwikkelingspatronen - Deze anti-patronen ontstaan ​​wanneer een systeem wordt geïmplementeerd door gewone programmeurs.
Het volledige scala aan antipatronen is veel exotischer, maar we zullen ze vandaag niet allemaal in overweging nemen. Voor gewone ontwikkelaars zou dat te veel zijn. Laten we om te beginnen een anti-managementpatroon als voorbeeld beschouwen.

1. Analytische verlamming

Analyse verlammingwordt beschouwd als een klassiek management-antipatroon. Het houdt in dat de situatie tijdens de planning te veel wordt geanalyseerd, zodat er geen beslissing of actie wordt genomen, waardoor het ontwikkelingsproces in feite wordt verlamd. Dit gebeurt vaak wanneer het doel is om perfectie te bereiken en absoluut alles te overwegen tijdens de analyseperiode. Dit anti-patroon wordt gekenmerkt door in cirkels te lopen (een alledaags gesloten lus), het herzien en maken van gedetailleerde modellen, wat op zijn beurt de workflow verstoort. Je probeert bijvoorbeeld dingen op een niveau te voorspellen: maar wat als een gebruiker ineens een lijst met werknemers wil maken op basis van de vierde en vijfde letter van hun naam, inclusief de lijst met projecten waaraan ze de meeste werkuren hebben besteed tussen nieuwjaar en internationale vrouwendag in de afgelopen vier jaar? In wezen is het' Het is te veel analyse. Hier zijn een paar tips om analyseverlamming tegen te gaan:
  1. U moet een langetermijndoel definiëren als een baken voor besluitvorming, zodat elk van uw beslissingen u dichter bij het doel brengt in plaats van u te laten stagneren.
  2. Concentreer u niet op kleinigheden (waarom een ​​beslissing nemen over een onbeduidend detail alsof het de belangrijkste beslissing van uw leven is?)
  3. Stel een deadline voor een beslissing.
  4. Probeer een taak niet perfect uit te voeren - het is beter om het heel goed te doen.
Het is niet nodig om hier al te diep op in te gaan, dus we zullen geen rekening houden met andere management-antipatronen. Daarom gaan we zonder enige introductie verder met enkele architecturale antipatronen, omdat dit artikel waarschijnlijk eerder door toekomstige ontwikkelaars dan door managers zal worden gelezen.

2. God maakt bezwaar

Een God-object is een anti-patroon dat een overmatige concentratie van allerlei functies en grote hoeveelheden ongelijksoortige gegevens (een object waar de applicatie om draait) beschrijft. Neem een ​​klein voorbeeld:
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();
   }
}
Hier zien we een enorme klas die alles doet. Het bevat databasequery's en enkele gegevens. We zien ook de gevelmethode findAllWithoutPageEn, die bedrijfslogica bevat. Zo'n God-object wordt enorm en onhandig om goed te onderhouden. We moeten er in elk stukje code mee rommelen. Veel systeemcomponenten vertrouwen erop en zijn er nauw mee verbonden. Het wordt steeds moeilijker om dergelijke code te onderhouden. In dergelijke gevallen moet de code worden opgesplitst in afzonderlijke klassen, die elk slechts één doel hebben. In dit voorbeeld kunnen we het God-object opsplitsen in een 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());
   }

                               ........
}
Een klasse met gegevens en methoden voor toegang tot de gegevens:
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;
   }
                    ....
En het zou passender zijn om de methode met bedrijfslogica naar een service te verplaatsen:
private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3. Eenling

Een singleton is het eenvoudigste patroon. Het zorgt ervoor dat er in een toepassing met één thread één enkele instantie van een klasse is, en het biedt een wereldwijd toegangspunt tot dit object. Maar is het een patroon of een antipatroon? Laten we eens kijken naar de nadelen van dit patroon:
  1. Globale status Wanneer we toegang krijgen tot de instantie van de klasse, kennen we de huidige status van deze klasse niet. We weten niet wie het heeft gewijzigd of wanneer. De staat lijkt misschien niet op wat we verwachten. Met andere woorden, de juistheid van het werken met een singleton hangt af van de volgorde van toegangen. Hierdoor zijn subsystemen van elkaar afhankelijk en wordt een ontwerp serieus complexer.

  2. Een singleton schendt de SOLID-principes - het single responsibility-principe: naast zijn directe taken controleert de singleton-klasse ook het aantal instanties.

  3. De afhankelijkheid van een gewone klasse van een singleton is niet zichtbaar in de interface van de klasse. Omdat een singleton-instantie gewoonlijk niet wordt doorgegeven als een methodeargument, maar in plaats daarvan rechtstreeks wordt verkregen via getInstance(), moet u de implementatie van elke methode ingaan om de afhankelijkheid van de klasse van de singleton te identificeren — u hoeft alleen maar naar de public van een klasse te kijken overeenkomst is niet voldoende.

    De aanwezigheid van een singleton vermindert de testbaarheid van de applicatie als geheel en de klassen die de singleton in het bijzonder gebruiken. Allereerst kun je de singleton niet vervangen door een nepobject. Ten tweede, als een singleton een interface heeft om zijn status te veranderen, dan zullen de tests van elkaar afhangen.

    Met andere woorden, een singleton verhoogt de koppeling, en al het bovenstaande is niets meer dan een gevolg van een verhoogde koppeling.

    En als je erover nadenkt, kun je voorkomen dat je een singleton gebruikt. Het is bijvoorbeeld heel goed mogelijk (en zelfs noodzakelijk) om verschillende soorten fabrieken te gebruiken om het aantal exemplaren van een object te controleren.

    Het grootste gevaar schuilt in een poging om een ​​hele applicatiearchitectuur te bouwen op basis van singletons. Er zijn tal van prachtige alternatieven voor deze aanpak. Het belangrijkste voorbeeld is Spring, namelijk de IoC-containers: ze zijn een natuurlijke oplossing voor het probleem van het controleren van de creatie van services, aangezien het eigenlijk "fabrieken op steroïden" zijn.

    Veel eindeloze en onverzoenlijke debatten woeden nu over dit onderwerp. Het is aan jou om te beslissen of een singleton een patroon of een antipatroon is.

    We blijven er niet bij stilstaan. In plaats daarvan gaan we verder met het laatste ontwerppatroon voor vandaag: poltergeist.

4. Poltergeist

Een poltergeist is een anti-patroon waarbij een zinloze klasse betrokken is die wordt gebruikt om methoden van een andere klasse aan te roepen of simpelweg een onnodige abstractielaag toevoegt. Dit anti-patroon manifesteert zich als objecten met een korte levensduur, zonder staat. Deze objecten worden vaak gebruikt om andere, meer permanente objecten te initialiseren.
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);
   }
}
Waarom hebben we een object nodig dat slechts een tussenpersoon is en zijn werk aan iemand anders delegeert? We elimineren het en brengen de kleine functionaliteit die het had over naar objecten met een lange levensduur. Vervolgens gaan we verder met de patronen die voor ons (als gewone ontwikkelaars) het meest interessant zijn, dwz anti-ontwikkelingspatronen .

5. Harde codering

Dus we zijn aangekomen bij dit vreselijke woord: harde codering. De essentie van dit antipatroon is dat de code sterk gebonden is aan een specifieke hardwareconfiguratie en/of systeemomgeving. Dit bemoeilijkt het porteren van de code naar andere configuraties aanzienlijk. Dit anti-patroon is nauw verbonden met magische getallen (deze anti-patronen zijn vaak met elkaar verweven). Voorbeeld:
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;
}
Doet pijn, nietwaar? Hier coderen we onze verbindingsinstellingen hard. Als gevolg hiervan werkt de code alleen correct met MySQL. Om de database te wijzigen, moeten we in de code duiken en alles handmatig wijzigen. Een goede oplossing zou zijn om de configuratie in een apart bestand te zetten:
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Een andere optie is het gebruik van constanten.

6. Bootanker

In de context van anti-patronen betekent een bootanker het behouden van delen van het systeem die niet langer worden gebruikt na enige optimalisatie of refactoring. Sommige delen van de code kunnen ook worden bewaard "voor toekomstig gebruik", voor het geval u ze plotseling nodig heeft. In wezen verandert dit uw code in een vuilnisbak. Voorbeeld:
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());
}
We hebben een updatemethode die een aparte methode gebruikt om gebruikersgegevens uit de database samen te voegen met de gebruikersgegevens die aan de methode zijn doorgegeven (als de gebruiker die is doorgegeven aan de updatemethode een null-veld heeft, wordt de oude veldwaarde uit de database gehaald) . Stel dat er een nieuwe vereiste is dat de records niet moeten worden samengevoegd met de oude, maar dat ze in plaats daarvan, zelfs als er null-velden zijn, worden gebruikt om de oude te overschrijven:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Dit betekent dat mergeUser niet langer wordt gebruikt, maar het zou jammer zijn om het te verwijderen — wat als deze methode (of het idee van deze methode) ooit van pas zou komen? Dergelijke code maakt systemen alleen maar ingewikkelder en leidt tot verwarring, die in wezen geen praktische waarde heeft. We moeten niet vergeten dat dergelijke code met "dode stukken" moeilijk door te geven is aan een collega wanneer je naar een ander project vertrekt. De beste manier om met bootankers om te gaan, is door de code te herstructureren, dwz delen van de code te verwijderen (hartverscheurend, ik weet het). Bovendien is het bij het opstellen van het ontwikkelingsschema noodzakelijk om rekening te houden met dergelijke ankers (om tijd vrij te maken voor opruimen).

7. Object beerput

Om dit antipatroon te beschrijven, moet u eerst kennis maken met het objectpoolpatroon . Een objectpool (resource pool) is een creatief ontwerppatroon , een set geïnitialiseerde en gebruiksklare objecten. Wanneer een toepassing een object nodig heeft, wordt het uit deze pool gehaald in plaats van opnieuw gemaakt. Wanneer een object niet langer nodig is, wordt het niet vernietigd. In plaats daarvan wordt het teruggebracht naar het zwembad. Dit patroon wordt meestal gebruikt voor zware objecten die tijdrovend zijn om te maken elke keer dat ze nodig zijn, zoals bij het verbinden met een database. Laten we eens kijken naar een klein en eenvoudig voorbeeld. Hier is een klasse die dit patroon vertegenwoordigt:
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);
   }
}
Deze klasse wordt gepresenteerd in de vorm van het bovenstaande singletonpatroon /antipatroon, dwz er kan slechts één object van dit type zijn. Het gebruikt bepaalde Resourceobjecten. Standaard vult de constructor de pool met 4 instanties. Wanneer u een object krijgt, wordt het uit de pool verwijderd (als er geen beschikbaar object is, wordt er een gemaakt en onmiddellijk geretourneerd). En aan het einde hebben we een methode om het object terug te plaatsen. Resource-objecten zien er als volgt uit:
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;
   }
}
Hier hebben we een klein object dat een kaart bevat met namen van ontwerppatronen als sleutel en overeenkomstige Wikipedia-links als waarde, evenals methoden om toegang te krijgen tot de kaart. Laten we eens kijken naar de belangrijkste:
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);
   }
}
Alles is hier duidelijk genoeg: we halen een poolobject, halen een object met resources uit de pool, halen de kaart uit Resourceobject, doen er iets mee en zetten dit alles op zijn plaats in de pool voor verder hergebruik. Voila, dit is het ontwerppatroon van de objectpool. Maar we hadden het over antipatronen, toch? Laten we het volgende geval in de hoofdmethode bekijken:
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);
Ook hier krijgen we een Resource-object, we krijgen de kaart met patronen en we doen iets met de kaart. Maar voordat de kaart weer wordt opgeslagen in de pool met objecten, wordt deze gewist en vervolgens gevuld met beschadigde gegevens, waardoor het Resource-object ongeschikt wordt voor hergebruik. Een van de belangrijkste details van een objectpool is dat wanneer een object wordt geretourneerd, het moet worden hersteld naar een staat die geschikt is voor verder hergebruik. Als objecten die naar de pool worden teruggestuurd in een onjuiste of ongedefinieerde staat blijven, wordt ons ontwerp een objectbeerput genoemd. Heeft het zin om voorwerpen op te slaan die niet geschikt zijn voor hergebruik? In deze situatie kunnen we de interne kaart onveranderlijk maken in de constructor:
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);
}
Pogingen en de wens om de inhoud van de kaart te veranderen zullen verdwijnen dankzij de UnsupportedOperationException die ze zullen genereren. Anti-patronen zijn valkuilen die ontwikkelaars vaak tegenkomen door acuut tijdgebrek, onvoorzichtigheid, onervarenheid of druk van projectmanagers. Haasten, wat gebruikelijk is, kan in de toekomst tot grote problemen voor de toepassing leiden, dus u moet van deze fouten op de hoogte zijn en ze van tevoren vermijden. Hiermee is het eerste deel van het artikel afgesloten. Wordt vervolgd...
Opmerkingen
  • Populair
  • Nieuw
  • Oud
Je moet ingelogd zijn om opmerkingen te kunnen maken
Deze pagina heeft nog geen opmerkingen