On Tue, Nov 2, 2010 at 9:29 PM, Jeremy Bopp <jeremy / bopp.net> wrote:
> On 11/2/2010 3:01 PM, Manuel Kiessling wrote:
>> Hello everyone,
>>
>> I'm quite new to Ruby, with a strong background in PHP.
>>
>> I had a great time finding my way around in Ruby, because it is a fantas=
tic language. However, right now I'm a bit stuck regarding the issue of loo=
se coupling / inversion of control / writing testable units.
>>
>> I spent the last days reading a lot of articles, but feel sort of puzzle=
d now.
>>
>> As far as I can see, there are several approaches. I will start with my =
own, which probably is the weirdest of all - feel free to dissect it.
>>
>> Let's say I have 2 classes, SomeClass and OtherClass, and SomeClass depe=
nds on OtherClass, because in some cases it needs to create an instance of =
this class, and in others it doesn't, which is why we can't simply pass an =
already created instance of OtherClass into SomeClass.
>>
>> This is how it could be done using no DI at all:
>>
>> class SomeClass
>> =A0 def do_something(a, b)
>> =A0 =A0 c =3D a + b
>> =A0 =A0 if (c > 5) then
>> =A0 =A0 =A0 o =3D OtherClass.new()
>> =A0 =A0 =A0 o.log(c)
>> =A0 =A0 end
>> =A0 end
>> end
>>
>> s =3D SomeClass.new
>> s.do_something(1, 2)
>>
>> Which of course doesn't allow me to replace "o" with a mock object for t=
esting etc.
>>
>>
>> This is how it might be done using the fact that ruby classes are object=
s (my approach):
>>
>> class SomeClass
>> =A0 def do_something(a, b, logObjectClass)
>> =A0 =A0 c =3D a + b
>> =A0 =A0 if (c > 5) then
>> =A0 =A0 =A0 o =3D logObjectClass.new()
>> =A0 =A0 =A0 o.log(c)
>> =A0 =A0 end
>> =A0 end
>> end
>>
>> s =3D SomeClass.new
>> s.do_something(1, 2, OtherClass)
>>
>> This way, I can inject into SomeClass which class to actually use - as l=
ong as whatever I pass using logObjectClass supports the expected protocol,=
 all should be fine. I could easily inject a mock log object for my tests.
>>
>> But, I couldn't find any articles recommending this approach, and I gues=
s there's a reason for that...

Certainly.  But whether this reason also means that your approach is
bad is another question.  Personally I find it totally valid and
natural to use a class object as /factory/ for instances.  The other
approach, which might be a bit more Ruby like, is to use a block:

class SomeClass
  # block as factory
  def do_something_1(a, b, &log)
    c =3D a + b
    if (c > 5) then
      o =3D log.call
      o.log(c)
    end
  end

  # block as callback
  def do_something_2(a, b, &log)
    c =3D a + b
    if (c > 5) then
      log.call(c)
    end
  end
end

sc.do_something_1(1,2) { OtherClass.new }
sc.do_something_2(1,2) {|item| OtherClass.new.log(item) }

Btw, if OtherClass is some kind of logger I would probably make the
factory a member of SomeClass, so you do not have to pass it into
every call.

> What I'm about to suggest will generate warnings from Ruby and is
> probably not the "best" way, but you could also replace OtherClass with
> your MockLoggerClass with a simple assignment during your test:
>
> OtherClass =3D MockLoggerClass =A0# This generates a warning...
> s =3D SomeClass.new
> s.do_something(1, 2)

This is not exactly the same as it does not allow control over the
created instance per instance.  You basically change global state vs.
Manuel's approach with local state.

> Ruby will warn you about replacing an already initialized constant,
> namely OtherClass, but it should get the job done with a minimum of fuss
> on your part.
>
> One more option would be to override OtherClass.new and make it simply
> call MockLoggerClass.new with the same arguments:
>
> def OtherClass.new(*args)
> =A0MockLoggerClass.new(*args)
> end
>
> s =3D SomeClass.new
> s.do_something(1, 2)

Same problem: global vs. local state.

Kind regards

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/