JavaRush /Blog Java /Random-PL /Czym są antywzorce? Spójrzmy na przykłady (część 1)

Czym są antywzorce? Spójrzmy na przykłady (część 1)

Opublikowano w grupie Random-PL
Czym są antywzorce?  Spójrzmy na przykłady (część 1) - 1Dzień dobry wszystkim! Któregoś dnia udzieliłem wywiadu i zadano mi pytanie dotyczące antywzorców: co to za bestia, jakie są ich rodzaje i przykłady w praktyce. Oczywiście odpowiedziałem na pytanie, ale bardzo powierzchownie, ponieważ nie zagłębiałem się w badanie tego zagadnienia. Po rozmowie zacząłem przeszukiwać Internet, coraz bardziej zagłębiając się w ten temat. Dzisiaj chciałbym dokonać krótkiego przeglądu najpopularniejszych antywzorców i ich przykładów, których lektura może dostarczyć Państwu niezbędnej wiedzy na ten temat. Zacznijmy! Zanim więc omówimy, czym jest antywzór, przypomnijmy sobie, czym jest wzór. Wzorzec to powtarzalny projekt architektoniczny służący rozwiązywaniu typowych problemów lub sytuacji pojawiających się podczas projektowania aplikacji. Ale dzisiaj nie mówimy o nich, ale o ich przeciwieństwach - antywzorcach. Antywzorzec to powszechne podejście do rozwiązywania klasy często spotykanych problemów, które jest nieskuteczne, ryzykowne lub nieproduktywne. Innymi słowy, jest to wzorzec błędu (czasami nazywany także pułapką). Czym są antywzorce?  Spójrzmy na przykłady (część 1) - 2Z reguły antywzory dzielą się na następujące typy:
  1. Antywzorce architektoniczne - antywzorce architektoniczne powstające podczas projektowania konstrukcji systemu (zwykle przez architekta).
  2. Antywzorzec zarządzania - antywzorce w obszarze zarządzania, z którymi spotykają się najczęściej różni menedżerowie (lub grupy menedżerów).
  3. Antywzorce rozwojowe - antywzorce to problemy programistyczne, które pojawiają się, gdy zwykli programiści piszą system.
Egzotyka antywzorców jest znacznie szersza, ale nie będziemy ich dzisiaj rozważać, ponieważ dla zwykłych programistów będzie to przytłaczające. Na początek weźmy przykład antywzorca z dziedziny zarządzania.

1. Paraliż analityczny

Paraliż analityczny uważany jest za klasyczny antywzorzec organizacyjny. Polega ona na nadmiernej analizie sytuacji podczas planowania, w wyniku której nie zostaje podjęta żadna decyzja ani działanie, co zasadniczo paraliżuje rozwój. Dzieje się tak często, gdy celem jest osiągnięcie perfekcji i całkowite zakończenie okresu analizy. Ten anty-wzór charakteryzuje się chodzeniem w kółko (rodzaj zamkniętej pętli), przeglądaniem i tworzeniem szczegółowych modeli, co z kolei zakłóca tok pracy. Na przykład próbujesz przewidzieć takie rzeczy jak: co się stanie, jeśli użytkownik nagle będzie chciał stworzyć listę pracowników na podstawie czwartej i piątej litery swojego nazwiska, uwzględniającą projekty, na które poświęcił najwięcej czasu pracy pomiędzy Nowym Rokiem a Nowym Rokiem ósmego marca w ciągu czterech poprzednich lat? W istocie jest to nadmiar analiz. Dobrym przykładem z życia wziętym jest paraliż analityczny, który doprowadził firmę Kodak do bankructwa . Oto kilka krótkich wskazówek, jak walczyć z paraliżem analitycznym:
  1. Musisz zdefiniować cel długoterminowy jako drogowskaz przy podejmowaniu decyzji, tak aby każda decyzja, którą podejmiesz, przybliżała Cię do celu, a nie zmuszała do wyznaczania czasu.
  2. Nie skupiaj się na drobnostkach (po co podejmować decyzję o drobnym niuansie, jakby był ostatnim w życiu?)
  3. Ustal termin na podjęcie decyzji.
  4. Nie staraj się wykonać zadania doskonale: lepiej zrobić to bardzo dobrze.
