Setters and their effects

Ivan Yordanov
7 min readOct 21, 2024

--

Screenshot by author

Last time I discussed accessor methods, their effect, how we can hide representation and the benefits of doing it. I continue with posts related to hidden (or not so obvious) unwanted effects of common practices in software development and how to minimize their negative effect, in attempt to reduce accidental complexity in software development. This time I’ll focus on mutator methods. I’ll talk more about objects creation, dependency injection and immutable objects. Mutators just illustrate or symbolize few common violations of these ideas.

Mutator method also known as setter refers to a public method that modify value of private variable. Compared to direct mutation of variable setters have advantages like easier debugging (simply a place to put breakpoint), adding validation before changing the value, amending the value with internal logic, etc. As we saw earlier in getters article we can’t compare different units like methods and properties. Fair comparison requires same units.

We apply setters in two main cases — to finalize object creation and to update object’s state. Let’s focus on first case. When we initialize an object it often needs other objects in order to behave properly. We call such objects dependencies. We can provide such objects statically at compile time and dynamically at runtime. Statically means to hard-code necessary objects creation into main object’s constructor.

class Module1 {
public Module1() {
this.module2 = new Module2();
}
}

This technique has several problems. Module1’s constructor must accept all parameters that Module2 requires or artificially increase module1 arguments. When we change Module2 arguments we must change Module1 arguments at same time. We can not benefit of open closed principle as we have static relation between objects. We can not skip Module2 creation in case we have scenario that don’t require module2 (unless we add flag, thus artificially increase parameters). Testing such objects becomes harder, as we can not test Module1 in isolation.

We can eliminate all mentions issues by passing already created object as argument.

class Module1 {
public Module1(Module2 module2) {
this.module2 = module2;
}
}

In this approach Module1 don’t know anything about Module2 arguments. If we change Module2 we don’t need to update Module1 as well. We can pass different strategies and benefit from open-closed principle, and we can prepare special Module2 implementation for testing (mock). All issues solved at once. We call such approach dependency injection, as dependencies get injected instead of created by main object.

We have two ways to inject such objects. Via constructor and via mutator method. We should always use constructor injection. It has several advantages compared to mutator method. If we use mutator method for dependencies there exists some period when object remains incomplete or corrupted. Invoking a method during this period (let’s say from another thread) we’ll receive a runtime error. Such errors appear randomly and discovering them requires extra effort and debugging. Using constructor injection makes such situations impossible. We transform possible runtime error into compile error. Object’s initialization completes with constructor.

class Module1 {
public Module1() {}
public setModule2() {this.module2 = module2;}
public void process() {
module2.method();
}

public static void main(String[] args) {
Module1 module = new Module1();
module1.process(); //module2 not initialized yet.
}
}

Object initialization must complete with construct method. Otherwise we have window with corrupted objects. On the other hand constructors must not do any real work, only setting properties passed as arguments. When we need something done by constructor we better use creation design pattern like factory, builder, prototype, etc., delegate to them actual computing and return created object. In this way objects become more testable.

Sometimes we have two or more objects that depend on each other to function properly. We call such modules mutually recursive or circular dependencies. They increase coupling between modules and may cause domino effect when local change spreads and cause problems in other modules. Circular dependencies can also cause infinite recursion or memory leak in case our garbage collector rely on reference count to de-allocate unused objects.

Often, our object doesn’t need all the parameters all the time (in every single functionality). Some people recommend setter injection for optional dependencies, in order to reduce number of constructor parameters, but we can do better. We can remove all optional dependencies in our class and either wrap the object at runtime in another object that holds the optional dependency — like in decorator pattern or move optional parameter from constructor to function, only for methods that demand it. This suggests rule of thumb — if we don’t use a dependency in all object’s methods, either methods or the dependency itself doesn’t belong to this class.

//As Optional argument
class Module1 {
public Module1() {this.module2 = module2;}
public void process(Optional1 argument) { //pass optional dependency only for method that needs it.
module2.method(argument.process());
}

public static void main(String[] args) {
Module1 module = new Module1(new Module2());
module1.process(new Optional1()); //module2 not initialized yet.
}
}
//As Wrapper class
class Module1 {
public Module1() {this.module2 = module2;}
public void process(OptionalResult result) { //pass the result as argument.
module2.method(result);
}

public static void main(String[] args) {
Module1 module = new Module1(new Module2());
module1.process(new Optional1());
}
}

class Module1Wrapper {
public Module1Wrapper(Module1 module1, Optional1 optional1) {
this.module1 = module1;
this.optional1 = optional1;
}

public void process(){
module1.process(optinal1.process());
}
}

