It happens that having to call methods of a class in a particular order—unless there’s an obvious, hard dependency like “don’t invoke
Email::send()
until all the headers and body have been added”—leads to fragility. There’s no hint from the IDE about the dependencies or ordering.Worse, if a class has a lot of methods that must be called after setting some crucial data, every one of those methods has to check that the necessary data is set. It’s better to pass that into the constructor, and have the created instance always contain that data.
Given a class that wants to maintain a valid state for all its methods at any given time, how do setters fit into that class? It needs to provide not individual setters, but higher-level methods that change “all” relevant state at once.
So, I’m starting to see
get
and set
methods as an anti-pattern.I’ve run into this before. Say I want an object with a position and size, that keeps its bounding rectangle within the bounds of the canvas, in that it doesn’t let itself go fully offscreen. If the object allows updating x and width separately, it enables invalid state. Some caller could narrow the width before moving it closer to being on-screen, but due to a bug, not perform the second update.
Maybe detecting (or trying to draw) this only wastes time in the
draw
call, but by then, all draw
can really do is either skip drawing or crash, and the stack won't point to the problem any longer.But validating the object when each individual property is updated means that the state between “old x” and “old width” and their new values must also be a valid state. Either the caller gets stuck worrying about it, or making extra assignments, or the API needs a “mass update” method… and at that point, why keep individual
set
methods?The main exception I make to the “no setters” structure is for builder-like objects. Once an email message has all its metadata, body, and attachments, it really needs a method to create the text on the wire, and there’s no way around having that method depend on the ones that came before.
Another way to look at that is, a builder object is a collection of independent setters and a single, final stage where validation happens: during building. From the perspective of code outside, it is immaterial whether the builder or the built thing does the validation. (And potentially, not even observable.)
I’ve found something else interesting by banishing
set
methods. The approach also leads to a lack of get
methods, because none of the callers are interested in “what are these private variables you’re hiding from me?” any more. They’re interested in the operations they can perform on the object. I suspect that’s a better design, overall.
No comments:
Post a Comment