Nie będziemy teraz wchodzić zbyt głęboko i rozważać inne antywzorce zarządzania. Zatem bez wstępów przejdźmy do niektórych antywzorców architektonicznych, gdyż najprawdopodobniej ten artykuł czytają przyszli deweloperzy, a nie menadżerowie.

2. Obiekt Boga

Boski obiekt to anty-wzorzec opisujący nadmierną koncentrację zbyt wielu odrębnych funkcji, przechowujący dużą ilość różnorodnych danych (obiekt, wokół którego kręci się aplikacja). Weźmy mały przykład:
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();
   }
}
Widzimy tutaj rodzaj dużej klasy, która robi wszystko na raz. Zawiera zapytania do Bazy Danych, zawiera pewne dane, widzimy też metodę fasadową findAllWithoutPageEnz logiką biznesową. Taki boski przedmiot staje się ogromny i nieporęczny w odpowiednim utrzymaniu. Musimy majstrować przy nim w każdym fragmencie kodu: wiele węzłów w systemie na nim polega i jest z nim ściśle powiązanych. Utrzymanie takiego kodu staje się coraz trudniejsze. W takich przypadkach należy go podzielić na osobne klasy, z których każda będzie miała tylko jeden cel (cel). W tym przykładzie możesz podzielić go na klasę dao:
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());
   }

                               ........
}
Klasa zawierająca dane i metody dostępu do nich:
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;
   }
                    ....
A właściwsze byłoby przeniesienie do usługi metody z logiką biznesową:
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

Singleton to najprostszy wzorzec gwarantujący istnienie pojedynczej instancji jakiejś klasy w aplikacji jednowątkowej i zapewniający globalny punkt dostępu do tego obiektu. Więcej na ten temat możesz przeczytać tutaj . Ale czy jest to wzór czy antywzór? Czym są antywzorce?  Spójrzmy na przykłady (część 1) - 3Przyjrzyjmy się wadom tego szablonu:
  1. Stan globalny. Kiedy uzyskujemy dostęp do instancji klasy, nie wiemy, jaki jest bieżący stan tej klasy, kto i kiedy ją zmienił, a stan ten może nie być taki, jakiego oczekujemy. Inaczej mówiąc, poprawność pracy z singletonem zależy od kolejności jego wywołań, co powoduje, że podsystemy są od siebie zależne i w efekcie poważnie zwiększa złożoność rozwoju.

  2. Singleton łamie jedną z zasad SOLID-u - zasadę pojedynczej odpowiedzialności - klasa Singleton oprócz wykonywania swoich bezpośrednich obowiązków kontroluje także liczbę swoich instancji.

  3. Zależność zwykłej klasy od singletonu nie jest widoczna w interfejsie klasy. Ponieważ zwykle instancja singletonu nie jest przekazywana w parametrach metody, ale jest uzyskiwana bezpośrednio poprzez getInstance(), aby zidentyfikować zależność klasy od singletonu, należy zagłębić się w implementację każdej metody - po prostu przeglądając publiczną umowa przedmiotu nie wystarczy.

    Obecność singletonu zmniejsza ogólnie testowalność aplikacji, a w szczególności klas korzystających z singletonu. Po pierwsze, nie można wstawić obiektu Mock w miejsce singletonu, a po drugie, jeśli singleton posiada interfejs umożliwiający zmianę jego stanu, testy będą od siebie zależne.

    Innymi słowy, singleton zwiększa łączność, a wszystkie powyższe są niczym więcej niż konsekwencją zwiększonej łączności.

    A jeśli się nad tym zastanowić, można uniknąć używania singletonu. Na przykład, aby kontrolować liczbę wystąpień obiektu, całkiem możliwe (i konieczne) jest użycie różnego rodzaju fabryk.

    Największe niebezpieczeństwo kryje się w próbie zbudowania całej architektury aplikacji w oparciu o singletony. Istnieje wiele świetnych alternatyw dla tego podejścia. Najważniejszym przykładem jest Spring, a dokładnie jego kontenery IoC: tam problem kontroli tworzenia usług zostaje rozwiązany w sposób naturalny, bo tak naprawdę są to „fabryki na sterydach”.

    Obecnie jest wiele holivarów na ten temat, więc od Ciebie zależy, czy singleton jest wzorcem, czy antywzorem.

    I nie będziemy się nad tym rozwodzić i przejdziemy do ostatniego na dziś wzorca projektowego - poltergeista.

