Inheritance and Constructors


#1

In Crystal, my experience using initialize is that it obstructs all superclass constructors:

class Mammal
  property name = "unnamed"
  
  def initialize(name)
    @name = name
  end
end

class Dog < Mammal
  getter breed = "unknown"
  
  def initialize(name, breed)
    @name = name
    @breed = breed
  end
end

sadie = Dog.new("Sadie")
Error in line 18: wrong number of arguments for 'Dog.new' (given 1, expected 2)
Overloads are:
 - Dog.new(name, breed)

That seems like reasonable compiler behavior, but what if I want to avoid repeating myself (e.g. if there are several shared constructors across several subclasses)? There are a couple options.

I could use the inherited macro:

class Mammal
  property name = "unnamed"
  
  macro inherited
    def initialize(name)
      @name = name
    end
  end
end

class Dog < Mammal
  getter breed = "unknown"
  
  def initialize(name, breed)
    @name = name
    @breed = breed
  end
end

sadie = Dog.new("Sadie")

I could use self.new in the subclass instead of initialize:

class Mammal
  property name = "unnamed"
  
  def initialize(name)
    @name = name
  end
end

class Dog < Mammal
  getter breed = "unknown"
  
  def self.new(name, breed)
    self.name = name
    self.breed = breed
  end
end

sadie = Dog.new("Sadie")

I could add an extra, catch-all initialize:

class Mammal
  property name = "unnamed"
  
  def initialize(name)
    @name = name
  end
end

class Dog < Mammal
  getter breed = "unknown"
  
  def initialize(name, breed)
    @name = name
    @breed = breed
  end
  
  def initialize(*args)
    super(*args)
  end
end

sadie = Dog.new("Sadie")

There may very well be other options. Among the aforementioned choices (and any others that you may know of), is there a “best” solution to this problem? I could certainly just repeat code on every subclass, but that violates DRY, so I’d like to avoid that. Also, please assume that my use case necessitates that at least one of the subclasses must have a new constructor.

Postscript: I know is isn’t really asking for help with a problem, but this seemed like the appropriate category. Please move it if that is not the case.


#2

If your hierarchy of types has no rule that states “all objects should be constructed with just a name” I think you will end up providing a constructor for each subclass.

In your example, the new attributes have already a default value. That is not always the case, but if that holds on your hierarchy I see two options:

  • do not define other constructors and use properties to set the extended state like #breed=.
    • This will give you the DRY feeling.
  • manually provide a def initialize(name) together with the custom constructor that will otherwise hide that one.
    • I don’t think it violates the DRY since there is no constraint how each object should be built, but each constructor needs to fully initialize all ivars.

I would normally do the later. Unless there are some other design constraints.

class Mammal
  property name : String 
  
  def initialize(@name = "unnamed")
  end
end

class Dog < Mammal
  getter breed : String
  
  def initialize(name, @breed = "unknown")
    super(name)
  end
end

sadie = Dog.new("Sadie")

or

class Dog < Mammal
  getter breed : String

  def initialize(name)
    initialize(name, "Unkown")
  end

  def initialize(name, @breed)
    super(name)
  end
end