I don't like getters and setters and prefer direct field access

September 13, 2018

One of the great language divides and system design debates is between direct access to fields in objects and doing things through getter and setter functions. In this debate, so far I come down firmly on the side of direct field access. I have at least two reasons for this.

The obvious reason to dislike getters and setters is that they're bureaucracy and litter. We've all seen codebases that have a whole collection of tiny methods whose only purpose is to get or set a field, and they only exist because someone said they had to (sometimes this is the language, sometimes this is someone's coding standard). Using these methods is annoying, writing these methods is annoying (and I consider that IDEs can automate this to be a danger sign), and just having them around cluttering up the code is annoying. Direct field access wipes all of this extra clutter away.

The more subtle reason I don't like getters and setters is that they obscure what's actually going on. A getter could be a simple field access that will be inlined into your code in many situations (in some languages), or it could be a whole series of expensive operations that talk to a database and so on. The difference between these two extremes matters quite a bit when you're actually writing code that uses the getter, so one way or another you have to know. With direct field access, generally what you see is what you get; direct field access is just that, and method calls are for things that clearly involve some computation (and perhaps a lot of it, you'll want to consult the documentation).

This straightforward honesty matters, because part of the purpose of an 'API' boundary is to communicate to other people (perhaps you could argue that this is its entire purpose). Getters and setters are mumbling; direct field access is speaking clearly (at least in theory).

At this point it's traditional to raise the possibility that you'll need to change the internal implementation of your objects (or structures) without changing the API that other people use. Using getters and setters theoretically allows you this freedom, while direct field access doesn't. One part of my views here is that performance is part of your API, at least as far as slowing things down goes. Because performance is part of your API, in practice how much you can change your getters and setters is already fairly constrained. If you turn a fast getter or setter into a slow one, people will be unhappy with you and they will probably have to change their code.

All of this gives me certain biases in larger language design issues. For one relevant example, I rather wish that Go interfaces included fields, not just methods (although I sort of see why they don't).

Sidebar: The downside of fields in Go interfaces

If you allow interfaces to have fields, not just methods, anything implementing such an interface must have those fields. In other words, such interfaces can only be implemented by types that are structs (or pointers to structs, which is the more likely case). Method-only interfaces have the advantage that they can be implemented by any type, including weird types such as functions or the empty struct.

Comments on this page:

Thank you for your thought. I see that the fundamental point is accessors should be performant, as cheap as a direct access. But accessors do have some benefits, like handling field renames and internal struct changes. And if you try to include fields in an interface, their access essentially becomes simple accessors because you need to find the fields in some way (similar to calling a method via vtables).

On the other side, in dynamically-typed languages like Python, Ruby and JavaScript, without inspecting close enough, nobody can be sure whether a seemly direct access is really that direct. (And sometimes, accessing an attribute causes visible side effects like sending out HTTP/RPC requests...or even worse: there is a tool called pudb in Python, and every time I access the pu.db attribute the debugger is spawned.)

By cks at 2018-09-13 11:35:26:

In a language like Go, accessing fields through interfaces would be simple to implement. Go interfaces are already a combination of (essentially) a pointer to the object and a pointer to a descriptor table. Normal field access for static types is 'pointer to start of object plus a fixed offset'; field access for interface fields would turn into 'pointer to start of object plus a variable offset loaded from descriptor table'. This would be slightly less efficient than before, but not much, and it definitely wouldn't require function calls.

As far as performant accessors go, I think it's okay if accessor functions are slow as long as they're honest about it. The problem with the usual getters/setters approach is that it deliberately hides this information, which is partly a social thing. As far as field renames and minor restructuring go, I think there are other mechanisms that languages could use to support this if they wanted to. For more major ones, I start getting dubious that the practical API is being preserved even if the nominal one is.

Languages that allow interception of what looks like simple fields are a different thing entirely. One view of the situation is that these languages have made a cultural choice; another one is that they've created an implicit contract that something that looks like simple field access should work more or less like it. In practice, I think Python is more on the cultural choice side, where you accept that accessing 'fields' may do random magic and you have to count on people to be sensible.

By Greg A. Woods at 2018-09-17 14:18:42:

I too have always firmly sided with field access and detested getters and setters.

(except maybe in the one true OO language, i.e. smalltalk, where it's all message passing all the time for any interaction other than passing the whole object around as an opaque pointer)

The code base I work with these days gave me slight pause to think about this again though, and that's because it's a C using pthreads, and thus mutexes. With the added complication of requiring proper locking order for field access it only really makes sense to mandate helper functions to access internal object fields.

I've also noticed that use of opaque objects in C can be somewhat confusing and troublesome if the naming of types (and important identifiers) isn't very well done and understood, especially if the type describing such an object even goes so far as to hide whether it is a struct or a pointer. This is alleviated somewhat, at least for experienced programmers, if they are also allowed to at least examine the implementation for clues as to how it might best be used.

Another issue I've found cropping up in our code is extreme bypassing of the type system (i.e. abuse of "void *"). In our case it's slightly justified as a means of implementing "generics" in C, since the code in question is effectively an ORM, but it can lead to terrifically difficult-to-debug bugs, especially for those of us who have grown used to having the compiler detect type system violations.

Written on 13 September 2018.
« A surprise discovery about procmail (and wondering about what next)
How you migrate ZFS filesystems matters »

Page tools: View Source, View Normal, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Thu Sep 13 01:03:44 2018
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.