CodeGym/Java Blog/Design Patterns in Java/What are anti-patterns? Let's look at some examples (Part...
Konstantin
Level 36
Odesa

What are anti-patterns? Let's look at some examples (Part 1)

Published in the Design Patterns in Java group
members
Good day to all! The other day I had a job interview, and I was asked some questions about anti-patterns: what they are, what types there are, and what practical examples there are. Of course, I answered the question, but very superficially, since I had not previously dived deep into this topic. After the interview, I began to scour the Internet and immersed myself more and more in the topic. What are anti-patterns? Let's look at some examples (Part 1) - 1 Today I would like to provide a brief overview of the most popular anti-patterns and review some examples. I hope that reading this will give you the knowledge you need in this area. Let's get started! Before we discuss what an anti-pattern is, let's recall what a design pattern is. A design pattern is a repeatable architectural solution for common problems or situations that arise when designing an application. But today we're not talking about them, but rather their opposites — anti-patterns. An anti-pattern is a widespread but ineffective, risky, and/or unproductive approach to solving a class of common problems. In other words, this is a pattern of mistakes (also sometimes called a trap). As a rule, anti-patterns are divided into the following types:
  1. Architectural anti-patterns — These anti-patterns arise as the structure of a system is designed (generally by an architect).
  2. Management/organizational anti-patterns — These are anti-patterns in project management, usually encountered by various managers (or groups of managers).
  3. Development anti-patterns — These anti-patterns arise as a system is implemented by ordinary programmers.
The full range of anti-patterns is far more exotic, but we won't consider them all today. For ordinary developers, that would be too much. For starters, let's consider a management anti-pattern as an example.

1. Analytical paralysis

Analysis paralysis is considered a classic management anti-pattern. It involves over-analyzing the situation during planning, so that no decision or action is taken, essentially paralyzing the development process. This often happens when the goal is to achieve perfection and consider absolutely everything during the analysis period. This anti-pattern is characterized by walking in circles (a run-of-the-mill closed loop), revising and creating detailed models, which in turn interferes with the workflow. For example, you are trying to predict things at a level: but what if a user suddenly wants to create a list of employees based on the fourth and fifth letters of their name, including the list of projects on which they spent the most working hours between New Year's and International Women's Day over the past four years? In essence, it's too much analysis. Here are a few tips for fighting analysis paralysis:
  1. You need to define a long-term goal as a beacon for decision-making, so that each of your decisions moves you closer to the goal rather than causing you to stagnate.
  2. Do not concentrate on trifles (why make a decision about an insignificant detail as if it were the most important decision of your life?)
  3. Set a deadline for a decision.
  4. Don't try to complete a task perfectly — it is better to do it very well.
No need to go too deeply here, so we won't consider other managerial anti-patterns. Therefore, without any introduction, we'll move on to some architectural anti-patterns, because this article is most likely to be read by future developers rather than managers.

2. God object

A God object is an anti-pattern that describes an excessive concentration of all sorts of functions and large amounts of disparate data (an object that the application revolves around). Take a small example:
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();
   }
}
Here we see a huge class that does everything. It contains database queries as well as some data. We also see the findAllWithoutPageEn facade method, which includes business logic. Such a God object becomes enormous and awkward to properly maintain. We have to mess around with it in every piece of code. Many system components rely on it and are tightly coupled with it. It becomes harder and harder to maintain such code. In such cases, the code should be split into separate classes, each of which will have only one purpose. In this example, we can split the God object into a Dao class:
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());
   }

                               ........
}
A class containing data and methods for accessing the data:
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;
   }
                    ....
And it would be more appropriate to move the method with business logic to a service:
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

A singleton is the simplest pattern. It ensures that in a single-threaded application there will be a single instance of a class, and it provides a global access point to this object. But is it a pattern or an anti-pattern? Let's look at the disadvantages of this pattern:
  1. Global state When we access the instance of the class, we do not know the current state of this class. We don't know who has changed it or when. The state may not be anything like what we expect. In other words, the correctness of working with a singleton depends on the order of accesses to it. This means that subsystems are dependent on each other and, as a result, a design becomes seriously more complex.

  2. A singleton violates the SOLID principles — the single responsibility principle: in addition to its direct duties, the singleton class also controls the number of instances.

  3. An ordinary class's dependence on a singleton is not visible in the class's interface. Because a singleton instance is not usually passed as a method argument, but instead is obtained directly through getInstance(), you need to get into the implementation of each method in order to identify the class's dependence on the singleton — just looking at a class's public contract is not enough.

    The presence of a singleton reduces the testability of the application as a whole and the classes that use the singleton in particular. First of all, you cannot replace the singleton with a mock object. Second, if a singleton has an interface for changing its state, then the tests will depend on one another.

    In other words, a singleton increases coupling, and everything mentioned above is nothing more than a consequence of increased coupling.

    And if you think about it, you can avoid using a singleton. For example, it is quite possible (and indeed necessary) to use various kinds of factories to control the number of instances of an object.

    The greatest danger lies in an attempt to build an entire application architecture based on singletons. There are tons of wonderful alternatives to this approach. The most important example is Spring, namely its IoC containers: they are a natural solution to the problem of controlling the creation of services, since they are actually "factories on steroids".

    Many interminable and irreconcilable debates are now raging on this subject. It's up to you to decide whether a singleton is a pattern or anti-pattern.

    We won't linger on it. Instead, we'll move on to the last design pattern for today — poltergeist.

