May 312024
 

I’ve been against the idea of writing getters and setters off as boilerplate for a while, yet sadly that’s the prevailing idea in software development. A large part of that is probably fueled by the fact that we treat objects is nothing more than a local copy of database records. The result is that half the time we’re assuming the data is already valid and we don’t have to validate or default anything (when we’re reading it from the database), or that we just have to validate data when it first enters our system via endpoint (when creating or updating a database record). As a result, we don’t have to actually write proper getters and setters, and can lazily just use the templates our IDEs make for us, and outsource our validation to some simple annotations. It’s a tempting sales pitch, but it’s for a specific (limited) use case. Getter and setter methods, on the other hand, are universal.

A bit about getters and setters

Getters and setters are a basic concept of object-oriented programming, generally covered shortly after learning member variable visibility. As a reminder, it’s generally a good idea to make a class’s member variables private so they can’t be manipulated by other parts of your code. Instead, we use getters and setters to control access to the variables and make sure nobody is doing something they aren’t supposed to (e.g. setting a number that should be positive to a negative value, entering a phone number with the wrong number of digits, or an email value that isn’t an actual email address, etc.).

The problem is that in a lot of web applications and services, the objects are mostly intended to be representations of records in the database. Because we tend to trust data already in our database, we don’t feel the need to check it, so we feel safe using the bare-bones methods generated by IDEs. This gives the (false) impression that getters and setters are simply “boilerplate” that add clutter to your code, to be automatically generated through IDEs or hidden away through tools like Lombok.

It’s like we learned what getters and setters are, but never learned why code should use them. Right now, our use of default getters and setters leaves us with “private” variables that are functionally public, instead of a place to ensure variables have valid data (and required variables have data at all). Validation logic is still logic, is still specific to your problem domain, and still needs to be in your code.

Don’t the annotations work?

Sure, to validate the object as it’s being translated from the JSON string into your object that will be used throughout the code. After that, you’re on your own. Now, if your request body is final and you’re not creating or modifying these objects anywhere else in your code (i.e. you’re just treating these objects as records read from the database as-is, or user input to write to the database as-is), then you can totally get away with just using simple annotations.

However, if you’re doing anything else with these objects, the best place to put this validation is in individual setters. In fact, I think you should just skip the annotations and use the setters anyways, because validating fields in setters always works, both when converting JSON to an object and when working with objects elsewhere in your code. Relying on validators means you have to remember that field validation only happens when the object enters your system via HTTP endpoint, and that you’re responsible for making sure objects are valid from that point forward. On the other hand, writing proper setters means the validation is always there, where you need it, when you need it.

Back to getters and setters

Remember, the point of getters and setters is to control access to variables, particularly the values that get assigned to them and set defaults when needed. To be more precise, we want to be able to do this no matter when these variables are being populated or read. With web applications, we moved from a simple system that worked to something that only checks once, when an object enters our system, and then assumes everything’s fine from that point on. The whole point of setters is that you put your validation logic there. While you’re at it, build some default values into your getters too – that way the values your objects are returning always have a sane default, just in case you missed something with your setter.

A quick note on fields whose values depend on other fields

So far, this whole post has ignored fields whose validation depends on the value of another field (think start and end, or min and max values). There are ways to do this with custom annotations, but it’s also possible to do with simple setters too. Below is a simple demo of what I mean:

package net.hydrick.objectmapperdemo;

public class SampleObjectWithSetters {

    private int min;

    private int max;

    /* False to start */
    private boolean initialized = false;

    public SampleObjectWithSetters(int min, int max) {

        /*
         * I'm going to use asserts in the setters for simplicity and cleanliness, you'd
         * likely want an if-statement and proper Exception.
         */
        setMin(min);
        setMax(max);

        assert min < max;

        initialized = true;

    }

    public int getMax() {

        if (max >= 100) {

            max = 100;

        }

        if (initialized && max <= min) {

            max = min + 1;

        }

        return max;

    }

    public void setMax(int max) {

        assert max >= 1 && max <= 100;

        if (initialized) {

            assert max > min;

        }

        this.max = max;

    }

    public int getMin() {

        if (min <= 0) {

            min = 0;

        }

        if (initialized && min >= max) {

            min = max - 1;

        }

        return min;
    }

    public void setMin(int min) {

        assert min >= 0 && min <= 99;

        if (initialized) {

            assert min < max;

        }

        this.min = min;

    }

}

The basics here are that I have an object with a couple of values, min and max, which have to be between 0 and 100 (0 – 99 and 1 – 100 technically, I’ll get to that next sentence). I also have a requirement that max has to be greater than min (note that they can’t be equal). However, when I’m creating the object I can’t exactly compare the max and min values in my setter when I’m initializing the object the first time. So I’m adding a new flag, initialized, to track when I’ve initialized the object. Once my variables are set (and valid on their own), I can do an initial check of their values against each other, and upon confirming they’re valid compared to each other, set the initialized flag. Now every time I update the value, I compare min and max to make sure min is always less than max.

Getters and setters serve a real purpose in our code, and the trend of trying to blow them off as “boilerplate” that we should somehow be trying to ignore or avoid needs to stop. The reality is, these are the places to put your validation logic (and yes, validation is still logic, still important, and should absolutely be part of your codebase). Just because we’ve gotten lazy with assuming that everything was all checked in whatever web form our website has before calling us so it’s already valid doesn’t make that true (spoiler alert, front-end code can have bugs, and the web back-ends are just handlers for HTTP requests – there’s no guarantee that request originated from your form with the handy validation). It’s also meaningless if you ever create 1 of these objects, or update object values, on the server. There’s a reason we teach CS students about writing getters and setters so early in their educational career – because they work.

 Posted by at 11:45 PM