Home Design Patterns Private Class Data

Private Class Data

Foundations

Reduce exposure to unintended change by hiding a class's data behind the narrowest surface that still works.

Purpose

A private class data pattern uses information hiding to protect a class's data. In doing so it blocks a client from making it unstable by unintentionally or intentionally modifying its state.

There are two levels to it, and only the second one is interesting. The first is the private keyword, which every modern language has and which you apply without thinking about it. The second is narrowing write access further than the language makes you: moving the attributes into a separate data object that is filled once by the constructor and afterwards exposes readers only. At that point even the class that owns the data cannot change it, which is a stronger and more useful guarantee than keeping outsiders away from it.

Design

It is built into most modern languages, so those implementations are simple. Languages like JavaScript did not directly support private data. In these cases it is necessary to use some type of programming construct to achieve the result.

The full version of the pattern separates the class from its data. The visible class keeps the behavior and holds one reference to a data object. The data object has private fields, a constructor that sets them, and accessors that read them. Nothing else. The interesting question is not who can read the values; it is how many places in the program can write them, and the answer this design gives is one.

UML class diagram: a Client depends on AxisLimits, which owns a LimitsData object holding private fields with a constructor and two getters.
Note what is missing. There is no setter anywhere in the picture, so the only arrow that can change a value is the one that runs during construction.

Example

The Autobot Robotics embedded controller keeps the soft travel limits for every axis: the positions past which the arm will collide with its own fixture. For a long time those were plain public fields on a settings object, because on a small controller everything is in one address space and privacy feels like ceremony.

Then a calibration routine wrote a limit while a move was in flight. It was not a wild bug; the routine was written to relax the limits temporarily so it could reach a reference switch, and it was supposed to put them back. Under one particular abort path it did not. The arm was then free to travel into its own fixture, and it did. Finding it took a week, because the value was wrong and every module in the firmware was allowed to write it, so there was no small set of suspects.

The fix was not clever. The limits moved into a data object with a constructor and two getters, the settings object was made to hold one, and relaxing a limit became a matter of constructing a new limits object and installing it, which is one call that is easy to find and easy to log. The bug class did not come back, and that is the whole benefit: not that the data became safe, but that the list of places that could have written it went from the whole firmware to a single line.

class LimitsData
{
    private float minPos;
    private float maxPos;

    public LimitsData(float min, float max) {
        minPos = min;
        maxPos = max;
    }

    public float getMin() { return minPos; }
    public float getMax() { return maxPos; }
    // no setters, on purpose
}

class AxisLimits
{
    private LimitsData data;

    public AxisLimits(LimitsData d) { data = d; }

    public bool allows(float pos) {
        return pos >= data.getMin() && pos <= data.getMax();
    }
}
The constructor of LimitsData and the absence of a setter are the pattern. Everything else is ordinary code.

The office-side reporting console is a different problem, because it is JavaScript and for most of its life the language had no private at all. In the case of JavaScript, you would put the private members in the constructor:

//constructor
function secret(){
    var privateString = "I'm private";
};
var inside the constructor is the whole trick. privateString is a local of a function that has returned, so it exists only for code that was created inside that same call.

Methods declared inside the constructor close over that variable and can read it; nothing outside can, because there is no name to reach it by. See Private Members in JavaScript for more information. Modern JavaScript has native # fields and you should use those instead, but plenty of running code was written before they existed.

Seen in the Wild

The C++ pimpl idiom is this pattern taken to its conclusion: a class holds one pointer to an implementation struct declared only in the .cpp file, which is how Qt's d-pointer works and how a library changes its data layout without breaking binary compatibility with programs already compiled against it. Java's String keeps its characters in a private final array and copies on construction, so no caller retains a writable reference to the contents. JavaScript added # private class fields in ECMAScript 2022, enforced by the runtime rather than by convention. Python, by contrast, does not enforce anything: a leading double underscore only mangles the name, and the field is still reachable if you want it.

Comparisons

  • Facade narrows what a caller can reach as well, but at the scale of a subsystem, and reaching past a Facade is legitimate. Reaching past private data is not.
  • Module is the same instinct one level up: a deliberately small public surface over a larger unit of code.
  • Memento is the case where you must let state out of the class without letting anyone read it, solved by giving one object two interfaces.
  • Flyweight depends on this. Sharing one object between thousands of callers is only safe because the shared, intrinsic part cannot be written.
  • Builder is how you construct an object with many fields that are all set once, when a constructor with nine parameters has stopped being readable.
  • Aggregate applies the same argument to a group of objects: one entry point, and the members are not writable from outside.

Why It Exists

My take

The pressure here is a specific bug, and everybody who has maintained a large system for a few years has met it: a value is wrong, and you cannot find out who wrote it. Making a field private does not make the program correct. It makes that search finite. It converts "somewhere in four hundred thousand lines" into "one of the eleven methods in this file", and that difference is the entire return on the pattern. Read access has almost nothing to do with it, which is why I think the usual explanation of encapsulation, all about hiding implementation details from clients, undersells it.

In embedded work the same discipline has to be applied without any language support. C has no private, so a global written by an interrupt and read by the main loop is legal, ordinary, and the source of the worst bugs in the system, because the failure is timing dependent and will not reproduce on the bench. The rule we used was one owner, one writer, and everybody else gets an accessor. That is exactly this pattern, enforced by review instead of by a compiler, and it is the version that taught me the value of the idea rather than the syntax of it.

The part that deserves to be called a pattern is not private. It is the second move: set the field once, in the constructor, and do not write a setter. That produces an object you can share between threads without a lock, cache without invalidating, and compare by value. Most codebases declare every field private and then generate a getter and a setter for each one, which is public fields with more typing and a slower build.

Criticisms

Since it is built into most languages, it might not be considered a design pattern by some, and I have some sympathy with that. A pattern is supposed to be a solution you assemble, not a keyword you type. However, for the languages that do not support it, it can be challenging to come up with a way to make it, and it is useful to have a pre-designed pattern to use in those cases. That was the situation JavaScript was in for twenty years, and it is still the situation in C.

Private is a courtesy from the compiler and not a security boundary. Reflection in Java and C# will read and write a private field cheerfully, serialization frameworks do it routinely, and a debugger ignores the distinction entirely. If your reason for hiding data is that the caller might be hostile rather than careless, this pattern is the wrong tool and you need a process boundary. The JavaScript closure is actually stronger than most languages' keyword here, since there is genuinely no name to reach, but it costs: privileged methods have to be created inside the constructor, so they cannot live on the prototype and every instance carries its own copy of every function. With ten objects that is nothing. With ten thousand it is measurable.

Finally, private does not mean immutable, and the two get conflated constantly. A getter that returns the internal list hands out a writable reference and undoes the whole exercise, quietly, with no compiler error. If you take the pattern seriously you have to return copies or read-only views, and at that point you are paying an allocation on every read to protect against a mistake nobody has made yet. That trade is worth making for the settings object that controls where a robot arm is allowed to travel. It is not worth making everywhere.