We initialize wrapper in scenarios where our logic uses optional dependency and the original module in other cases.

These two strategies block our original class from growing and becoming god object that knows too much and requires extra effort for both maintenance and extension. We can also reuse it more often as we shift coupling between modules in wrapper or parameter instead of our main class.

Passing optional dependency as argument to method gives more flexibility as we can invoke its functionality any time.

class Module1 {
public Module1(Module2 module2) {this.module2 = module2;}
public void process(Optional1 argument) { //pass optional dependency only for method that needs it.
//some code
argument.process();
//some more code
}

public static void main(String[] args) {
Module1 module = new Module1(new Module2());
module1.process(new Optional1()); //module2 not initialized yet.
}
}

Creating wrapper limits us only to executing it before or after the original method (in this case module1.process).

class Module1 {
public Module1() {this.module2 = module2;}
public void process() {
//some internal logic
}

public static void main(String[] args) {
Module1 module = new Module1(new Module2());
module1.process(new Optional1()); //module2 not initialized yet.
}
}

class Module1Wrapper {
public Module1Wrapper(Module1 module1, Optional1 optional) {
this.module1 = module1;
this.optional1 = optional1;
}

public void process(){
optinal1.process(module1.process());
}
}

Now our Module1 doesn’t know anything about Optional argument. The wrapper deals with it. I prefer the wrapper when possible as it limits encoded knowledge in our objects. The case when we have non-mandatory dependency in the middle of a block code signals for refactoring. We better consider splitting our original functionality if possible and stick with decorator technique. That makes our code more clear and in same time increases reusability.

Setters have another major use — updating instance’s state. Mutator doesn’t provide anything meaningful like computation result. In fact, they don’t compute anything at all. They just update a state for future use (usually through getter). Sometimes such updates may cause unexpected effects also known as bugs. Consider heavy date computation:

public Calendar exactDateToLandOnMars() {
//some heavy computation.
return theDate;
}

At some point we have to use this date more than once. To avoid heavy computation we cache it.

public Calendar exactDateToLandOnMars() {
if (!this.computedDate == null) {
//some heavy computation
this.computedDate = theDate;
}
return this.computedDate;
}

After we land on Mars we need to do something there, like plant some trees or bring some organisms to produce oxygen. We have to wait before measuring oxygen in the air to estimate how long before next step. We set measurement for two months after landing.

class Application {
public static void main(String[] args) {
Calendar date = exactDateToLandOnMars(); //we create second reference to our mutable instance.
date.add(Calendar.MONTH, 2);
app.setMeasurementDate(date);
}
}

Now we have two references (one in cache and one in our main method) that point to same date variable. As we changed calendar’s state, now exactDateToLandOnMars returns wrong date. We call this effect “action at a distance”. When we have many references to same object, and we amend the state in one reference, other reference holders don’t receive notification of this change. This effect has several aspects. It makes tests less relevant as they pass, but our program may still behave incorrectly. Testing all possible scenarios takes too much effort and becomes practically impossible. Storing objects in various collections (i.e. HashMap) may return null, even if the object exists in the collection. We may get wrong results from let’s say priority queue. We may read wrong state in case of multithreaded application. To address this issue we can do few things. First we can inform other references via observer pattern. This approach reuses the reference but increases complexity of the system. It needs additional place that keeps list of all references and possibly introduces cyclic relations. We can also copy object’s value into another object. In this way we don’t have to worry about state changes as our original reference stays encapsulated in one place. In our example above instead of returning cached date we can return new object. We could make our life simpler if we used immutable date implementation that doesn’t force us to bother with its implementation details. Some may have concerns about creating many new objects, but its not 95 anymore and we overestimate this effect. As oracle documentation says:

Programmers are often reluctant to employ immutable objects, because they worry about the cost of creating a new object as opposed to updating an object in place. The impact of object creation is often overestimated, and can be offset by some of the efficiencies associated with immutable objects. These include decreased overhead due to garbage collection, and the elimination of code needed to protect mutable objects from corruption.

We can still use setters in few scenarios — when we have single reference (and it will stay single forever), and when the cost of copy surpass the effort to protect mutation. Careful with first constraint. We don’t know the future (at time of this writing we still can’t travel in time ;) ) and today’s single reference may change tomorrow. We know few cases like builder or fluent interface where mutation fits. Applications of these patterns meet our criteria for single reference. They usually stay isolated in their own scope (usually single method) and once the method completes they get destroyed from garbage collector. Because of their isolation we also don’t have to protect them as long as they get destroyed once the method completes.

--

--