Nicholas Erdenberger Nicholas Erdenberger - 16 days ago 5
Ruby Question

"Parent", "Child" Classes In Ruby (Not Inheritance)

Let's say I have two classes. One class, "parent", has many of another class "child". This is not inheritance, I don't want parent methods to act on child objects. What I want is the child object to be able to reference the parent object, get variables from it (child.parent.var) and call parent methods that modify the parent (

child.parent.update
).

I'd like one object (which could be thought of as a child-but-not-child-because-this-isn't-inheritance) to be passed a reference to another object when it is initialized. I'd compare this to a parent child relationship in a database where we store info on the parent so we don't have to duplicate it onto every child.

Example:

class Parent
attr_accessor :var

def initialize(num)
@var = num
end

def increase
@var += 1
end
end

class Child
attr_accessor :var, :parent

def initialize(parent, num)
@parent = parent
@var = num
end

def sum
@parent.increase
@parent.var + var
end
end

parent1 = Parent.new(1)

child1 = Child.new(parent1, 2)
child2 = Child.new(parent1, 3)

child1.parent.increase # update the parent variable
child2.parent.var # get the parent variable


The above code does work, but is there a better (more concise, or more ruby-esq) way to achieve this?

Thanks so much for your help/thoughts.

Answer

This is basically how it's supposed to be done :) There are a couple of possible improvements though, depending on what you actually want to achieve.

Right now, your Child instances expose access to the parent on their external interface (via the public parent accessor). This is often a violation of the Law of Demeter which states that objects should only talk to their direct neighbors. In this sense, the parent is a stranger when accessed though the child object.

You could improve your design by hiding the parent object:

class Child
  extend Forwardable

  def_delegator :@parent, :var, :parent_var
  def_delegator :@parent, :increase

  attr_accessor :var

  def initialize(parent, num)
    @parent = parent
    @var = num
  end

  def sum
    @parent.increase
    @parent.var + var
  end
end

Here, we use Ruby's Forwardable module to provide access to some methods of the parent from the client. This makes these methods part of the single public interface of your Child class.

parent = Parent.new(1)

child = Child.new(parent, 2)

child.var
# => 2
child.parent_var
# => 1
child.increase
# => 2
parent.var
# => 2
# ^^^^ the increase method was called on the parent object

From the outside, it doesn't matter that the methods are forwarded and you can later change this without affecting your external interface at all.


A second improvement could be to extend your Parent class to generate children directly:

class Parent
  # ...

  def child(num)
    Child.new(self, num)
  end
end

This is usually called a Factory Method, i.e. a method which builds other objects. With this, you can hide the complexity of building your Child objects and attaching them to your parent.

You can call it like

parent = Parent.new(1)
child = parent.child(2)