Getters and their influence

Ivan Yordanov
5 min readJun 25, 2024

--

Screenshot by the author

Last time I discussed null pointers, their effect and how to minimize their usage. I continue with posts related to common mistakes we make as developers and how to address them, in attempt to reduce accidental complexity in software. This time I’ll focus on accessor methods. I’ll talk more about information hiding and coupling. Accessors just illustrate or symbolize few common violations of these ideas.

Getter or accessor refers to a public method that expose private variable outside its original scope. Such pattern hides and protects variables from outside world and only its accessor can work with them directly. This enables flexibility on later stage like lazy loading, validations, transformations or sometimes switching internal representation. This positive features justify the popularity of this approach. It excels compared to direct access of variables, but we compare different units (functions vs. variables). For objective comparison we should compare getters with other functions and our metrics should include readability, information hiding, responsibilities, coupling, maintainability, reliability, etc. For objective criteria we should include not only the method itself, but also its influence to other methods. How higher level methods should interact with it in case of ambiguous behavior, parameters or result.

By definition getters don’t compute anything, they return variable known in advance. This makes them easy to read, most of the time we don’t even bother, but also forces higher level classes to deal with all the computation. This includes extra encoded knowledge of what various states of variable may represent like what null means in particular scenario and how to deal with it. By definition this breaks single responsibility. The higher level module deals with both lower level details and its own logic. Modules become more coupled as shared knowledge between them grows. High coupling means higher maintenance cost and higher chance for errors. On the other hand low coupling leads to reusable modules, faster response to change, reduced code duplication, reliable software.

Consider a counter object which responsibility limits to check if value surpass given threshold. Accessor implementation may look like code below:

public class Count {
private int value;
public Count(int initialValue) {
this.value = initialValue;
}
public int getValue() {
return value;
}
}

And higher level class that uses this counter:

public class Service {
private final Count limit = new Count(1000);
private Count counter;
public Service(Count initialCount) {
this.counter = initialCount;
}

public void process() {
if (counter.getValue() >= limit.getValue()) {
throw new RuntimeException("Limit exceeded");
}
System.out.println("Processing request...");
}
}

We all know change is the only constant and sooner or later we’ll have to make some changes in our original code. Suppose our services grows and we extend the limit. At some point our requirements don’t fit in integer range, and we must replace our original integer with BigInteger. We can convert BigInteger to integer with precision lost, but such modification won’t fit the requirements. We have to change our interface and return BigInteger instead of int. Furthermore, we also have to update every single class that uses our Count.

On the other hand we don’t really care about actual count, we only care if we surpass the limit. If instead of retrieving value we pass our limit as an argument to the Count and the counter compare values internally we change only Count class without modifying every single module that uses it.

public class Count {
private int value;
public Count(int initialValue) {
this.value = initialValue;
}
public boolean exceeds(Count threshold) {
return value >= threshold.value;
}
}

public class Service {
private final Count limit = new Count(1000);
private Count counter;
public Service(Count initialCount) {
this.counter = initialCount;
}
public void process() {
if (counter.exceeds(limit)) {
throw new RuntimeException("Limit exceeded");
}
System.out.println("Processing request...");
}
}

When we need to change it, we simply replace comparison withvalue.compareTo(threshold.value) > -1. Service class stays unchanged. Few design decisions makes this possible. First we design our functionality based on what we need, not how we represent objects. Or with other words we design against behavior, not against data. Second we compare same units. If we represent limit as something different from Count we have to update Service in order to handle new representation. As we represent limit as same type, we don’t need to adjust our higher level code. Both decisions contribute to information hiding and minimize future amount of changes, but any single one of them can’t completely hide the information by itself. By combining them we can achieve synergy (result greater than sum of its parts) and reduce maintenance cost. Below you can find the updated Count class. Note that Service class don’t require any changes.

public class Count {
private BigInteger value;
public Count(int initialValue) {
this(BigInteger.valueOf(initialValue));
}
//Note: instead of changing constructor we use overloading for backwards compatibility.
public Count(BigInteger initialValue) {
this.value = initialValue;
}
public boolean exceeds(Count threshold) {
return value.compareTo(threshold.value) > -1;
}
}

Here I would like to introduce a metric called object’s influence. It means how many other objects know information details about our object. Another interpretation counts how many classes we must update when we change representation. The metric starts from one to infinity. The lower the score, the better. We should apply this metric during design/coding phase, before we push our code. That means before creating an accessor method, to stop and think about purpose of the variable. How it affects the result? In what computations we use it? Can we compute something (even partially) and return it instead of exposing the variable?

By moving computation related to variable to where we define it we get several benefits:

  • Handle changes quickly. We simply have to make changes on single place. Even tests don’t require refactoring, because we expose behavior and behavior stays unchanged.
  • Reduce maintenance cost — directly fallows from previous benefit.
  • Reduce higher level code responsibilities to handle only business policies. Low level code handle everything related to their internal representation.
  • Improve readability of high level methods — they don’t have to handle lower level representation details.
  • In case of using same counter on many places we reduce code duplication, as every module will have to deal with lower level representation the same way.

Grasping such concept on business-logic level looks not so difficult, although it takes some effort to start consistently using it. The problem emerges when we need to transmit our business object internal state to I/O stream like database, rest service, etc. If we intend to hide all representation details, our tools for I/O like ORM, marshaling, etc., would not work anymore, as they rely on accessor to retrieve object’s data. If we think about it, such tools don’t modify the variable, they do not perform any computation, just write it to some stream. In such cases when variable corresponds to final result without any modifications we can use getters. I’ll mention another solution. We can create bridge that transfers state between business object and user interface or database. Export method that returns Map with all the properties acts as such bridge. I’ll describe this idea in more details in another post.

As an exception we can write accessor to retrieve an object that exposes behavior instead of data. Like in our service class above we can write something like getCount as it returns Count. Count class doesn’t leak its implementation details. We have full control over it, and we can handle any future changes internally, without modifying every module that uses it.

In conclusion — prefer behavior to data. Behavior resists to change more than data.

--

--