Getters and Setters aren’t all they’re cracked up to be
Allen Holub, author of Holub On Patterns, has an interesting article on how if you use getters and setters, you’re probably doing it wrong.
His main claim is that getters and setters are a way of shoehorning procedural design into an object-oriented language, done by people who don’t really understand the difference between a C-style struct and a class. He mentions a course taught in the ’80s by Beck and Cunningham, in which they lament, “the most difficult problem in teaching object-oriented programming is getting the learner to give up the global knowledge of control that is possible with procedural programs, and rely on the local knowledge of objects to accomplish their tasks. Novice designs are littered with regressions to global thinking: gratuitous global variables, unnecessary pointers, and inappropriate reliance on the implementation of other objects.” Holub talks about the CRC design model, in which Classes have Responsibilities and Collaborators. The idea is that the interface to a class should be a list of actions the class can do (its responsibilities) for the other classes it works with (its collaborators). In particular, the interface should not be a list of what data the class can store/retrieve for strangers (which is what getters/setters really are). Under this model, he claims that nearly all getters/setters can be removed in favor of functions that do the work you wanted for you, rather than giving you the info you need to do the work yourself.
One concrete example he obliquely mentions is that objects in a UI could have a DrawYourself function whose arguments are everything from the outside world you need to draw to the screen. Holub admits that it looks prima facie like we’re going to “put UI code into the business logic,” but the trick is that the DrawYourself function is mostly just a way to keep the getters and setters private: all the real work is done by whatever object contains the frame buffer and does graphics stuff, and DrawYourself just passes a bunch of private values to the graphic-drawing system it got as an argument. This keeps the graphics stuff abstracted away in the graphics classes, while at the same time eliding the need for getters/setters entirely.
Reading this, I was reminded of the Visitor design pattern.1 Both the Visitor pattern and Holub’s examples of avoiding getters and setters use double dispatch in such a way that all involved objects get the information they need to do the work you want done, but without giving any of their own information to anything they don’t already trust. I don’t know how verbose or cumbersome this can become because I haven’t yet tried removing all getters and setters from my code, but it’s an interesting idea.
I started looking into this sort of thing when Dustin mentioned at work one day that he didn’t see the point of getters and setters in the first place. It was his opinion that if you have a variable with a getter and a setter, you should just make it public and get rid of the wrappers around it. Sure, the common wisdom is that all your data should be private, but if you really need a getter and setter for this data, making it private doesn’t help you at all. If you want to change the way the data is stored but you want your external interface to stay the same (a common argument for why data should be private), he points out that the G&S don’t help: when you change the underlying representation (for instance, changing an int
to a long
), your getter and setter will also need to change, in which case you’re still changing the interface to your class. The one exception here is if you wanted to start using some sort of lazy evaluation and compute the value on the fly, rather than storing it in a variable. but in practice, how often does that actually happen? Roughly never. So, Dustin’s take is that if you have getters and setters, it’s simpler and more straightforward to just make the variable public. This kinda sounds like something is wrong, but I couldn’t figure out what it was (Dustin had an excellent rebuttal to every argument I could put forth). After reading Holub and seeing how he goes one step further and points out that if you have getters and setters in the first place, your design is bad because you’re still thinking procedurally instead of object-orientedly, I can see the mistake in Dustin’s argument: it’s his (and my) implicit premise that getter- and setter-like behavior is permissible at all! If you accept that getters and setters are ok, you don’t lose much of anything by just making those variables public, but you’ve already lost much of the power of object-oriented design by giving up parts of encapsulation and abstraction. Now that I see this, the only classes I can come up with that I still think require getters/setters are things that could easily be represented as a C-style struct instead of a class: they’re really just a set of data that goes together, rather than anything really classworthy.
1 People are often biased against design patterns because they have poor introductions to the topic. Design patterns are really only useful when you start working on huge codebases, so if you try to learn them on a toy problem in school, they’re going to seem silly and pointless. I’d go so far as to say that you almost never need to use design patterns at all, even in most large codebases. but it’s handy to know them, because every once in a while something comes along where applying a design pattern can take something that was nearly impossible or full of kludges and turn it into something that is pretty darn elegant. I don’t consider myself a proponent of design patterns, but I’m not afraid of using them if the need arises.
Another potential use of getters and setters is data validation.
However, I agree with the high level point that usually they are not necessary at all. In practice, I find myself writing code with few getters and no setters.
Even with classes that can be seen as little more than a fancy pants struct, it is often cleaner to have the data fields be immutable from the outside. E.g., I create a point with X and Y values which have getters but no setters; if you want to change the point, you create a new one (obvious caveat: there may be situations where this is inefficient). Oftentimes, as with a point, this is conceptually cleaner; a point exists at one location, it does not move around.