- Architectural antipatterns — антипаттерны архитектуры, возникающие при проектировании структуры системы (как правило архитектором).
- Management Anti Pattern — антипаттерны в области управления, с которыми как правило сталкиваются разнообразные менеджеры (или группы менеджеров).
- Development Anti Pattern — антипаттерны проблемы разработки, возникающие при написании системы рядовыми программистами.
1. Analytical paralysis
Аналитический паралич — считается классическим организационным антипаттерном. Его суть заключается в чрезмерном анализировании ситуации при планировании, так что решение или действие не предпринимаются, по сути парализуя разработку. Зачастую это случается в тех случаях, когда цель состоит в достижении совершенства и полной завершенности периода анализа. Этот антипаттерн характеризуется хождением по кругу (такой себе замкнутый цикл), пересмотром и созданием детальных моделей, что в свою очередь мешает рабочему процессу. К примеру, вы пытаетесь предугадать вещи уровня: а что если вдруг пользователь захочет создать список сотрудников на основе четвертых и пятых букв их имени, с включением в список проектов, которым они уделили больше всего рабочих часов между Новым Годом и Восьмым марта за четыре предыдущих года? По сути это переизбыток анализа. Хороший пример из жизни — как аналитический паралич привел Kodak к банкротству. Вот парочка небольших советов для борьбы аналитическим параличом:- Нужно определить долгосрочную цель в качестве маяка для принятия решений, чтобы каждое ваше решение приближало к цели, а не заставляло топтаться на месте.
- Не концентрироваться на пустяках (зачем принимать решение по незначительному нюансу так, словно оно последнее в жизни?)
- Задайте крайний срок для принятия решения.
- Не старайтесь сделать задачу совершенно: лучше сделать очень хорошо.
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
Одиночка — это самый простой паттерн, гарантирующий, что в однопоточном приложении будет единственный экземпляр некоторого класса, и предоставляющий глобальную точку доступа к этому объекту. Подробнее о нём можно почитать вот тут. Но паттерн ли это или антипаттерн? Давайте рассмотрим недостатки данного шаблона:Глобальное состояние. Когда мы получаем доступ к экземпляру класса, мы не знаем текущее состояние этого класса, и кто и когда его менял, и это состояние может быть вовсе не таким, как ожидается. Иными словами, корректность работы с синглтоном зависит от порядка обращений к нему, что вызывает зависимость подсистем друг от друга и, как следствие, серьезно повышает сложность разработки.
Синглтон нарушает один из принципов SOLID — Single Responsibility Principle — класс синглтона, помимо выполнения своих непосредственных обязанностей, занимается еще и контролированием количества своих экземпляров.
Зависимость обычного класса от синглтона не видна в интерфейсе класса. Так как обычно экземпляр синглтона не передается в параметрах метода, а получается напрямую, через
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
Лодочный якорь в контексте антипаттернов означает хранение неиспользуемых частей системы, которые остались после какой-то оптимизации или рефакторинга. Также некоторые части кода могли быть оставлены «на будущее», вдруг придётся ещё их использовать. По сути это делает из кода мусорное ведро. Пример:
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 непригодным для переиспользования.
Один из главных нюансов пула объектов — после того, как объект возвращен, он должен вернуться в состояние, пригодное для дальнейшего переиспользования. Если объекты после возвращения в пул оказываются в неправильном или неопределенном состоянии, такая конструкция и называется объектной клоакой.
Смысл нам хранить объекты, непригодные для переиспользования?
В данной ситуации можно сделать в конструкторе внутреннюю мапу неизменяемой:
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).
Антипаттерны — ловушки, в которые разработчик часто встревает из-за острой нехватки времени, невнимательности, неопытности или пинков со стороны менеджеров. Обычная нехватка времени и спешка может вылиться в большие проблемы для приложения в будущем, поэтому эти ошибки нужно нужно знать и заранее их избегать.
На этом, первая часть статьи подошла концу: продолжение следует.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