Monday, January 17, 2011

Code reuse in Ruby, why composition is so rare?

I've been researching topics related to code reuse and object oriented design in Ruby apps recently.

There are three popular ways of reusing code in the Ruby community:

  1. Inheritance
  2. Modules/mixins
  3. Composition
1. Inheritance

class User < ActiveRecord::Base
  def change_login(new_login)
    login = new_login
    save
  end
end
It's very popular, mostly because this is the default way you can use ActiveRecord in Rails applications. To be honest, I don't like inheritance. It doesn't fit into my model of thinking about object oriented design. That's a topic for another post, though.
Inheritance let's us access methods available in the base class. The limitation is that a class can only inherit from one base class. Python is better here, allowing multiple inheritance. I don't like inheritance, both from a theoretical and practical point of view.
2. Modules/mixins
module Buyer
  def add_to_cart(product_id, quantity)
    cart.add(find_product(product_id), quantity))
  end
end

class User
  include Buyer
end

#usage
User.new.add_to_cart(params[:product_id, params[:quantity])
A module is basically a set of methods. You can't instantiate a module and that's the main reason I'm not a big fan of modules. 
You can include as many modules as you want into one class. That's kind of a work around for the lack of multiple inheritance. Modules are extremely popular in the Ruby community - just look at the Rails source code.
One problem with modules is that in order to unit test them you either test every single method or you need to create a dummy class that includes the module and then you test an object of this class. 
Another problem (in my opinion) is that modules introduce hidden dependencies. You can't easily create an object and replace the module with a mock in order to test the collaboration.
The good thing about modules is that it's very easy to use. When you see a common set of methods in two classes, you extract it to a module and the duplication disappears.
My biggest concern with modules is that they break the concept of "Everything is an object", so the theory doesn't fit well with my understanding of object orientation. The practical usage however is very useful, you just extract methods to a module and then you can include it in many classes.
3. Composition
class User
  attr_accessor :buyer
  
  def add_to_cart(product_id, quantity)
    buyer.add_to_cart(product_id, quantity)
  end
end
Composition feels most right to me.
You can compose many objects in one place and delegate the work that you specify. Composition uses classes/objects so you can easily test all the classes separately.
When you want to test a class that uses composition you can easily replace one of the collaborators with a mock and just test the communication.
Composition is perfect for a good OO design, but it's very rare to see it in Ruby/Rails projects and here is my question: Is composition so rare because it's hard to use or is there any other reason?

Update: Added code examples, changed modules to modules/mixins

11 comments:

Anonymous said...

by 'modules' i think you mean 'mixin'

Anonymous said...

by 'modules' i think you mean 'mixin'

Anonymous said...

(2) is called 'mixin'

Postmodern said...

You can infact instantiate a Module:

m = Module.new { def hello; 'world'; end }
m.class # => Module
m.class.class # => Class

include m
hello # => 'world'

Iain said...

Modules/Mixins are actually Ruby's way of doing multiple inheritance, but without the diamond problem.

The fact that they cannot be instantiated, doesn't mean it breaks the concept of "everything being an object", since a module is still an object. It's not "everything is a class", but "everything is an object". Modules are objects, not classes. But classes are objects. Still following? It might not fit well with your understanding of OO, but it fit very cleanly in the Ruby Object Model.

Thirdly, Rails has support for composition/aggregation, but the syntax is pretty weak. See the method composed_of. I usually go with a DIY approach.

I think the conclusion is that every language looks at OO through its own colored glasses. Some concepts of OO are more natural to one language than to the other. Ruby's mixin's are very powerful, so it's natural to see a tendency towards that, which leaves composition underused.

Andrzej Krzywda said...

Iain:

Thanks for your comment.

"It's not "everything is a class", but "everything is an object". Modules are objects, not classes."

I wasn't very clear with my explanation, what I meant was that you can't easily test a module in isolation because there's no such thing as module instance.

Using Postmodern's example, I can't say:
m = Module.new { def hello; 'world'; end }
assert_equal m.hello, "world"

"Ruby's mixin's are very powerful, so it's natural to see a tendency towards that, which leaves composition underused."

This is exactly the answer I was looking for. I think you're right that's the reason for composition being so unpopular.

I use modules and they help a lot. It's a bit hard to explain but the problem I have with modules is that they create a dependency that's a bit hard to "unwire".

I would like to move some logic to a module but when I use the base class in tests I would like to provide a mock, so that the real methods of the module are not executed (for example because they require internet connection or db access).
It's a bit hard to do so with modules. Maybe I'm missing something?

francois said...

Given your last example, how is the Buyer class to know how to store data in the DB, without accepting some kind of User or user_id?

If you send along self, how is that better than using a module?

Andrzej Krzywda said...

francois:

I ignored the db issue for the sake of simplicity.

I would need to pass the db object somehow. With ActiveRecord I need to pass self (that's the problem with inheritance in AR). You're right that in this case it's less valuable.

Anonymous said...

A little bit of a workaround, but you can do

m = Module.new { def hello; 'world'; end }
m.extend(m)
assert_equal m.hello, "world"

Andrzej Krzywda said...

m.extend(m)

nice :) thanks!

fuzzyman said...

Isn't part of the issue that even with composition you still have to write all the delegation code - something you just don't have to do with inheritance.