2026-05-12 · By David Buch

What “read FX write FX” actually saves you

This is the cheapest one-line refactor I know. The before:

property Username: string read FUsername write SetUsername;
...
procedure TUser.SetUsername(AValue: string);
BEGIN
    TRY
        FUsername := AValue;
    EXCEPT
        RAISE;
    END;
END;

The after:

property Username: string read FUsername write FUsername;

That is the entire change. Eight lines became one. The behaviour is identical — the property still encapsulates the field, the property still controls access, the field is still touched only through the property name elsewhere in the unit. We have only deleted dead ceremony.

What we lost

Nothing. Walk through it. The setter we removed did one thing: it copied AValue into FUsername. That is exactly what write FUsername does. The TRY/EXCEPT/RAISE wrapper was there because the codebase rule says every method body has one. With no method body, no wrapper is needed. The rule still applies; it just has nothing to apply to.

What we gained

Less surface area. One fewer method to step into. One fewer place a future developer can — mistakenly — add validation, transformation, or a side effect that one caller depends on and the next caller does not. If validation belongs in the setter, the setter exists and earns its keep. If it does not, the setter does not exist. The rule generalises and stays honest.

When the setter does earn its keep

Three patterns:

  1. Validation: IF AValue.IsEmpty THEN RAISE EArgumentException.... The setter rejects bad inputs at the boundary instead of letting them propagate.
  2. Transformation: FUsername := AValue.Trim.ToLower;. The setter normalises so consumers see one canonical form.
  3. Notification: FUsername := AValue; IF Assigned(FOnChanged) THEN FOnChanged(Self);. The setter triggers an observer side effect.

If your setter does any of those three things, keep it. If it does none of them, delete it.

The audit

Open any non-trivial unit in your codebase. Search for procedure Set. Read each match. For every setter whose body is one line of FX := AValue, delete the method, change the property to read FX write FX. The whole pass takes about ten minutes per thousand lines. The unit gets shorter. The class’s public surface stays exactly the same. Nothing breaks.

This is the rule from Chapter 13 of The Delphi Way, in one blog post. The chapter has the worked example and the longer rationale; this is enough to do the refactor without reading the chapter first.


Back to the blog