skip to content

Why do experienced Ruby developers avoid @@class variables, and what would you use instead for per-class settings in a Shape hierarchy?

level: seniorimportance: should knowfreq 45%

answer

  1. one slot, whole subtree
  2. subclass writes leak upward
  3. meaning depends on load order
  4. Style/ClassVars flags assignments
  5. class-level ivars plus a fallback

basics

~20 s

A @@ variable is one slot shared by a class and its whole subclass tree, so a subclass's write changes everyone's value, and load order can create conflicting copies. Use class-level instance variables behind def self. readers instead.

solid answer

~40 s

`@@` looks like per-class state but is shared by the class, its whole subclass tree and their instances, and a module's `@@` reaches every class that includes it. So `Circle` setting `@@defaults` rewrites `Shape`'s and `Square`'s too; which slot a name refers to depends on which file ran first, and since Ruby 3.0 conflicting copies raise `RuntimeError`. RuboCop's `Style/ClassVars` flags `@@` assignments by default (Standard turns it off). For per-class settings use **class-level instance variables** behind `def self.` readers: a `def self.defaults` reader that does `@defaults ||= superclass.defaults.dup` (with a base case in `Shape`) gives each subclass its own copy, seeded from the parent. Use constants for values that never change, and keep `@@` only for one value that is truly global to the tree.

code

yaml · 3 lines
yaml
# .rubocop.yml
Style/ClassVars:
  Enabled: true   # the RuboCop default; Standard disables this cop

go deeper

for a junior

Know that @@ is shared with subclasses, so a subclass can change the parent's value, and that class-level @ivars are the usual replacement.

for a middle

Show the subclass write leaking into the parent with a short example and write a def self. reader that falls back to superclass for defaults.

for a senior

Justify the replacement: copy-on-first-use vs merge-at-read semantics, shallow dup risks, the 3.0 overtaking error, and whether Style/ClassVars should be enforced.

for a principal

Set the policy for class-level configuration in a codebase: which settings may live on classes, how inheritance of settings works, and when an explicit config object replaces them.

## The problem in one example ```ruby class Shape @@defaults = {color: :black} def self.defaults = @@defaults end class Circle < Shape @@defaults = {color: :red} end Shape.defaults # => {color: :red} ``` The author of `Circle` meant "circles are red". Because `@@defaults` already existed in `Shape`, the assignment in `Circle` did not create a Circle-only variable; it **replaced Shape's**, and with it the value every other subclass reads. ## Why `@@` is avoided - **One slot for the whole tree.** A class variable is shared by the defining class, all subclasses and all their instances. Any subclass can change it for everyone, which is exactly wrong for settings that should differ per class. - **Modules leak it too.** A `@@` variable in a module is visible to every class that includes the module, which widens the audience further. - **Order-dependent meaning.** If a subclass creates `@@name` before its parent does, two variables exist. Since Ruby 3.0 the next access raises `RuntimeError` ("overtaken by"); before that, code quietly used different slots. - **Looks like an instance variable, behaves like a global.** Readers see `@@` inside a class and assume it is scoped to that class. - **Tooling pushes back.** RuboCop's `Style/ClassVars` cop, enabled by default, reports assignments to class variables and recommends class instance variables; Standard's configuration disables that cop. - **Concurrency.** A class variable cannot be accessed from a non-main Ractor at all, and shared mutable state under threads needs synchronisation, a topic of its own. ## What to use instead | Need | Use | |---|---| | A fixed value per class | a constant, frozen if it is a collection | | A per-class setting that subclasses may override | a class-level `@ivar` behind a `def self.` reader | | A per-class setting seeded from the parent | a reader that copies `superclass`'s value on first use | | One value truly shared by the whole tree | `@@`, or better a single explicit owner such as `Shape.registry` | The copy-on-first-use reader looks like this: ```ruby class Shape def self.defaults @defaults ||= superclass.respond_to?(:defaults) ? superclass.defaults.dup : {} end defaults[:color] = :black end class Circle < Shape defaults[:color] = :red end Shape.defaults # => {color: :black} Circle.defaults # => {color: :red} ``` How it behaves: 1. `Shape.defaults` finds no `@defaults` on Shape; `Object` has no `defaults` method, so it starts from `{}`. 2. `Circle.defaults` finds no `@defaults` on Circle, copies Shape's hash with `dup` and stores the copy in Circle's own `@defaults`. 3. Circle's change touches only its copy. Shape and any `Square` are unaffected. ## Trade-offs to state in an interview - The copy is taken **once**. A later change to `Shape.defaults` does not reach a subclass that already copied it. If you need live inheritance, have the reader merge at read time, such as `superclass.defaults.merge(@overrides || {})`. - `dup` is **shallow**: nested hashes are still shared, so copy deeper or freeze them if subclasses might mutate nested values. - Copying can also happen when a subclass is created, through the class-creation hook, which is a separate topic. - A single shared counter, such as a total count of all shapes, is the rare case where one shared slot is actually the requirement; even then, naming one owner (`Shape`) and exposing it through `Shape.total` keeps it readable. ## Why interviewers ask it The answer shows whether a candidate reasons about **who owns a piece of state**. A strong candidate explains the shared-slot behaviour with a concrete example, names the 3.0 overtaking error as a symptom of the same design, and offers class-level instance variables with an explicit inheritance rule rather than just saying "@@ is bad".

  • Is there any case where @@ is the right choice?
    When the requirement really is one value shared by a class and every subclass, such as a total count of all shapes, and you control every class in the tree. Even then, assign it once in the base class, read and write it only through `Shape.total`-style methods, and never assign it in subclasses, so the overtaking error cannot arise.
  • Why does the copy-on-first-use reader call dup on the parent's hash?
    Without `dup`, `Circle.defaults` would store a reference to the very same Hash object as `Shape`'s `@defaults`, and `Circle.defaults[:color] = :red` would mutate Shape's settings again, recreating the `@@` problem with extra steps. `dup` gives Circle its own top-level hash; nested objects inside it are still shared.

saying these in an interview costs you the question

  • A subclass that assigns @@defaults gets its own copy, leaving Shape's alone.
  • A class-level @defaults in Shape is automatically inherited by Circle.
  • Class variables are fine for per-class settings as long as names are unique.
  • Swapping @@defaults for a $defaults global solves the sharing problem.