There are three popular ways of reusing code in the Ruby community:
- Inheritance
- Modules/mixins
- 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
Update: Added code examples, changed modules to modules/mixins
11 comments:
by 'modules' i think you mean 'mixin'
by 'modules' i think you mean 'mixin'
(2) is called 'mixin'
You can infact instantiate a Module:
m = Module.new { def hello; 'world'; end }
m.class # => Module
m.class.class # => Class
include m
hello # => 'world'
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.
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?
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?
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.
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"
m.extend(m)
nice :) thanks!
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.
Post a Comment