Что такое антипаттерны? Разбираем примеры (часть 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, по умолчанию в конструкторе пул заполняется 4-мя экземплярами; при взятии такого объекта он удаляется из пула (если его нет, то создается и сразу отдается), и в конце — метод, чтобы положить объект обратно. Объекты 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 непригодным для переиспользования. Один из главных нюансов пула объектов — после того, как объект возвращен, он должен вернуться в состояние, пригодное для дальнейшего переиспользования. Если объекты после возвращения в пул оказываются в неправильном или неопределенном состоянии, такая конструкция и называется объектной клоакой. Что такое антипаттерны? Разбираем примеры (часть 1) - 5Смысл нам хранить объекты, непригодные для переиспользования? В данной ситуации можно сделать в конструкторе внутреннюю мапу неизменяемой:
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На этом, первая часть статьи подошла концу: продолжение следует.