skip to content

Java convention says make every field private and add getX()/setX(); Python convention says expose the attribute and reach for @property only if you later need one. Explain what language feature makes these opposite conventions both correct, and what each community pays for its rule.

level: middleimportance: should knowfreq 45%

answer

  1. Uniform access principle - Meyer
  2. Python: public first, @property later
  3. C#: source-compatible, binary-breaking; no ref/out on properties
  4. Go: no properties, accessor is Name() not GetName()
  5. A getter returning a field is a slower field

basics

~20 s

The uniform access principle: in Python, Ruby, C#, Kotlin and Swift a stored attribute can become a computed one without changing call sites, so accessors up front are noise. Java has no properties, so x.field cannot later become a computation - the getter buys future freedom, not encapsulation.

solid answer

~50 s

The real argument for accessors is never "encapsulation" - a getter that returns a field is a slower field. It is: can I change the representation without breaking callers? The answer is language-specific. - **Python**: `obj.total` can become `@property def total` later, callers unchanged - so pre-emptive getters are noise. Price: a property can hide expensive or side-effecting work behind something that reads like a field. - **Ruby**: attribute access is already a message send, so overriding is always possible; `attr_accessor` being one word makes the data-bag design the path of least resistance. - **C#**: field to property is source-compatible but **binary breaking** - dependent assemblies must recompile - and a property cannot be passed as `ref`/`out`, which is why the C# rule is "public members are properties from day one". - **Kotlin and Swift** compile properties to accessors, so Java's concern disappears. - **Go** has no properties at all and names the accessor `Name()`, not `GetName()`.

code

python · 9 lines
python
class Order:
    def __init__(self, lines):
        self.total = sum(lines)          # v1: plain attribute

# v2, later - callers still write order.total
class Order:
    @property
    def total(self):
        return sum(line.amount for line in self._lines)

go deeper

for a junior

Know that the accessor convention differs by language and that the reason is whether a field can later become a computation without changing callers.

for a middle

Name the uniform access principle, give the Python/@property and Java contrast, and know the C# binary-compatibility caveat.

for a senior

Steer the conversation from accessor style to invariants and behaviour placement, and apply the per-accessor test of what the caller does with the value.

for a principal

Set the convention per language for a polyglot codebase and decide the boundary rule - which types are open data at edges and which own rules - rather than mandating one accessor style everywhere.

## The principle being argued about Bertrand Meyer's **uniform access principle** says a client should not be able to tell whether a service is provided by stored data or by a computation. Where a language honours it, `account.balance` may be a field today and a computed member tomorrow, and no caller changes. Where it does not, the two forms are written differently at the call site, so switching is a breaking change to every caller. That single fact explains why two communities give opposite advice and both are right. ## Java: no properties, so the getter is insurance In Java, reading a field is `a.balance` and calling a method is `a.getBalance()`. Once you publish a public field you can never turn it into a computation, validate on write, add lazy loading, or fire a change event without editing every caller - and for a library, callers you cannot edit. The bean convention is a hedge against that future, plus an interoperability contract, because reflective tools historically discovered state through `getX`/`setX` names. Records give a shorter accessor form (`p.balance()`), still a method call. What Java pays: the convention that every field gets a getter and a setter mutates into the belief that this *is* encapsulation. A class with a private field and a public pair of accessors has the same public surface as a public field, with more code. Worse, the reflex produces objects that only carry data while behaviour drifts into service classes. ## Python and Ruby: the hook is always available Python's `@property` converts an attribute read into a method call transparently, so the community rule is "start public, add a property when you need one". Writing `get_total()` in Python is a recognised sign the author learned the idiom elsewhere. Ruby goes further: `obj.total` is already a message send, so every attribute read was always overridable, and `attr_reader`/`attr_accessor` merely generate the trivial methods. What they pay: uniform access cuts both ways. If the caller cannot distinguish a field read from a computation, they also cannot see that `order.total` performs a database query or mutates a cache. A property that is slow, that throws, or that has side effects is a genuine hazard, which is why the discipline is that properties stay cheap and pure. And Ruby's one-word `attr_accessor` makes an entirely open, invariant-free object the easiest thing to type. ## C#: uniform access with a compilation-model asterisk C# properties look like fields at the call site, so `p.Total` may become a computed property with no caller source change. The asterisk matters: fields and properties are different at the IL level, so the change is **binary breaking** and already-compiled assemblies must be recompiled. A property also cannot be passed to a `ref` or `out` parameter, and field-versus-property differs for reflection, serializers and data binding. Consequently the C# rule is not "add getters everywhere" but "anything public is a property from the start", with auto-properties making that cost one line. ## Kotlin, Swift, Objective-C, Go Kotlin exposes `val`/`var` as properties and compiles them to accessor methods, so declaring `val total: Int` and later giving it a custom `get()` is source-compatible and the JVM-level concern evaporates. Swift has stored and computed properties with the same syntax, plus `willSet`/`didSet` observers. Objective-C's `@property` and dot syntax made accessors the norm decades ago. Go deliberately has neither properties nor the bean convention: `p.Name` and `p.Name()` are different call sites, so Go APIs choose early and permanently, and the style guide explicitly rejects the `Get` prefix - the accessor is `Name()`, the mutator `SetName()`. ## What this actually means for design The accessor debate is a **representation-stability** question with a language-specific answer, and it is mostly independent of the design question that matters. Neither `public int total` nor `getTotal()` protects an invariant, because both expose the same information; and a `setTotal()` that assigns without checking anything is strictly worse than a public field, since it implies validation that is not there. The productive questions are: does this type own a rule that must always hold, and does the operation belong here or at the caller? A useful test for any codebase: for each accessor, ask what the caller does with the value. If the answer is "decides something the object could have decided", the accessor is enabling logic that belongs inside. If the answer is "serialises it", "renders it", or "asserts on it in a test", the accessor is legitimate boundary work. The language's property support tells you how much syntax that costs; it does not tell you which answer is right.

  • If Java's getter convention is really about representation stability, what does that say about a class whose getters and setters cover every field?
    That the hedge has been paid for and nothing bought with it. The public surface is identical to public fields, so no representation is actually hidden. The right follow-up is whether any invariant exists that the type should enforce, and whether the logic reading those getters belongs inside the type - not whether the accessors are formatted correctly.
  • Why is a property hiding an expensive computation a specific hazard in Python or C#, and not in Go?
    Because uniform access makes a call site look identical to a field read, so a caller in a loop cannot see that each read hits a database or recomputes a large structure. Go has no properties, so anything that computes is written with call parentheses and the cost is visible at the call site. The Python and C# mitigation is convention: properties stay cheap, pure and non-throwing, and anything else is a method.

saying these in an interview costs you the question

  • Saying getters and setters give encapsulation - a full accessor pair exposes exactly what a public field exposes
  • Writing get_total() in Python or getTotal() in Go, importing a convention the language does not share
  • Believing a C# field-to-property change is entirely non-breaking; it is source-compatible but binary breaking and blocks ref/out
  • Putting a heavy query or a side effect behind a property because the syntax lets you
  • Concluding that all accessors are wrong - serialization, rendering and test boundaries legitimately need them

context