CodeGym /Blog Java /Random-PL /Czym są antywzorce? Spójrzmy na kilka przykładów (część 1...
John Squirrels
Poziom 41
San Francisco

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

Opublikowano w grupie Random-PL
Dzień dobry wszystkim! Któregoś dnia miałem rozmowę o pracę i zadano mi kilka pytań na temat antywzorców: czym są, jakie są rodzaje i jakie są praktyczne przykłady. Oczywiście odpowiedziałem na pytanie, ale bardzo powierzchownie, ponieważ wcześniej nie zagłębiałem się w ten temat. Po wywiadzie zacząłem przeszukiwać internet i coraz bardziej zagłębiałem się w temat. Czym są antywzorce?  Spójrzmy na kilka przykładów (część 1) - 1 Dzisiaj chciałbym przedstawić krótki przegląd najpopularniejszych antywzorców i przejrzeć kilka przykładów. Mam nadzieję, że lektura tego artykułu dostarczy Ci niezbędnej wiedzy w tej dziedzinie. Zacznijmy! Zanim omówimy, czym jest antywzorzec, przypomnijmy sobie, czym jest wzorzec projektowy. Wzór projektowyto powtarzalne rozwiązanie architektoniczne typowych problemów lub sytuacji, które pojawiają się podczas projektowania aplikacji. Ale dzisiaj nie mówimy o nich, ale raczej o ich przeciwieństwach — antywzorcach. Antywzorzec to szeroko rozpowszechnione, ale nieskuteczne, ryzykowne i / lub nieproduktywne podejście do rozwiązywania typowych problemów. Innymi słowy, jest to wzorzec błędów (czasami nazywany również pułapką). Z reguły antywzorce dzielą się na następujące typy:
  1. Architektoniczne antywzorce — te antywzorce powstają podczas projektowania struktury systemu (zwykle przez architekta).
  2. Antywzorce zarządcze/organizacyjne — Są to antywzorce w zarządzaniu projektami, z którymi zwykle spotykają się różni menedżerowie (lub grupy menedżerów).
  3. Antywzorce programistyczne — te antywzorce powstają, gdy system jest wdrażany przez zwykłych programistów.
Pełna gama antywzorców jest znacznie bardziej egzotyczna, ale nie będziemy ich wszystkich dzisiaj rozważać. Dla zwykłych programistów byłoby to za dużo. Na początek rozważmy jako przykład antywzorzec zarządzania.

1. Paraliż analityczny

Paraliż analitycznyjest uważany za klasyczny antywzorzec zarządzania. Polega na nadmiernym analizowaniu sytuacji podczas planowania, tak aby nie została podjęta żadna decyzja ani działanie, co zasadniczo paraliżuje proces rozwoju. Często dzieje się tak, gdy celem jest osiągnięcie perfekcji i uwzględnienie absolutnie wszystkiego w okresie analizy. Ten antywzorzec charakteryzuje się chodzeniem w kółko (zamknięta pętla), przeglądaniem i tworzeniem szczegółowych modeli, co z kolei zakłóca przepływ pracy. Na przykład próbujesz przewidzieć rzeczy na poziomie: ale co, jeśli użytkownik nagle chce utworzyć listę pracowników na podstawie czwartej i piątej litery ich nazwiska, w tym listę projektów, nad którymi spędził najwięcej godzin pracy między Nowym Rokiem a Międzynarodowym Dniem Kobiet w ciągu ostatnich czterech lat? W istocie to' za dużo analiz. Oto kilka wskazówek, jak walczyć z paraliżem analitycznym:
  1. Musisz zdefiniować cel długoterminowy jako drogowskaz do podejmowania decyzji, tak aby każda z twoich decyzji przybliżała cię do celu, a nie powodowała stagnację.
  2. Nie skupiaj się na drobiazgach (po co podejmować decyzję o nieistotnym szczególe, jakby to była najważniejsza decyzja w Twoim życiu?)
  3. Ustal termin decyzji.
  4. Nie próbuj wykonać zadania perfekcyjnie — lepiej zrobić to bardzo dobrze.
Nie ma potrzeby wchodzić w to zbyt głęboko, więc nie będziemy rozważać innych antywzorców menedżerskich. Dlatego bez żadnego wstępu przejdziemy do niektórych antywzorców architektonicznych, ponieważ ten artykuł najprawdopodobniej przeczytają raczej przyszli programiści niż menedżerowie.

2. Obiekt Boga

Obiekt Boga to antywzorzec, który opisuje nadmierną koncentrację wszelkiego rodzaju funkcji i dużą ilość odmiennych danych (obiekt, wokół którego obraca się aplikacja). Weź 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();
   }
}
Tutaj widzimy ogromną klasę, która robi wszystko. Zawiera zapytania do bazy danych, a także niektóre dane. Widzimy również metodę fasady findAllWithoutPageEn, która obejmuje logikę biznesową. Taki obiekt Boga staje się ogromny i niewygodny w utrzymaniu. Musimy się z tym bawić w każdym fragmencie kodu. Wiele elementów systemu opiera się na nim i jest z nim ściśle powiązanych. Utrzymanie takiego kodu staje się coraz trudniejsze. W takich przypadkach kod powinien być podzielony na osobne klasy, z których każda będzie miała tylko jeden cel. W tym przykładzie możemy podzielić obiekt Boga 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 danych:

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;
   }
                    ....
