Forum: Ruby OO design: avoiding an infinite loop in classes initializing each other?

D1293dba7ab354e671044619fc1a00d7?d=identicon&s=25 Andrew S. (andrew_s)
on 2013-09-01 19:53
I have two classes, each of which use methods from the other class.  But
because they initialize an instance of each other, this creates an
infinite loop!

I would like to keep them separate and not merge them.  What's the best
/ most elegant / proper way of solving this problem?  There is probably
something simple that I'm just not seeing.

Simplified code:

class A
  def initialize
    b = B.new
  end

  def method_foo
    # do something with b
  end
end

class B
  def initialize
    a = A.new
  end

  def method_bar
    # do something with a
  end
end

Any help gratefully received!
0e6ac58dab6125c1cd2e7ac645076b6f?d=identicon&s=25 Joel VanderWerf (Guest)
on 2013-09-01 21:48
(Received via mailing list)
On 09/01/2013 10:53 AM, Andrew S. wrote:
> class A
>    def initialize
>      a = A.new
>    end
>
>    def method_bar
>      # do something with a
>    end
> end
>
> Any help gratefully received!
>

class A
   def initialize b = nil
     @b = b || B.new(self)
   end
end

class B
   def initialize a = nil
     @a = a || A.new(self)
   end
end

a = A.new

p a  # #<A:0x21dbf9e0 @b=#<B:0x21dbf9cc @a=#<A:0x21dbf9e0 ...>>>
D1293dba7ab354e671044619fc1a00d7?d=identicon&s=25 Andrew S. (andrew_s)
on 2013-09-02 00:25
Hi,

Thanks for replying!  Well, I've tested it and it works (see extended
example below).  But I don't know why it works!

Why set the arguments to nil?  Why does  @b = b || B.new(self) work?

Extended example:

class A
  def initialize b = nil
    @b = b || B.new(self)
  end

  def do_something_with_b
    @b.say_bar
  end

  def say_foo
    puts "foo"
  end
end

class B
  def initialize a = nil
    @a = a || A.new(self)
  end

  def do_something_with_a
    @a.say_foo
  end

  def say_bar
    puts "bar"
  end
end

a = A.new
b = B.new

b.do_something_with_a # => foo
a.do_something_with_b # => bar
0e6ac58dab6125c1cd2e7ac645076b6f?d=identicon&s=25 Joel VanderWerf (Guest)
on 2013-09-02 01:31
(Received via mailing list)
On 09/01/2013 03:25 PM, Andrew S. wrote:
> Hi,
>
> Thanks for replying!  Well, I've tested it and it works (see extended
> example below).  But I don't know why it works!
>
> Why set the arguments to nil?  Why does  @b = b || B.new(self) work?

The `b = nil` means that b is optional. If it is not passed by the
caller (as in: `A.new`), then the initial value of b is nil. If an
argument is provided by the caller (as in: `A.new(self)`), then that
value is assigned to b.

In those two cases, this line

       @b = b || B.new(self)

will behave differently.

If b was passed in as an argument to A.new, then the effect is

       @b = b

If not, then the effect is

       @b = B.new(self)

Having these two cases is what makes it possible to set up the circular
reference that you wanted. When you construct an A like so:

       a = A.new

there is no argument. So A.new calls B.new. The trick is that it calls
B.new with an argument: B.new(self). Since an argument is passed to
B.new, it simply assigns that argument (an instance of A) to its @a
attribute.

Hope that helps...
D1293dba7ab354e671044619fc1a00d7?d=identicon&s=25 Andrew S. (andrew_s)
on 2013-09-02 04:48
I think I understand.  Thank you very much!
2ffac40f8a985a2b2749244b8a1c4161?d=identicon&s=25 Mike Stok (Guest)
on 2013-09-02 17:38
(Received via mailing list)
On 2013-09-02, at 12:31 AM, Joel VanderWerf <joelvanderwerf@gmail.com>
wrote:

> In those two cases, this line
>
>      @b = B.new(self)
>
> Having these two cases is what makes it possible to set up the circular
reference that you wanted. When you construct an A like so:
>
>      a = A.new
>
> there is no argument. So A.new calls B.new. The trick is that it calls B.new
with an argument: B.new(self). Since an argument is passed to B.new, it simply
assigns that argument (an instance of A) to its @a attribute.
>
> Hope that helps...

