clean code

Stop using setters

Setters, also known as mutator methods, are a common feature in object-oriented programming languages that allow for the modification of an object’s state. While setters can be useful in certain contexts, they can also introduce some problems and challenges that can lead to bugs, performance issues, and other difficulties. In this article, we will explore some of the key drawbacks of using setters in your code and provide some tips and strategies for mitigating these risks. Whether you are a seasoned software engineer or just getting started in the field, understanding the limitations of setters can help you write more robust, efficient, and maintainable code.

Let’s talk about the problems:

Using setters instead of constructors

Before we start please look at this piece of code:

public void doSomething() {
    Client client = new Client();
    
    client.makeCall();
}

Would you be able to confidently determine whether the client is prepared to invoke the makeCall() method by examining the code in your IDE? Chances are, you might be unsure, as the implementation of the Client class may not be readily apparent. This is a valid observation. However, why should you need to familiarize yourself with the inner workings of the Client class? Shouldn’t the class itself provide clear instructions on what actions to take?

After opening the Client class and learning how it works, your code might look like this:

public void doSomething() {
    Client client = new Client();
    client.setHttpClient(new HttpClient());
    client.setEventManager(new EventManager());

    client.makeCall();
}

Whenever I encounter this issue, I often wonder why the class in question lacks a straightforward constructor that provides unambiguous guidance on how to correctly instantiate the object. After all, the primary role of a constructor is to create an object that is immediately usable. Rather than being able to do this, I am often required to devote time to studying classes like Client. When I ask developers about this, one of the most common answers is that they are just used to using setters.

Now let’s take a closer look at the problem.

One of the primary challenges of relying on setters is that it can be difficult to determine the correct approach to initializing an object. Only the individual who created the class possesses the necessary knowledge to achieve this task. If you do not have direct access to this person, then the only recourse available to you is to familiarize yourself with the intricacies of the class itself.

Now consider a scenario in which you need to work with five such classes. In all likelihood, you will find the process of learning and implementing these classes to be a source of significant frustration and anxiety, ultimately leading to demotivation.

Constructors serve as a useful solution to these issues. Upon instantiation of the class, your IDE may provide guidance on which dependencies should be passed to the constructor to properly initialize the object. By leveraging constructors in this manner, it becomes unnecessary to study the class in-depth, thus instilling a sense of confidence that the object has been instantiated correctly, as the initialization process becomes more transparent and explicit.

Look at the example of how it is easy from the client’s perspective to use a constructor.

public void doSomething() {
    Client client = new Client(
        new HttpClient(),
        new EventManager()
    );
    
    client.makeCall();
}

Using setters to change the state of an object

The second problem is when developers use setters to change the state of an object. Especially when they use @Setters annotation.

Employing setters in this manner represents a violation of the fundamental encapsulation principle, which dictates that an object should conceal its inner workings and solely expose its behavior.

When working with an object that relies heavily on setters, it becomes challenging to place trust in it, as its state may be altered by external classes at any point. This lack of certainty regarding the original state of the object can make it difficult to rely on it and can lead to issues with data consistency and integrity.

Before implementing a setter or adding the @Setters annotation, it is worthwhile to ask yourself why such changes are necessary. It may be the case that there is a deeper issue with the overall design of your component that needs to be addressed first. In some cases, the presence of setters may actually be an indicator of a poorly conceived design.

If there is a legitimate need to alter the state of the object, it is important to do so deliberately and with careful consideration. Rather than simply creating a setter, it may be more appropriate to develop a method that accurately conveys the nature of the changes being made to the object. By adopting this approach, you can maintain greater control over the object’s state and ensure that it remains stable and reliable over time.

user.setStatus(1);
// vs
user.activate();

The example presented here serves to illustrate my point. As compared to the setter, the second method is more descriptive in nature and does not represent a breach of the fundamental encapsulation principle.

Conclusion

In conclusion, the overreliance on setters in coding can present several issues that can ultimately harm the functionality and reliability of the software. Relying solely on setters can make it difficult to ascertain the correct approach to initializing an object, and it can also lead to data inconsistency and integrity issues. Moreover, setters can represent a violation of the encapsulation principle, causing trust issues with the object’s state. In instances where the alteration of an object’s state is necessary, it is recommended to do so through carefully designed methods, which maintain greater control over the object’s state and are less likely to result in issues over time. Therefore, as software engineers, it is imperative to be mindful of the potential drawbacks associated with setters and to take a more nuanced approach to object initialization and state alteration to avoid these potential pitfalls.