JavaRush /Java блог /Random UA /Що таке антипатерни? Розбираємо приклади (частина 1)
Константин
36 рівень

Що таке антипатерни? Розбираємо приклади (частина 1)

Стаття з групи Random UA
Що таке антипатерни?  Розбираємо приклади (частина 1) - 1Всім доброго доби! Днями я проходив співбесіду, і мені поставабо питання про антипаттерни: що це за звір такий, які бувають їхні види та приклади на практиці. На запитання я, звичайно, відповів, але дуже поверхово, тому що не сильно заглиблювався у вивчення цього питання. Після співбесіди я почав нишпорити по просторах інтернету, дедалі більше занурюючись у цю тему. Сьогодні я хотів би зробити невеликий огляд найпопулярніших антипаттернів та їх прикладів, прочитання якого, можливо, дасть вам необхідні знання у цьому питанні. Почнемо! Отже, як міркувати, що таке антипаттерн, давайте згадаємо, що таке патерн. Паттерн- Це повторна архітектурна конструкція для вирішення часто зустрічаються проблем або ситуацій, що виникають при проектуванні програми. Але сьогодні у нас йдеться не про них, а про їхні протилежності — антипатерни. Антипаттерн - це поширений підхід до вирішення класу проблем, що часто зустрічаються, який є неефективним, ризикованим або непродуктивним. Інакше висловлюючись, це патерн помилок (також іноді званий пасткою). Що таке антипатерни?  Розбираємо приклади (частина 1) - 2Як правило, антипатерни ділять на такі види:
  1. Architectural antipatterns - антипатерни архітектури, що виникають при проектуванні структури системи (як правило архітектором).
  2. Management Anti Pattern - антипатерни в галузі управління, з якими зазвичай стикаються різноманітні менеджери (або групи менеджерів).
  3. Development Anti Pattern - антипатерні проблеми розробки, що виникають під час написання системи рядовими програмістами.
Екзотика антипатернів набагато ширша, але ми їх розглядати сьогодні не будемо, тому що для звичайних розробників цього буде з головою. Спочатку як приклад розглянемо антипаттерн у сфері управління.

1. Analytical paralysis

Аналітичний параліч- Вважається класичним організаційним антипаттерном. Його суть полягає в надмірному аналізуванні ситуації при плануванні, так що рішення чи дія не робляться, по суті, паралізуючи розробку. Найчастіше це трапляється у тих випадках, коли мета полягає у досягненні досконалості та повної завершеності періоду аналізу. Цей антипаттерн характеризується ходінням по колу (такий собі замкнутий цикл), переглядом та створенням детальних моделей, що у свою чергу заважає робочому процесу. Наприклад, ви намагаєтеся передбачити речі рівня: а якщо раптом користувач захоче створити список співробітників на основі четвертих і п'ятих літер їх імені, з включенням до списку проектів, яким вони приділабо найбільше робочих годин між Новим Роком і Восьмим березня за чотири попередні роки ? Насправді це надлишок аналізу.привів Kodak до банкрутства . Ось пара невеликих порад для боротьби аналітичним паралічем:
  1. Потрібно визначити довгострокову мету як маяк для прийняття рішень, щоб кожне ваше рішення наближало до мети, а не змушувало тупцювати на місці.
  2. Не концентруватися на дрібницях (навіщо приймати рішення щодо незначного нюансу так, ніби воно останнє у житті?)
  3. Вкажіть крайній термін для прийняття рішення.
  4. Не намагайтеся зробити завдання досконало: краще зробити дуже добре.
Занадто поглиблюватись і розглядати інші управлінські антипатерни ми зараз не будемо. Тому без передмов переходимо до деяких архітектурних антипаттернів, адже, швидше за все, цю статтю читають майбутні розробники, а не менеджери.

2. God object

Божественний об'єкт - антипаттерн, який описує зайву концентрацію занадто великої кількості різношерстих функцій, зберігання великої кількості різноманітних даних (об'єкт, навколо якого обертається додаток). Візьмемо невеликий приклад:
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();
   }
}
Тут ми бачимо якийсь великий клас, який робить все й одразу. Містить запити до Бази даних, містить у собі якісь дані, також бачимо фасадний метод findAllWithoutPageEnіз бізнес-логікою. Такий божественний об'єкт стає величезним та неповоротким для адекватної підтримки. Нам доводиться возитися з ним у кожному шматочку коду: багато вузлів системи покладаються на нього і жорстко з ним пов'язані. Підтримувати такий код стає складніше і складніше. У разі його треба розрубати деякі класи, в кожного з яких буде лише одне призначення (мета). У цьому прикладі можна розбити на клас дао:
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());
   }

                               ........
}
Клас, що містить дані та методи доступу до них:
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;
   }
                    ....
