Reputation: 141
I'm confused about the usage of modules in ruby. Specifically, when modules are mixed into classes, how should the object instance variables / methods be accessed in the module. Consider the following example.
class A
include B
def initialize(my_var)
@my_var = my_var
@result = nil
end
def get_result
@result = foo
end
end
module B
def foo
"Result of #{@my_var}"
end
end
In the above example, module B is designed in a way that, it expects encapsulating object to define variable my_var. Is this is a good design? It does not feel right because module B is, in this case, somehow dependent on class A. Another way to construct module B would be something like this.
module B
def foo(my_var)
"Result of #{my_var}"
end
end
In this version, the method takes an argument and performs the necessary computation and returns a value. This is much more cleaner but this is perhaps a contrived example, for in many real world scenarios, things are much more complicated.
Also, it is not just about instance variables. I think the same question applies to instance methods as well. Can we construct a module expecting that the encapsulating object has certain methods and/or instance variables. What is the right way to design a solution in such cases?
I have looked at several stackoverflow threads closely related to this question but could not find a satisfactory answer. Thanks in advance for all your answers
Upvotes: 2
Views: 542
Reputation: 369498
In your second example, foo
isn't really a method, it's a procedure. What distinguishes a method is that it has privileged access to the receiver, i.e. self
. But your second foo
doesn't make any use of that! So, why make it a method at all? You could do the same thing with a procedure in BASIC.
It's perfectly fine to access self
in a method, that's what methods are there for.
There is no way in Ruby itself to declare what exactly a mixin expects of self
to do its work, except documentation, though. But it is quite common: Comparable
expects self
to respond to <=>
, Enumerable
expects self
to respond to each
, for example.
I call these "leverage mixins", because they act like a lever: you only need to provide a small force yourself (a single each
method), but they allow you to do a lot of heavy lifting.
It's not much different from other sorts of expectations in Ruby. Array#join(sep)
expects sep
to respond to to_str
and the array elements to respond to to_s
, Array#[](idx)
expects idx
to respond to to_int
. The Range
membership test methods expect the left element to respond to <=>
, the Range
iteration methods (each
, to_a
, step
, …) expect the left element to respond to succ
. Enumerable#sort
expects the elements of the enumerable to respond to <=>
and so on and so forth.
Placing expectations on self
is not much different than placing them on any other object.
These "expectations" are typically called "protocols". Objects conform to protocols (the sets of messages they support and how they react to them), and depend on protocols of other objects. For example, you could say that the Enumerable
mixin depends on the Iteration
protocol. However, there is no construct in the Ruby language for expressing such protocols other than simple documentation.
However, it is in general not very OO to use instance variables in Ruby. OO is about messaging, but Ruby only does messaging for methods, not for variables (unlike Self, for example, where instance variables are looked up via messages), therefore you should prefer methods over variables. If you are concerned about a getter leaking information or a setter breaking some invariant, make them private
.
Upvotes: 2
Reputation: 8424
I think this book will answer a lot of questions for you: Practical Object-Oriented Design in Ruby.
Regarding instance variables, no, it's not a good idea to use them in modules. Modules are expected to extract common behavior and are usually mixed-in in more than 1 classes (and the more classes you include them in, the bigger the chances are that you'll have instance variable name collision. A better way (mentioned in the above book) is something like this:
module B
def foo
"Result of #{my_var}"
end
end
class A
include B
attr_reader :my_var
def initialize(my_var)
@my_var = my_var
end
end
If you don't want your programs to crash in case, for example, my_var
is not a method in the mixed-in class, you can do something like this to check for the method's existence:
def foo
puts 'hi' if respond_to?(:my_var)
end
Upvotes: 0
Reputation: 37409
This is simply another aspect of ruby being duck-typed.
Just like when you pass parameters to a method, and method assumes the parameters respond to some methods, a module may assume stuff about the including class.
Upvotes: 1