4. Poltergeist

A poltergeist is an anti-pattern involving a pointless class that is used to call methods of another class or simply adds an unnecessary layer of abstraction. This anti-pattern manifests itself as short-lived objects, devoid of state. These objects are often used to initialize other, more permanent objects.
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);
   }
}
Why do we need an object that is just an intermediary and delegates its work to someone else? We eliminate it and transfer the little functionality it had to long-lived objects. Next, we move on to the patterns that are of most interest to us (as ordinary developers), i.e. development anti-patterns.

5. Hard coding

So we've arrived at this terrible word: hard coding. The essence of this anti-pattern is that the code is strongly tied to a specific hardware configuration and/or system environment. This greatly complicates porting the code to other configurations. This anti-pattern is closely associated with magic numbers (these anti-patterns are often intertwined). Example:
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;
}
Hurts, doesn't it? Here we hard code our connection settings. As a result, the code will work correctly only with MySQL. To change the database, we will need to dive into the code and change everything manually. A good solution would be to put the configuration in a separate file:
spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Another option is to use constants.

6. Boat anchor

In the context of anti-patterns, a boat anchor means keeping parts of the system that are no longer used after performing some optimization or refactoring. Also, some parts of code could be kept "for future use" just in case you suddenly need them. Essentially, this turns your code into a dustbin. Example:
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());
}
We have an update method that uses a separate method to merge user data from the database with the user data passed to the method (if the user passed to the update method has a null field, then the old field value is taken from the database). Then suppose there is a new requirement that the records must not be merged with the old ones, but instead, even if there are null fields, they are used to overwrite the old ones:
public User update(Long id, User request) {
   return userDAO.update(user);
}
This means that mergeUser is no longer used, but it would be a pity to delete it — what if this method (or the idea of this method) might come in handy someday? Such code only complicates systems and introduces confusion, having essentially no practical value. We must not forget that such code with "dead pieces" will be difficult to pass on to a colleague when you leave for another project. The best way to deal with boat anchors is to refactor the code, i.e. delete sections of code (heartbreaking, I know). Additionally, when preparing the development schedule, it is necessary to account for such anchors (to allocate time for tidying up).

7. Object cesspool

To describe this anti-pattern, first you need to get acquainted with the object pool pattern. An object pool (resource pool) is a creational design pattern, a set of initialized and ready-to-use objects. When an application needs an object, it is taken from this pool rather than being recreated. When an object is no longer needed, it is not destroyed. Instead, it is returned to the pool. This pattern is usually used for heavy objects that are time-consuming to create every time they are needed, such as when connecting to a database. Let's look at a small and simple example. Here is a class that represents this pattern:
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);
   }
}
This class is presented in the form of the above singleton pattern/anti-pattern, i.e. there can be only one object of this type. It uses certain Resource objects. By default, the constructor fills the pool with 4 instances. When you get an object, it is removed from the pool (if there is no available object, one is created and immediately returned). And at the end, we have a method to put the object back. Resource objects look like this:
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;
   }
}
Here we have a small object containing a map with design pattern names as the key and corresponding Wikipedia links as the value, as well as methods to access the map. Let's take a look at main:
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);
   }
}
Everything here is clear enough: we get a pool object, get an object with resources from the pool, get the map from Resource object, do something with it, and put all this in its place in the pool for further reuse. Voila, this is the object pool design pattern. But we were talking about anti-patterns, right? Let's consider the following case in the main method:
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);
Here, again, we get a Resource object, we get its map of patterns, and we do something with the map. But before saving the map back to the pool of objects, it is cleared and then populated with corrupted data, making the Resource object unsuitable for reuse. One of the main details of an object pool is that when an object is returned, it must restore to a state suitable for further reuse. If objects returned to the pool remain in an incorrect or undefined state, then our design is called an object cesspool. Does it make any sense to store objects that are not suitable for reuse? In this situation, we can make the internal map immutable in the constructor:
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);
}
Attempts and the desire to change the map's contents will fade away thanks to the UnsupportedOperationException they will generate. Anti-patterns are traps that developers encounter frequently due to an acute lack of time, carelessness, inexperience, or pressure from project managers. Rushing, which is common, can lead to big problems for the application in the future, so you need to know about these errors and avoid them in advance. This concludes the first part of the article. To be continued...
Comments
  • Popular
  • New
  • Old
You must be signed in to leave a comment
This page doesn't have any comments yet