When building a Ruby DSL, what does an RSpec-style shape, where each group block becomes the body of a new class, give you over Rake-style methods added to main?
answer
- Rake: self.extend Rake::DSL on main
- top-level def lands on Object
- RSpec: Class.new(parent) plus module_exec
- examples run on fresh instances
- nesting becomes subclassing
basics
~20 sRake extends the top-level main object with private DSL methods: flat files work, but helper defs land on Object. RSpec runs each group block as a Class.new subclass body via module_exec, so helpers are methods, nesting is inheritance and examples get fresh instances.
solid answer
~40 sRake's shape is **flat**: `rake/dsl_definition.rb` ends with `self.extend Rake::DSL`, adding `task`, `file` and friends as private methods of the top-level `main` object only, so a Rakefile is a list of calls. It is simple, but anything the file defines with `def` becomes a private method of `Object`, visible everywhere, and grouping happens only through namespace names. RSpec's shape is **class-based**: `describe` creates `Class.new(parent)` and runs the block with `module_exec`, so the block is a class body — `def helper` defines an instance method, nested groups are subclasses that inherit helpers, and each example block runs with `instance_exec` on a new instance, isolating state. For an alert-rule DSL, the flat shape fits a small rules file; the class shape fits groups that share helpers and need per-evaluation state.
go deeper
Recall that Rakefiles call task at the top level, while RSpec nests describe and it blocks.
Explain what self is in each shape: main extended with Rake::DSL versus a new class body per describe block.
Weigh helper leakage, nesting and per-evaluation isolation when choosing a shape for your own DSL, and their implementation cost.
Pick the DSL shape from how users will organise and share rules, and plan for readable errors with anonymous classes.
## Two proven Ruby DSL shapes Two widely used Ruby tools show two different ways to host a DSL. Both are plain Ruby; they differ in **what `self` is** when the user's code runs. ## The flat shape: methods on main (Rake) At the top level of a Ruby file, `self` is a special object called **main**. Rake's `dsl_definition.rb` ends with: ```ruby self.extend Rake::DSL ``` `extend` adds the module's methods to that one object, and `Rake::DSL` marks them **private**, so `task` can be called bare at the top level of a Rakefile but is not a public method of every object. A Rakefile is then a flat list of declarations. Consequences: - **Simple to write and read**: no blocks around the whole file, no classes. - **Helpers leak**: a `def` at the top level defines a private method on `Object`, so a helper written in the DSL file becomes callable from every object in the process. - **Scoping is by naming** only: grouping is done with strings or namespace calls, not with Ruby scopes. ## The class shape: blocks as class bodies (RSpec) rspec-core builds a class for each group. Its `ExampleGroup.subclass` does, in essence: 1. `subclass = Class.new(parent)` — a new anonymous class inheriting from the enclosing group. 2. `subclass.module_exec(&example_group_block)` — the `describe` block runs as that class's body. 3. Each `it` block is stored, and later run with `instance_exec` on a **new instance** of the group class. Consequences: - **Helpers are methods**: `def helper` inside a group defines an instance method of the group class, visible to its examples and nested groups, and nowhere else. - **Nesting is inheritance**: an inner group is a subclass, so it inherits helpers and can override them. - **Isolation**: each example gets a fresh object, so instance variables do not leak between examples. ## Comparing them | Concern | Flat (methods on main) | Class-based (block as class body) | |---|---|---| | Where DSL methods live | private singleton methods of main | class methods of the group class | | Where a user's `def` goes | private method of `Object`, global | instance method of that group | | Nesting | naming conventions | subclassing | | State per evaluation | shared: main and `Object` | one instance per evaluation | | Implementation cost | a module and one `extend` | class creation, registration, per-run instances | | Error readability | direct | a `Class.new` class has no name until assigned to a constant | ## Applying it to an alert-rule DSL - A tool that loads one `alerts.rb` of independent rules can use the **flat shape**: `alert "cpu_high" do ... end` calls at the top level of an evaluated file. - If users group rules by service, share helpers such as `business_hours?`, and override thresholds in sub-groups, the **class shape** fits: `Alerts.group "api" do ... end` creates a subclass per group, and helpers defined in a group stay inside it. - A class made with `Class.new` has no permanent name until it is assigned to a constant, and Ruby 3.4+ prints a class name in backtrace labels only when it has one. rspec-core assigns each group a constant under `RSpec::ExampleGroups` for that reason; a class-shaped DSL should name its groups too, or at least put the group's description in its own error messages. ## What the question checks This is a design question: can the candidate describe how a familiar DSL is implemented in plain Ruby, and pick a shape from the user's needs rather than from taste?
- Why does a helper defined with def in a Rakefile become callable on unrelated objects?A `def` at the top level of a Ruby file defines a private instance method of `Object`. Every object inherits from `Object`, so the helper can be called without a receiver from inside any method in the process, which is why flat-shape DSL files should keep helpers in a module.
- In the class shape, why do examples not share instance variables?Each example block is run with `instance_exec` on a new instance of its group class. Instance variables set in one example live on that instance, which is discarded before the next example runs.
saying these in an interview costs you the question
- Rake adds task as a public method to every object via Object.include.
- A def in a Rakefile stays local to that file.
- RSpec runs every describe block with instance_eval on one shared object.
- Nested describe groups share one class and cannot override helpers.