Why not let Ruby do the defaulting for you? For example:

#!/usr/bin/env ruby

class A
 def initialize(b = B.new(self))
   @b = b
 end

 def method_foo
   # do something with b
 end
end

class B
 def initialize(a = A.new(self))
   @a = a
 end

 def method_bar
   # do something with a
 end
end


a = A.new
p a
b = B.new
p b

__END__

produces the right results.

ratdog:tmp mike$ ./try.rb
#<A:0x007fafc4128b50 @b=#<B:0x007fafc4128b28 @a=#<A:0x007fafc4128b50
...>>>
#<B:0x007fafc4128970 @a=#<A:0x007fafc4128948 @b=#<B:0x007fafc4128970
...>>>

Is this bad form?

Mike

--

Mike Stok <mike@stok.ca>
http://www.stok.ca/~mike/

The "`Stok' disclaimers" apply.
Aa082c8b00a50928e5860dcd70bf2368?d=identicon&s=25 tamouse m. (tamouse_m)
on 2013-09-02 20:14
(Received via mailing list)
On Sep 1, 2013, at 12:53 PM, Andrew S. <lists@ruby-forum.com> wrote:

> I have two classes, each of which use methods from the other class.  But
> because they initialize an instance of each other, this creates an
> infinite loop!
>
> I would like to keep them separate and not merge them.  What's the best
> / most elegant / proper way of solving this problem?  There is probably
> something simple that I'm just not seeing.

I obviously don't know the specifics of what you're doing, but I have to
say the general pattern has a distinct odour to it. This smacks of
too-tightly coupled classes, and probably low cohesion. I'd seriously
consider refactoring this, and thinking it out a bit further so your
classes are more independent.
E0d864d9677f3c1482a20152b7cac0e2?d=identicon&s=25 Robert Klemme (robert_k78)
on 2013-09-02 20:17
(Received via mailing list)
On Mon, Sep 2, 2013 at 8:14 PM, Tamara Temple <tamouse.lists@gmail.com>
wrote:
>
> I obviously don't know the specifics of what you're doing, but I have to say the
general pattern has a distinct odour to it. This smacks of too-tightly coupled
classes, and probably low cohesion. I'd seriously consider refactoring this, and
thinking it out a bit further so your classes are more independent.

... or at least do not employ such a circular dependency for
construction.

Kind regards

robert
0e6ac58dab6125c1cd2e7ac645076b6f?d=identicon&s=25 Joel VanderWerf (Guest)
on 2013-09-02 23:41
(Received via mailing list)
On 09/02/2013 08:37 AM, Mike Stok wrote:
> Why not let Ruby do the defaulting for you? For example:
>
> #!/usr/bin/env ruby
>
> class A
>   def initialize(b = B.new(self))
>     @b = b
>   end
...
> produces the right results.

Even better! However, it is More Magic....
0e6ac58dab6125c1cd2e7ac645076b6f?d=identicon&s=25 Joel VanderWerf (Guest)
on 2013-09-02 23:51
(Received via mailing list)
On 09/02/2013 11:14 AM, Tamara Temple wrote:
>
> I obviously don't know the specifics of what you're doing, but I have to say the
general pattern has a distinct odour to it. This smacks of too-tightly coupled
classes, and probably low cohesion. I'd seriously consider refactoring this, and
thinking it out a bit further so your classes are more independent.
>

A little "dependency injection" to decouple your classes...

class A
   attr_reader :container

   def initialize container
     @container = container
   end

   def b
     container.b
   end

   def do_something_with_b
     b.say_bar
   end

   def say_foo
     puts "foo"
   end
end

class B
   attr_reader :container

   def initialize container
     @container = container
   end

   def a
     container.a
   end

   def do_something_with_a
     a.say_foo
   end

   def say_bar
     puts "bar"
   end
end

class MyContainer
   def a
     @a ||= A.new(self)
   end

   def b
     @b ||= B.new(self)
   end
end

mc = MyContainer.new

mc.a.do_something_with_b # ==> bar
mc.b.do_something_with_a # ==> foo
Please log in before posting. Registration is free and takes only a minute.
Existing account

NEW: Do you have a Google/GoogleMail, Yahoo or Facebook account? No registration required!
Log in with Google account | Log in with Yahoo account | Log in with Facebook account
No account? Register here.