I bardziej odpowiednie byłoby przeniesienie metody z logiką biznesową do usługi:

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 jest najprostszym wzorcem. Zapewnia, że ​​w aplikacji jednowątkowej będzie tylko jedna instancja klasy i zapewnia globalny punkt dostępu do tego obiektu. Ale czy jest to wzorzec czy anty-wzorzec? Spójrzmy na wady tego wzorca:
  1. Stan globalny Gdy uzyskujemy dostęp do instancji klasy, nie znamy aktualnego stanu tej klasy. Nie wiemy, kto i kiedy to zmienił. Stan może nie być taki, jakiego oczekujemy. Innymi słowy, poprawność pracy z singletonem zależy od kolejności dostępów do niego. Oznacza to, że podsystemy są od siebie zależne, w wyniku czego projekt staje się znacznie bardziej złożony.

  2. Singleton narusza zasady SOLID — zasadę pojedynczej odpowiedzialności: oprócz swoich bezpośrednich obowiązków, klasa singleton kontroluje również liczbę instancji.

  3. Zależność zwykłej klasy od singletonu nie jest widoczna w interfejsie klasy. Ponieważ pojedyncza instancja zwykle nie jest przekazywana jako argument metody, ale jest uzyskiwana bezpośrednio przez getInstance(), musisz przejść do implementacji każdej metody, aby zidentyfikować zależność klasy od singletonu — wystarczy spojrzeć na publiczną klasę umowa nie wystarczy.

    Obecność singletonu zmniejsza testowalność aplikacji jako całości, aw szczególności klas korzystających z singletonu. Po pierwsze, nie można zastąpić singletonu próbnym obiektem. Po drugie, jeśli singleton ma interfejs do zmiany swojego stanu, testy będą od siebie zależne.

    Innymi słowy, singleton zwiększa sprzężenie, a wszystko, o czym wspomniano powyżej, jest niczym więcej niż konsekwencją zwiększonego sprzężenia.

    A jeśli się nad tym zastanowisz, możesz uniknąć używania singletona. Na przykład całkiem możliwe (i rzeczywiście konieczne) jest użycie różnego rodzaju fabryk do kontrolowania liczby wystąpień obiektu.

    Największym niebezpieczeństwem jest próba zbudowania całej architektury aplikacji w oparciu o singletony. Istnieje mnóstwo wspaniałych alternatyw dla tego podejścia. Najważniejszym przykładem jest Spring, a dokładniej jego kontenery IoC: są naturalnym rozwiązaniem problemu kontrolowania tworzenia usług, ponieważ są tak naprawdę „fabrykami na sterydach”.

    Na ten temat toczy się obecnie wiele niekończących się i nieprzejednanych debat. To Ty decydujesz, czy singleton jest wzorcem, czy antywzorcem.

    Nie będziemy na tym poprzestawać. Zamiast tego przejdziemy do ostatniego wzorca projektowego na dziś — poltergeista.

4. Poltergeista

Poltergeist to antywzorzec obejmujący bezsensowną klasę, który służy do wywoływania metod innej klasy lub po prostu dodaje zbędną warstwę abstrakcji . Ten antywzorzec przejawia się jako obiekty krótkotrwałe, pozbawione stanu. Obiekty te są często używane do inicjowania innych, bardziej trwałych 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 tylko pośrednikiem i deleguje swoją pracę komuś innemu? Eliminujemy go i przenosimy niewielką funkcjonalność, jaką posiadał, na obiekty długowieczne. Następnie przechodzimy do wzorców, które najbardziej nas interesują (jako zwykłych programistów), czyli antywzorców programistycznych .

5. Twarde kodowanie

Dotarliśmy więc do tego okropnego słowa: twarde kodowanie. Istotą tego antywzorca jest to, że kod jest silnie powiązany z określoną konfiguracją sprzętową i/lub środowiskiem systemowym. To znacznie komplikuje przenoszenie kodu do innych konfiguracji. Ten antywzorzec jest ściśle powiązany z liczbami magicznymi (te antywzorce często się przeplatają). 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;
}
Boli, prawda? Tutaj na stałe kodujemy nasze ustawienia połączenia. W rezultacie kod będzie działał poprawnie tylko z MySQL. Aby zmienić bazę danych, będziemy musieli zagłębić się w kod 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 użycie stałych.

6. Kotwica łodzi