І метод з бізнес-логікою буде доречніше винести на сервіс:
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

Одинак ​​- це найпростіший патерн, що гарантує, що в однопотоковому додатку буде єдиний екземпляр деякого класу, і надає глобальну точку доступу до цього об'єкта. Докладніше про нього можна почитати ось тут . Але чи це патерн чи антипаттерн? Що таке антипатерни?  Розбираємо приклади (частина 1) – 3Давайте розглянемо недоліки даного шаблону:
  1. Світовий стан. Коли ми отримуємо доступ до екземпляра класу, ми не знаємо поточний стан цього класу, і хто і коли його змінював, і цей стан може бути зовсім не таким, як очікується. Іншими словами, коректність роботи з синглтон залежить від порядку звернень до нього, що викликає залежність підсистем один від одного і, як наслідок, серйозно підвищує складність розробки.

  2. Сінглтон порушує один із принципів SOLID - Single Responsibility Principle - клас синглтона, крім виконання своїх безпосередніх обов'язків, займається ще й контролюванням кількості своїх екземплярів.

  3. Залежність звичайного класу синглтона не видно в інтерфейсі класу. Оскільки зазвичай екземпляр синглтона не передається в параметрах методу, а виходить безпосередньо, через getInstance(), виявлення залежності класу від синглтона треба залізти у кожен метод — просто переглянути громадський договір об'єкта недостатньо.

    Наявність синглтона знижує тестованість програми загалом і класів, які використовують синглтон, зокрема. По-перше, замість синглтона не можна підкласти Mock-об'єкт, а по-друге, якщо синглтон має інтерфейс для зміни свого стану, тести залежатимуть один від одного.

    Іншими словами, синглтон підвищує зв'язність, і все перелічене вище є ніщо інше як наслідок підвищення зв'язності.

    І якщо замислитись, використання синглтона можна уникнути. Наприклад, контролю кількості екземплярів об'єкта цілком можна (та й потрібно) використовувати різноманітних фабрики.

    Найбільша ж небезпека чатує при спробі побудувати на основі синглтонів всю архітектуру програми. Такому підходу існує безліч чудових альтернатив. Найголовніший приклад — це Spring, а саме його IoC контейнери: там проблема контролю створення сервісів вирішується природним чином, оскільки вони, за фактом, є "фабриками на стероїдах".

    Зараз існує багато холівара на цю тему, та й вирішувати, синглтон - це патерн або антипаттерн, вже вам.

    А ми на ньому не затримуватимемося і перейдемо до останнього на сьогодні патерну проектування — полтергейсту.

4. Poltergeist

Полтергейст - це антипаттерн класу, що не має користі, який використовується для виклику методів іншого класу або просто додає непотрібний шар абстракції. Антипаттерн проявляється як короткоживучих об'єктів, позбавлених стану. Ці об'єкти часто використовуються для ініціалізації інших більш стійких об'єктів.
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);
   }
}
Навіщо нам потрібен об'єкт, який лише посередник і делегує свою роботу комусь іншому? Видаляємо його, а той невеликий функціонал, який він реалізує, виносимо до об'єктів-довгожителів. Далі ми переходимо до патернів, які надають найбільший інтерес для нас (як рядових розробників) - до антипатернів розробки .

5. Hard code

От ми й дісталися цього страшного слова — хардкод. Суть даного антипаттерну в тому, що код сильно прив'язаний до конкретної апаратної конфігурації та/або системного оточення, що ускладнює перенесення його на інші конфігурації. Цей антипаттерн тісно пов'язаний із магічними числами (вони часто переплітаються). Приклад:
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;
}
Прибито цвяхами, чи не так? Тут ми безпосередньо задаємо конфігурацію нашого з'єднання, за підсумком код справно працюватиме тільки з MySQL, і для зміни бази даних потрібно буде залізти в код і ручками все міняти. Хорошим рішенням буде винести конфіги в окремий файл:
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Як варіант ще – винос у константи.

6. Boat anchor