4. Poltergeist

Poltergeist to nieprzydatny antywzorzec klasy, który służy do wywoływania metod innej klasy lub po prostu dodaje niepotrzebną warstwę abstrakcji. Antywzorzec objawia się w postaci krótkotrwałych obiektów pozbawionych stanu. Obiekty te są często używane do inicjowania innych, trwalszych obiektów.
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);
   }
}
Po co nam obiekt, który jest jedynie pośrednikiem i deleguje swoją pracę komuś innemu? Usuwamy go i przenosimy drobne funkcjonalności, które implementuje, do długotrwałych obiektów. Następnie przechodzimy do wzorców, które nas (jako zwykłych programistów) interesują najbardziej – antywzorców deweloperskich .

5.Twardy kod

I tak dotarliśmy do tego okropnego słowa – hardcode. Istotą tego antywzorca jest to, że kod jest silnie powiązany z konkretną konfiguracją sprzętową i/lub środowiskiem systemowym, co bardzo utrudnia przeniesienie go do innych konfiguracji. Ten antywzór jest ściśle powiązany z liczbami magicznymi (często są one ze sobą powiązane). Przykład:
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;
}
Przybity, prawda? Tutaj bezpośrednio ustawiamy konfigurację naszego połączenia, w rezultacie kod będzie działał poprawnie tylko z MySQL, a aby zmienić bazę danych, trzeba będzie wejść do kodu i zmienić wszystko ręcznie. Dobrym rozwiązaniem byłoby umieszczenie konfiguracji w osobnym pliku:
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Inną opcją jest przeniesienie go do stałych.

6. Kotwica łodzi

Kotwica łodzi w kontekście antywzorców oznacza przechowywanie nieużywanych części systemu, które pozostały po optymalizacji lub refaktoryzacji. Ponadto niektóre części kodu można pozostawić „na przyszłość”, na wypadek konieczności ich ponownego użycia. To zasadniczo zamienia kod w kosz na śmieci. Czym są antywzorce?  Spójrzmy na przykłady (część 1) - 4Przykład:
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());
}
Mamy metodę aktualizacji, która wykorzystuje osobną metodę do łączenia danych użytkownika z bazy danych i tego, który przyszedł po aktualizację (jeśli osoba, która przyszła po aktualizację ma puste pole, to jest zapisywane jako stara) z bazy danych). I np. istniał wymóg, aby rekordów nie łączyć ze starymi, lecz nadpisywać, nawet jeśli pola są puste:
public User update(Long id, User request) {
   return userDAO.update(user);
}
W rezultacie mergeUsernie jest już używany i szkoda go usunąć: a co jeśli on (lub jego pomysł) jest nadal przydatny? Taki kod tylko komplikuje i dezorientuje systemy, zasadniczo nie zapewniając żadnej praktycznej wartości. Nie możemy zapominać, że taki kod z „martwymi kawałkami” będzie trudny do przekazania współpracownikowi, gdy będziesz wyjeżdżał do innego projektu. Najlepszą metodą radzenia sobie z kotwicami łodzi jest refaktoryzacja kodu, a mianowicie usunięcie tych sekcji kodu (niestety, niestety). Również planując zagospodarowanie należy uwzględnić występowanie takich kotwic (zapewnić czas na uprzątnięcie odpadów poflotacyjnych).

7.Obiekt szambo

