Objects and Data Structures
Keeping a class's variables private is not a formality. The reason we hide them is to keep the freedom to change how they are stored or computed without breaking every caller. That freedom disappears the moment a class hands out a getter and setter for every field, because a pile of getters and setters is just a public variable with extra syntax. This chapter is about the two opposite ways of designing around that freedom, and about being deliberate regarding which one you are choosing.
Data abstraction
Compare two classes that both represent a point on a plane. One exposes its coordinates as public fields. The other exposes nothing but behavior - callers can move the point, but they cannot see whether it is stored as Cartesian coordinates or as polar ones underneath.
Neither shape is a mistake by itself. The Point object could switch from Cartesian to
polar storage tomorrow and no caller would notice, because none of them ever depended on the
representation - only on the behavior. PointData could not make that change invisibly,
because every function that touches .x and .y directly is already coupled to that
specific representation. What is a mistake is a class that tries to be both: public fields
next to getters and setters that just forward to them, which exposes the implementation
while adding the ceremony of an interface that hides nothing.
The rule of thumb: objects hide data behind behavior and expose what they do. Data structures expose data and have no meaningful behavior. Pick one on purpose, per class.
Object-oriented vs. procedural: the shape example
The clearest illustration of the tradeoff shows up when you have a handful of shape types and one operation - area - that needs to work on all of them.
One design gives every shape its own area() method behind a common interface:
The other design keeps the shapes as plain data - a Square is just a side, a Circle is
just a radius - and puts all the area logic in one Geometry class that switches on shape
type:
These two designs are not just different styles, they trade the same flexibility in opposite directions:
- Adding a new shape is trivial in the object-oriented design (one new class implementing
area()) and invasive in the procedural one (a newcasein every function that switches on shape type). - Adding a new operation - say,
perimeter()- is the reverse. In the procedural design it is one new function. In the object-oriented design it means touchingSquare,Circle, andRectangleall at once, since each one owns its own method table.
Neither design is universally correct. The object-oriented shape works well when new types show up often and the set of operations is comparatively stable - which matches most real-world shape catalogs. The procedural shape works well when the operations multiply and the types are fixed, which is common in things like compilers working over a fixed set of syntax node kinds. Choosing between them is choosing which axis you expect to change.
The Law of Demeter
Objects should hide their internals, which means a well-behaved method should only talk to its immediate collaborators: itself, its own fields, objects it created, objects passed to it as arguments. It should not reach through one object to get to another, then reach through that one to get to a third.
A chain like ctxt.getOptions().getScratchDir().getAbsolutePath() is often called a train
wreck, and the name fits - it is a chain of calls each hopping into the internals of the
previous return value, and it tells you that the calling code knows the entire structure of
Options and ScratchDir, not just what ctxt can do for it.
The fixed version asks ctxt directly for the one thing the caller actually wants. ctxt
can walk its own internals however it likes, and the caller never has to know that
Options or ScratchDir exist at all. If either of those change shape later, this call site
does not.
Whether a violation is a real problem depends on whether the objects in the chain are objects or data structures. Chaining through data structures - a plain record with fields that are themselves plain records - is not a Law of Demeter violation, because a data structure was never hiding anything in the first place. There is nothing to leak. The rule is about not reaching through objects whose whole point is that their insides are supposed to stay hidden.
Hybrids
The confusion between the two shapes above breeds a third, worse one: a class with real methods that also hands out public fields or pass-through accessors for the same data those methods operate on. It is hard to add a new function to it, the same as any object, and it is also hard to add a new data variant to it, the same as any data structure - it inherits the worst of both without the benefit of either. A hybrid is usually a sign that nobody decided, on purpose, which of the two shapes the class was supposed to be.
Data transfer objects
Sometimes you want a class that is nothing but exposed fields and no behavior on purpose, usually at a boundary: the shape of a row about to be read from a database, or the shape of a response about to be serialized to JSON. This is a data transfer object, or DTO, and it is the data-structure half of this chapter used deliberately rather than by accident.
A DTO is not a failure to write a "real" object. It is the right shape for something whose entire job is to carry values across a boundary - a database row, a wire format, a form submission - where there is no behavior to hide because there is no behavior at all. Trouble starts only when a DTO grows business logic, or when a real object gets treated like a DTO and has its internals exposed for a convenience that undoes the reason it was an object in the first place.
A common variant is the active record: a DTO with save() and find() methods bolted
on, usually a direct mirror of a database table. Those navigational methods are fine. The
hybrid trap reappears the moment business rules get added to the same class - the fix is the
same as anywhere else, keep the active record as a plain data structure and put the business
logic in a separate object that wraps it.