Karthik Mallavarapu
Karthik Mallavarapu

Reputation: 141

Ruby Mixin dependency on Objects

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

Answers (3)

Jörg W Mittag
Jörg W Mittag

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

daremkd
daremkd

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

Uri Agassi
Uri Agassi

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

Related Questions