Aby opisać ten antywzorzec, należy najpierw zapoznać się ze wzorcem puli obiektów . Pula obiektów (pula zasobów) to generatywny wzorzec projektowy , zbiór obiektów zainicjowanych i gotowych do użycia. Gdy aplikacja wymaga obiektu, nie jest on tworzony na nowo, lecz pobierany z tej puli. Kiedy obiekt nie jest już potrzebny, nie jest niszczony, ale wraca do puli. Zwykle używane w przypadku ciężkich obiektów, których tworzenie za każdym razem wymaga dużych zasobów, takich jak połączenie z bazą danych. Dla przykładu spójrzmy na mały i prosty przykład. Mamy więc klasę reprezentującą ten wzorzec:
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);
   }
}
Klasę tę prezentujemy w postaci opisanego powyżej wzorca/antywzorca singletonu , czyli może istnieć tylko jeden obiekt tego typu, operuje on na określonych obiektach Resource, domyślnie w konstruktorze pula jest wypełniona 4 kopiami; gdy taki przedmiot zostanie zabrany, jest on usuwany z puli (jeśli go nie ma, jest tworzony i od razu oddawany), a na koniec pozostaje metoda na odłożenie obiektu z powrotem. Obiekty Resourcewyglądają tak:
public class Resource {
   private Map<String, String> patterns;
   public Resource() {
       patterns = new HashMap<>();
       patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
       patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
       patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
       patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
   }
   public Map<String, String> getPatterns() {
       return patterns;
   }
   public void setPatterns(Map<String, String> patterns) {
       this.patterns = patterns;
   }
}
Tutaj mamy mały obiekt zawierający mapę z nazwami wzorców jako kluczem i linkami do nich jako wartością, a także metodami dostępu do mapy. Spójrzmy main:
class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

       Resource firstResource = pool.acquireResource();
       Map<String, String> firstPatterns = firstResource.getPatterns();
       // ......Jakим-то образом используем нашу мапу.....
       pool.releaseResource(firstResource);

       Resource secondResource = pool.acquireResource();
       Map<String, String> secondPatterns = firstResource.getPatterns();
       // ......Jakим-то образом используем нашу мапу.....
       pool.releaseResource(secondResource);

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // ......Jakим-то образом используем нашу мапу.....
       pool.releaseResource(thirdResource);
   }
}
Tutaj też wszystko jest jasne: bierzemy obiekt z basenu, wyciągamy z niego obiekt z zasobami, bierzemy z niego mapę, coś z nim robimy i wkładamy wszystko z powrotem do puli do dalszego wykorzystania. Voila: tutaj masz wzór puli obiektów. Ale mówiliśmy o antywzorcach, prawda? Spójrzmy na ten przypadek main:
Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// ......Jakим-то образом используем нашу мапу.....
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
Tutaj ponownie pobierany jest obiekt zasobu, pobierana jest jego mapa ze wzorami i coś się z nim robi, ale przed ponownym zapisaniem do puli obiektów mapa jest czyszczona i wypełniana niezrozumiałymi danymi, co sprawia, że ​​ten obiekt zasobu nie nadaje się do ponownego użycia. Jednym z głównych niuansów puli obiektów jest to, że po zwróceniu obiektu należy go przywrócić do stanu odpowiedniego do dalszego użycia. Jeśli obiekty po zwróceniu do puli znajdują się w nieprawidłowym lub niezdefiniowanym stanie, konstrukcja ta nazywana jest szambo obiektowym. Czy ma sens przechowywanie przez nas przedmiotów, które nie nadają się do ponownego użycia? W tej sytuacji możesz sprawić, że mapa wewnętrzna będzie niezmienna w konstruktorze:
public Resource() {
   patterns = new HashMap<>();
   patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
   patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
   patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
   patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
   patterns = Collections.unmodifiableMap(patterns);
}
(próby i chęć zmiany treści będą zgłaszane wraz z wyjątkiem UnsupportedOperationException). Antywzorce to pułapki, w które często wpadają programiści z powodu dotkliwego braku czasu, nieuwagi, braku doświadczenia lub kopnięć ze strony menedżerów. Zwykły brak czasu i pośpiech mogą w przyszłości skutkować dużymi problemami z aplikacją, dlatego warto wiedzieć o tych błędach i unikać ich z wyprzedzeniem. Czym są antywzorce?  Spójrzmy na przykłady (część 1) - 6Na tym zakończyła się pierwsza część artykułu: ciąg dalszy .
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION