One of the unpopular opinions I’ve long held in programming is that accessor methods (getters and setters) are a terrible practice. Now that I have found somebody who agrees with me (timestamps 1:13:06 to 1:16:25) I am ready to write about this!

This is what I mean (C++ code, but the same concept applies in any language):

class Foo {
private:
    int mData;

public:
    int GetData() const { return mData; }
    void SetData(int newData) { mData = newData; }
};

That’s a lot of code that does very little, compared to simply making mData be public!

You may also have had the misfortunate of seeing a C++-specific abomination of this, which defeats even the few possibly legitimate reasons why you may prefer accessor functions:

class Foo {
private:
    int mData;

public:
    int& Data() { return mData; }
};

Why do I dislike accessor functions so much? In almost every case it is simply a lot of pointless code to write and maintain. Also, accessor functions are functions: what one day was a cheap field access, the next day can turn into an expensive lookup. (This can also be a feature, in very limited scenarios as I explain later.) Even worse, what was once a getter may end up modifying state somewhere along the line! (Admittedly this is mostly mitigated by marking the getter method as const in C++.)

A common argument for having accessors is wanting to do validation in the setter function. In practice however I’ve found that it’s almost always better to do validation where you actually use the data. This is more flexible because it allows different parts of the code that may both operate on the same state to have different expectations against it, and it’s also easier to maintain because the relevant parts of the code are closer together, and less likely to drift apart in meaning or intention. This is the same kind of thinking as: parse, don’t validate.

When you may actually want them

There are some scenarios where accessor functions, though mostly just getters, do actually make sense. In all of these cases, exercise judgement on whether your reasons are really the right ones. In all cases, you should default to just exposing the field; the burden of proof lies with the side that argues for adding more code.

Read-only fields

If you want to make a field read-only, then making it private and adding only a getter to it can be okay, but think carefully whether your reasons for wanting this are strong enough. In many cases, it turns out that there’s no harm in exposing the field itself. One example is data that is just being exposed and will not be (directly) manipulated further, i.e. changing it would achieve nothing. If you are tempted to hide this behind a getter function, ask yourself: is that useful? (If you are worried about your colleagues writing code will be pointlessly changing the field, then you have an HR problem, not a technical one.)

I cannot think of any legitimate reasons to have write-only fields. Even if you think nobody could possibly have to read the field’s value, is it really worthwhile writing extra code to make that impossible?

Hiding representation

Suppose that you have a field that is currently an std::string, but you expect that that may change. For example, you may want to actually store the entirety of data in-line in a fixed size array. Or you know that your string will never grow or shrink, and saving 8 bytes (by not having to store separately the end of data and the end of buffer) will allow your data to fit into the cache better and will significantly improve performance. Then it can be a good idea to put your field behind accessors that hide the field’s actual type, e.g. by exposing std::string_view.

Consider also whether the other types may use (in place of the std::string in this example) would be able to provide the same API. If so, then maybe users wouldn’t even really notice the change in type, or if they do, it will be in the form of an extremely clear compiler error (expected type foo and got bar) that is trivially easy to find and fix.

Hiding location

In some scenarios, you may have a complex class, and you can choose whether to store some data as a field in the object itself, or somewhere else reachable from the object: e.g. through some pointer or reference, or even trivially derived from some other data. In this case, the field’s existence may be an unstable implementation detail, and hiding the field behind accessors may be okay. For example:

class Foo {
private:
    MoreData* m_indirectData;
    int m_barLocalCache; // this is just a performance optimization

public:
    std::string_view GetName() const { return m_nameLocalCache; }
};

Maintaining binary compatibily

If you are building a library and you need to maintain binary compatibility, then you either have to use accessors, or be very sure that your fields never need to change position or size, nor will they ever move somewhere else.

However, if you end up writing accessor functions in this case, you have to be careful: their body cannot live in a header file (that you publish along with your binaries), otherwise the compiler may inline the field access and it was all for naught. You must also keep in mind the performance impact: a field access is much cheaper than a function call that only does a field access. (Function calls may prevent optimizations because the compiler may no longer be able to prove that some piece of data accessed through any pointers or references were not overwritten sneakily by the function call.)