Човновий якір у контексті антипатернів означає зберігання частин системи, що не використовуються, які залишабося після якоїсь оптимізації або рефакторингу. Також деякі частини коду могли бути залишені «на майбутнє», раптом доведеться їх використовувати. По суті це робить із коду відро для сміття. Що таке антипатерни?  Розбираємо приклади (частина 1) – 4Приклад:
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());
}
У нас є метод оновлення, який використовує окремий метод для злиття даних користувача з бази даних і прийшов на оновлення (якщо у порожнього поля, що прийшов на оновлення, то воно записується старим з бази даних). І, наприклад, з'явилася вимога, що записи не повинні об'єднуватися зі старими, а перезаписуватися поверх, навіть якщо є порожні поля:
public User update(Long id, User request) {
   return userDAO.update(user);
}
Як результат, mergeUserвже не використовується і шкода його видаляти: раптом він (або його ідея) ще знадобиться? Такий код лише ускладнює та плутає системи, по суті не несучи зовсім ніякої практичної цінності. Потрібно не забувати, що подібний код з «мертвими шматками» важко передаватиме колезі, коли ви підете на інший проект. Найкращим методом боротьби з човновими якорями є рефакторинг коду, саме видалення даних ділянок коду (на жаль, на жаль). Також при плануванні розробки необхідно враховувати поява подібних якорів (виділяти час на зачистку хвостів).

7.Object cesspool

Для опису даного антипаттерну спочатку потрібно познайомитися з патерном пул об'єктів . Пул об'єктів (пул ресурсів) — шаблон проектування, що породжує , набір ініціалізованих і готових до використання об'єктів. Коли додатку потрібен об'єкт, він створюється заново, а береться з цього пулу. Коли об'єкт не потрібен, він знищується, а повертається в пул. Зазвичай використовується для важких об'єктів, які ресурсозатратно щоразу створювати, наприклад з'єднання з базою даних. Давайте розберемо невеликий і простий приклад для прикладу. Отже, у нас є клас, який представляє цей патерн:
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);
   }
}
Даний клас у нас представлений у вигляді вищеописаного патерну/антипатерну синглтона , тобто може бути тільки один об'єкт даного типу, оперує деякими об'єктами Resource, за замовчуванням в конструкторі пул заповнюється чотирма примірниками; при взятті такого об'єкта він видаляється з пулу (якщо його немає, то створюється і відразу віддається), і наприкінці - метод, щоб покласти об'єкт назад. Об'єкти Resourceвиглядають так:
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;
   }
}
Тут у нас невеликий об'єкт, що містить карту з назвами патернів як ключ і посилання на них як значення, а також методів доступу до карти. Дивимося main:
class SomeMain {
   public static void main(String[] args) {
       ReusablePool pool = ReusablePool.getInstance();

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

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

       Resource thirdResource = pool.acquireResource();
       Map<String, String> thirdPatterns = firstResource.getPatterns();
       // ......яким-то образом используем нашу мапу.....
       pool.releaseResource(thirdResource);
   }
}
Тут все теж зрозуміло: ми беремо об'єкт пула, витягуємо з нього об'єкт з ресурсами, беремо з нього карту, щось із нею робимо і кладемо все це на місце в пул для подальшого перевикористання. Вуаля: ось вам і патерн пул об'єктів. Але ж ми говорабо про антипатерни, чи не так? Давайте розглянемо такий випадок у main:
Resource fourthResource = pool.acquireResource();
   Map<String, String> fourthPatterns = firstResource.getPatterns();
// ......яким-то образом используем нашу мапу.....
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
Тут знову ж таки береться об'єкт ресурсів, береться його map з патернами і щось з ним робиться, але перед збереженням назад в пул об'єктів карта чиститься і забивається незрозумілими даними, що роблять даний об'єкт Resource непридатним для перевикористання. Один із головних нюансів пулу об'єктів — після того, як об'єкт повернуто, він має повернутися до стану, придатного для подальшого перевикористання. Якщо об'єкти після повернення в пул опиняються в неправильному чи невизначеному стані, така конструкція називається об'єктною клоакою. Зміст нам зберігати об'єкти, непридатні для перевикористання? У цій ситуації можна зробити в конструкторі внутрішню карту незмінною:
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);
}
(Спроби і бажання змінити вміст впадуть разом з UnsupportedOperationException). Антипатерни - пастки, в які розробник часто встряє через гостру нестачу часу, неуважності, недосвідченості або стусанів з боку менеджерів. Звичайна нестача часу і поспіх може ваботися у великі проблеми для застосування в майбутньому, тому ці помилки потрібно знати і заздалегідь їх уникати. Що таке антипатерни?  Розбираємо приклади (частина 1) – 6На цьому, перша частина статті добігла кінця: продовження слідує .
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