Search code examples
rubyrubocop

Why shouldn't I extend an instance initialized by Struct.new?


We have a legacy codebase where rubocop reports some of these errors which I never could wrap my head around:

Don't extend an instance initialized by Struct.new. Extending it introduces a superfluous class level and may also introduce weird errors if the file is required multiple times.

What exactly is meant by "a superfluous class level" and what kind of "weird errors" may be introduced?

(Asking because obviously we didn't have any such problems over the last years.)


Solution

  • Struct.new creates an anonymous class that happens to be a subclass of Struct:

    s = Struct.new(:foo)
    #=> #<Class:0x00007fdbc21a0270>
    
    s.ancestors
    #=> [#<Class:0x00007fdbc21a0270>, Struct, Enumerable, Object, Kernel, BasicObject]
    

    You can assign that anonymous class to a constant in order to name it:

    Foo = Struct.new(:foo)
    #=> Foo
    
    Foo.ancestors
    #=> [Foo, Struct, Enumerable, Object, Kernel, BasicObject]
    

    That's the regular way to create a Struct subclass.

    Your legacy code on the other hand seems to contain something like this:

    class Foo < Struct.new(:foo)
    end
    

    Struct.new creates an anonymous class (it's not assigned to a constant) and Foo subclasses it, which results in:

    Foo.ancestors
    #=> [Foo, #<Class:0x00007fee94191f38>, Struct, Enumerable, Object, Kernel, BasicObject]
    

    Apparently, the anonymous class doesn't serve any purpose.

    It's like:

    class Bar
    end
    
    class Foo < Bar   # or Foo = Class.new(Bar)
    end
    
    Foo.ancestors
    #=> [Foo, Bar, Object, Kernel, BasicObject]
    

    as opposed to:

    class Bar
    end
    
    class Foo < Class.new(Bar)
    end
    
    Foo.ancestors
    #=> [Foo, #<Class:0x00007fdb870e7198>, Bar, Object, Kernel, BasicObject]
    

    The anonymous class returned by Class.new(Bar) in the latter example is not assigned to a constant and therefore neither used nor needed.