W kontekście antywzorców kotwica łodzi oznacza zachowanie części systemu, które nie są już używane po przeprowadzeniu optymalizacji lub refaktoryzacji. Ponadto niektóre części kodu można zachować „do wykorzystania w przyszłości” na wypadek, gdybyś nagle ich potrzebował. Zasadniczo zamienia to twój kod w śmietnik. Przykł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 oddzielną metodę do scalania danych użytkownika z bazy danych z danymi użytkownika przekazanymi do metody (jeśli użytkownik przekazany do metody aktualizacji ma pole null, to stara wartość pola jest pobierana z bazy danych) . Załóżmy więc, że istnieje nowe wymaganie, że rekordy nie mogą być łączone ze starymi, ale zamiast tego, nawet jeśli istnieją pola zerowe, są one używane do nadpisywania starych:

public User update(Long id, User request) {
   return userDAO.update(user);
}
Oznacza to, że mergeUser nie jest już używany, ale szkoda byłoby go usunąć — a co, jeśli ta metoda (lub idea tej metody) może się kiedyś przydać? Taki kod jedynie komplikuje systemy i wprowadza zamieszanie, nie mając zasadniczo żadnej wartości praktycznej. Nie wolno nam zapominać, że taki kod z „martwymi fragmentami” będzie trudny do przekazania koledze, gdy wyjeżdżasz do innego projektu. Najlepszym sposobem radzenia sobie z kotwicami łodzi jest refaktoryzacja kodu, tj. usunięcie sekcji kodu (wiem, że to bolesne). Dodatkowo, przygotowując harmonogram zabudowy, należy uwzględnić takie kotwice (aby wygospodarować czas na uporządkowanie).

7. Szambo obiektowe

Aby opisać ten antywzorzec, najpierw musisz zapoznać się ze wzorcem puli obiektów . Pula obiektów (pula zasobów) to kreacyjny wzorzec projektowy , zbiór zainicjowanych i gotowych do użycia obiektów. Gdy aplikacja potrzebuje obiektu, jest on pobierany z tej puli, a nie odtwarzany. Kiedy przedmiot nie jest już potrzebny, nie ulega zniszczeniu. Zamiast tego wraca do puli. Ten wzorzec jest zwykle używany w przypadku ciężkich obiektów, których tworzenie jest czasochłonne za każdym razem, gdy są potrzebne, na przykład podczas łączenia się z bazą danych. Spójrzmy na mały i prosty przykład. Oto klasa reprezentująca 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);
   }
}
Klasa ta jest przedstawiona w postaci powyższego singletonowego wzorca/antywzorca, czyli może istnieć tylko jeden obiekt tego typu. Wykorzystuje określone Resourceobiekty. Domyślnie konstruktor wypełnia pulę 4 instancjami. Gdy otrzymasz obiekt, jest on usuwany z puli (jeśli nie ma dostępnego obiektu, obiekt jest tworzony i natychmiast zwracany). I na koniec mamy sposób na odłożenie obiektu z powrotem. Obiekty zasobów wyglądają następująco:

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;
   }
}
Tutaj mamy mały obiekt zawierający mapę z nazwami wzorców projektowych jako kluczem i odpowiednimi linkami do Wikipedii jako wartością, a także metodami dostępu do mapy. Rzućmy okiem na główne:

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);
   }
}
Tutaj wszystko jest wystarczająco jasne: bierzemy obiekt puli, pobieramy obiekt z zasobami z puli, pobieramy mapę z obiektu Resource, coś z tym robimy i umieszczamy to wszystko na swoim miejscu w puli do dalszego wykorzystania. Voila, to jest wzorzec projektowy puli obiektów. Ale mówiliśmy o antywzorcach, prawda? Rozważmy następujący przypadek w metodzie main:

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);
Tutaj ponownie otrzymujemy obiekt Resource, otrzymujemy jego mapę wzorców i robimy coś z mapą. Jednak przed zapisaniem mapy z powrotem do puli obiektów jest ona czyszczona, a następnie wypełniana uszkodzonymi danymi, przez co obiekt Resource nie nadaje się do ponownego użycia. Jednym z głównych szczegółów puli obiektów jest to, że po zwróceniu obiektu musi on zostać przywrócony do stanu odpowiedniego do ponownego wykorzystania. Jeśli obiekty zwrócone do puli pozostają w nieprawidłowym lub niezdefiniowanym stanie, nasz projekt nazywa się szambo obiektowym. Czy przechowywanie przedmiotów, które nie nadają się do ponownego użycia ma sens? W tej sytuacji możemy uczynić mapę wewnętrzną niezmienną w konstruktorze:

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);
}
Próby i chęć zmiany zawartości mapy znikną dzięki wygenerowanemu przez nie wyjątkowi UnsupportedOperationException. Antywzorce to pułapki, na które programiści często napotykają z powodu dotkliwego braku czasu, nieostrożności, braku doświadczenia lub presji ze strony kierowników projektów. Pośpiech, który jest powszechny, może prowadzić do dużych problemów dla aplikacji w przyszłości, dlatego musisz wiedzieć o tych błędach i unikać ich z wyprzedzeniem. Na tym kończy się pierwsza część artykułu. Ciąg dalszy nastąpi...
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION