Created
November 4, 2012 20:42
-
-
Save chriskillpack/4013683 to your computer and use it in GitHub Desktop.
Exploring Ruby modules
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# My explorations into ruby modules and instance variables. | |
module TestModule | |
def tm_foo | |
@bar = 1 | |
end | |
def self.hello | |
'world' | |
end | |
end | |
class TestClass | |
include TestModule | |
def tc_foo | |
@bar | |
end | |
end | |
# When a class includes a module the modules instance methods are included | |
# directly onto the class, so instance variables in these methods are instance | |
# methods on the class. | |
tc = TestClass.new | |
tc.tc_foo # => nil | |
tc.tm_foo # => 1 | |
tc.tc_foo # => 1 | |
tc.instance_variable_get('@bar') # => 1 | |
# What about the module's class methods? | |
tc.tm_set_class_var # NoMethodError, inaccessible | |
# Instead, module class methods behave like the namespaced static methods: | |
TestModule.hello # => 'world' | |
# So that's instance methods, is it possible to add class methods via a module? | |
# Yes, but using the extend keyword instead: | |
class TestClassExtend | |
extend TestModule | |
end | |
TestClassExtend.tm_foo # => 1 | |
TestClassExtend.instance_variable_get('@foo') # => 1 | |
TestClassExtend.new.tm_foo # NoMethodError | |
# So the include keyword adds instance methods, and the extend keyword adds | |
# class methods, what if I wanted to add a mixture? | |
# Let's try the obvious: | |
class TestClassIncludeExtend | |
include TestModule | |
extend TestModule | |
end | |
TestClassExtend.tm_foo # => 1 | |
TestClassExtend.new.tm_foo # => 1 | |
# It works, but it's not very useful - why add the same methods as both class | |
# and instance. What if I want to add some methods as instance methods and | |
# others as class methods. We could split into separate modules: | |
module TestInstanceMethods | |
def tim_hello | |
"world" | |
end | |
end | |
module TestClassMethods | |
def tcm_greeting | |
"hello there!" | |
end | |
end | |
class TestClassSplitModule | |
include TestInstanceMethods | |
extend TestClassMethods | |
end | |
TestClassSplitModule.tcm_greeting # => "hello there!" | |
TestClassSplitModule.new.tim_hello # => "world" | |
# Hooray that works, but maintain two separate files is a bit of a pain. We | |
# can do better by making use of the handy included module function. If this | |
# class method is defined then Ruby will call it when a module is included: | |
module MyImprovementModule | |
def tim_hello | |
"world" | |
end | |
module ClassMethods | |
def tcm_greeting | |
"hello there!" | |
end | |
end | |
# klass is the class of the receiving class, which in the example below will | |
# be TestImprovedModule. | |
def self.included(klass) | |
klass.extend ClassMethods | |
end | |
end | |
class TestImprovedModule | |
include MyImprovementModule | |
end | |
TestImprovedModule.tcm_greeting # => "hello there!" | |
TestImprovedModule.new.tim_hello # => "world" | |
# When I first learned the pattern showed in the MyImprovementModule it brought | |
# home the dynamism of Ruby. include and extend are not priviledged keywords | |
# that get special treatment from the language in order to work their magic. | |
# Instead they are functions that can be applied dynamically as the situation | |
# requires. | |
module TestModule | |
def self.tm_set_class_var | |
@delta = 1 | |
end | |
def self.delta | |
@delta | |
end | |
end | |
TestModule.delta # => nil | |
TestModule.tm_set_class_var # => 1 | |
TestModule.delta # => 1 | |
TestModule.instance_variable_get('@delta') # => 1 | |
# This implies that modules can have their own state which is 'private' from the | |
# class. Very useful in avoiding naming collisions with instance variables | |
# in the receiving class. Write that one down. | |
# These modules instance variables, are they accessible from the receiving | |
# class? | |
TestModule.instance_variable_get('@delta') # => 1 | |
tc = TestClass.new | |
tc.instance_variable_get('@delta') # => nil, so no. | |
# What about module instance variables initialized in the module definition? | |
module TestModuleTwo | |
@cat = 2 | |
end | |
class TestClassTwo | |
include TestModuleTwo | |
def cat | |
@cat | |
end | |
end | |
TestClassTwo.new.cat # => nil | |
# Hmmm, weird. What happened? | |
# The answer, it's all about self. Each class has a set of instance variables | |
# and where they are stored depends on the value of self at the time of | |
# evaluation. | |
# Let's redefine TestModuleTwo, but this time annotate the value of self. | |
module TestModuleTwo | |
@cat = 2 # self is the module | |
def set_cow | |
@cow = 3 # self is the receiving class, because we don't evaluate this | |
# until we invoke the function. | |
end | |
def cow | |
@cow # again, self is the receiving class. | |
end | |
def self.cat | |
@cat | |
end | |
end | |
# Let's try again: | |
class TestClassTwo | |
include TestModuleTwo | |
end | |
tc = TestClassTwo.new | |
tc.instance_variable_get('@cat') # => nil | |
tc.cow # => nil | |
tc.set_cow # => 3 | |
TestModuleTwo.cat # => 2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment