Getters / setters sucks. Here's why.

Fellow developers, PLEASE don't teach the juniors those `getters/setters` quirks.

What?

When consulting a domain expert to describe an entity, it's unlikely they would characterize it simply as "a bunch of attributes." Utilizing objects solely as data structures can lead to numerous challenges related to software maintainability and evolution. This approach can inadvertently misuse and neglect the brilliant concepts that were established decades ago.

It's essential to recognize the potential pitfalls associated with exclusively relying on objects as data structures and to explore alternative approaches that align better with the principles of good software design.

Traditionally, programmers have adhered to the practice of using names like getAttribute...() to expose and relinquish control over previously private attributes. However, it is important to note that methods in the form of setAttribute...() or getAttribute...() should ideally not exist.

The fundamental principle of Object-Oriented Programming (OOP) revolves around bijection, which entails having a one-to-one correspondence between domain models and real-world objects. The functionalities of these objects should align accordingly. In my experience, I haven't encountered a real-world object where I can directly invoke getAttribute() without first interacting with the object itself to obtain the desired information. This underscores the importance of engaging with objects in a manner that reflects their inherent behavior and characteristics.

But what does it help?

Implementation Hiding

The practice of using objects as data structures has long been established, but it brings forth numerous challenges concerning software maintainability and evolution. Additionally, it tends to misuse brilliant concepts that were introduced half a century ago.

In his influential paper from 1972, titled "On the Criteria to Be Used in Decomposing Systems into Modules," David Parnas defined a groundbreaking and foundational concept for modern software engineering: Information Hiding.

The rule is straightforward:

If we hide our implementation we can change it as many times as necessary.

Before the publication of Parnas' paper, there was a lack of clear rules regarding information access in software development. It was a common practice to directly delve into data structures without questioning the consequences, often resulting in a dreaded ripple effect that imposed penalties whenever changes were made.

Example:

class Point {
   public double x;
   public double y;
}

Hence, if we want to change the accidental implementation of the point to its polar coordinates analogous:

class Point {
   public double angle;
   public double distance;
}

When the inner functionalities of a class are exposed to the outside world, any modifications made to the class will inevitably require corresponding changes in the code that utilizes it. This creates the anticipated ripple effect, which significantly complicates code maintenance and makes it more challenging to introduce changes. By exposing the inner workings of a class, we inadvertently increase the interdependencies and decrease the encapsulation, undermining the maintainability and flexibility of the codebase.

A good design is one in which objects are coupled to responsibilities (interfaces) and not representations.

Therefore, if we define a good point interface, they can arbitrarily change their representation (even on runtime) without propagating any ripple effect.

final class Point {
    private double x;
    private double y;

    public double x() {
        return x;
    }

    public double y() {
        return y;
    }
}

When the implementation changes:

final class Point {
    private double angle;
    private double distance;

    public double x() {
        return distance * Math.cos(angle);
    }

    public double y() {
        return distance * Math.sin(angle);
    }
}

Repeated or absent logic of invariant verification

Many objects have invariants that guarantee their cohesion and the validity of the representation to maintain real-world bijection. Allowing partial setting (an attribute) would force us to control representation invariants in more than one place, generating repeating code, which is always error-prone when modifying a reference and ignoring other references. This logic will be explored later.

Bonus: Collections

A significant number of objects are responsible for managing collections. The management of contents, invariants, and traversal methods should be the exclusive responsibility of these objects.

Let's consider a scenario where we aim to draw a polygon on a canvas. We can accomplish this using the following code:

Polygon triangle = new Polygon(new Point(1, 1), new Point(2, 2), new Point(3, 3));
Point lastPoint = triangle.getVertices().get(triangle.getVertices().size() - 1);
for (var vertex: triangle.getVertices()) {
    canvas.drawLine(vertex, lastPoint);
    lastPoint = vertex;
}

By exposing the vertices collection (and since collections are passed by reference in most languages) we lose control over that collection.

Nothing prevents this other code from running:

remove_first(triangle.getVertices());

The remove_first() function, which removes the first value from an array, introduces a problematic situation when applied to polygons. This mutation disrupts the consistency in the real-world bijection, as two-sided polygons violate the principle of being a closed figure.

At this juncture, we are presented with two options:

  1. Duplicating the business logic in both the constructor and the setter methods.

  2. Permanently eliminating the setter and embracing immutability.

If we opt for the first choice and accept duplicated code, the ripple effect will start to spread as our constraints become more stringent. For instance, if we impose an even stronger precondition:

Let's assume that the polygon must have a minimum of three distinct vertices.

According to our design axioms, the correct answer is the second option, emphasizing immutability.

When there is a need to return the collected elements from a collection, it is advisable to provide a copy (shallow) to maintain control over the data. Fortunately, with the advancements in technology, copying collections is now extremely fast. Even in cases where the collections are significantly large, there are design solutions available, such as iterators, proxies, and cursors, that can be employed to avoid the need for performing a full copy operation. These approaches enable efficient access to the collection elements without compromising control or incurring unnecessary overhead.

Solution

  • Avoid using setters. There are rarely valid justifications for their use.

  • Minimize the use of getters. If any of the object's responsibilities involve responding to a message corresponding to an attribute, carefully consider if this compromises encapsulation.

  • Avoid prefixing function names with "get". Instead, if a polygon in the real world can provide information about its vertices, use a method name that reflects this, such as vertices().

  • When returning collections, consider returning a copy or a proxy to maintain control and encourage the use of iterators.

  • Avoid having public attributes. They are akin to having implicit setters and getters and often indicate anemic objects.

  • Avoid having public static attributes. In addition to the reasons mentioned above, classes should ideally be stateless, and the presence of public static attributes is indicative of using a class as a global variable, which is generally considered a code smell.

The way we think

By shifting our focus from the accidental representation of objects and instead prioritizing their intrinsic behavior, we can effectively avoid the pitfalls of anemic classes. Anemic classes, which merely serve as data containers, are well-known examples of an anti-pattern.

Similar to data structures, anemic classes lack the means to ensure data integrity and enforce relationships. As operations on anemic classes occur outside their boundaries, there is no single point of control. Consequently, this leads to duplicated code and direct access to attributes, which compromises the integrity of our model.

We aim to emulate the behavior of objects as black boxes, enabling us to establish more realistic and declarative bijections, and moving away from the limitations imposed by anemic classes.

Bonus: if you need an actual data structure, consider https://www.baeldung.com/java-record-keyword

Read more