Inheritance
Inheritance is an OOP mechanism that lets you define a new class based on an existing (parent) class. The child class gets all the properties and methods of the parent.
Imagine we have a Browser class. Every browser can open and close tabs, display info about itself, and has a version, name, and platform:
class Browser
attr_accessor :name, :version, :works_on
def initialize(name = 'Browser')
@name = name
@version = 1.0
@works_on = %w(Linux Windows MacOS)
end
def open_tab
puts "Open {name} tab"
end
def close_tab
puts "Close {name} tab"
end
def about
puts "{name}: {version}"
end
end
We can create a Firefox class that inherits from Browser:
class Firefox < Browser
def initialize
@name = 'Firefox'
@version = 92.0
end
end
ff = Firefox.new
ff.open_tab # => Open Firefox tab
ff.close_tab # => Close Firefox tab
ff.name # => Firefox
ff.version # => 92.0
ff.works_on # => (nil — not set!)
super
works_on is set in Browser#initialize, which doesn't run when we call Firefox.new. To call the parent's initialize, use super:
class Firefox < Browser
def initialize
super('Firefox')
@version = 92.0
end
def group_tabs
puts "Group tabs in {name}"
end
end
ff = Firefox.new
ff.name # => Firefox
ff.version # => 92.0
ff.works_on # => Linux Windows MacOS
ff.group_tabs # => Group tabs in Firefox
public
All methods declared in a class are public by default — they can be called directly from outside:
ff = Firefox.new
ff.about # => Firefox: 92.0
private
A private method can only be called from within the same class or a subclass's public method — not from outside:
class Browser
# ...
def show_history
full_history[:publ]
end
protected
def full_history
history
end
private
def history
{
publ: %w(github.com google.com),
priv: %w(elixir-lang.org)
}
end
end
Calling a private method directly raises an error:
ff.history
# => NoMethodError: private method 'history' called
Instead, expose it through a public method:
ff.show_history
# => github.com
# google.com
You can technically bypass this with send, but you shouldn't:
ff.send(:history)
# => {:publ=>["github.com", "google.com"], :priv=>["elixir-lang.org"]}
protected
A protected method works like public within the class hierarchy, but cannot be called from outside. It can be called on other instances of the same class:
class Firefox < Browser
def call_protected_method
Firefox.new.full_history # works
end
def call_private_method
Firefox.new.history # raises NoMethodError
end
end
ff.call_protected_method
# => {:publ=>["github.com", "google.com"], :priv=>["elixir-lang.org"]}
Anonymous Classes
klass = Class.new do
def hello
puts "Hello from anonymous class"
end
end
klass.new.hello
# => Hello from anonymous class
Singleton Methods
You can add a method to a specific object instance without affecting other instances:
class Temp; end
temp1 = Temp.new
temp2 = Temp.new
def temp1.hello
puts "Hello from temp1"
end
temp1.hello # => Hello from temp1
temp2.hello # => NoMethodError
Same idea works on plain Object:
temp = Object.new
def temp.hello
puts "Hello from Object"
end
temp.hello # => Hello from Object
Class Methods
Class methods are called directly on the class, not on instances:
class MyClass
def self.command1
puts 'Command 1'
end
def MyClass.command2
puts 'Command 2'
end
class << self
def command3
puts 'Command 3'
end
end
end
MyClass.command1 # => Command 1
MyClass.command2 # => Command 2
MyClass.command3 # => Command 3
Class variables use @@ (rarely used in practice):
class MyClass
@@age = 28
@@name = 'zvlex'
def info_1
puts @@age
puts @@name
end
def self.info_2
puts @@age
puts @@name
end
end
MyClass.new.info_1 # => 28, zvlex
MyClass.info_2 # => 28, zvlex
Modules
Ruby doesn't support multiple inheritance, so we use Mixins — modules — instead.
module Runner
def run_command(com)
puts "Run command: {com}"
end
def run_setup
puts "Running setup"
end
end
To use the module's methods, include it in a class:
class MySystem
include Runner
end
sys = MySystem.new
sys.run_setup # => Running setup
sys.run_command('ls') # => Run command: ls
Modules can include other modules:
module A
def a; puts 1; end
end
module B
def b; puts 2; end
end
module C
include A
include B
def c; puts 3; end
end
class My
include C
end
my = My.new
my.a # => 1
my.b # => 2
my.c # => 3
Modules can also define attributes:
module Commands
attr_accessor :cmds
def setup
@cmds = []
end
def add(cmd)
@cmds << cmd
end
end
class My
include Commands
end
my = My.new
my.setup
my.add('ls')
my.add('pwd')
puts my.cmds # => ls, pwd
Anonymous Modules
m = Module.new do
def hello
puts "Hello from anonymous module"
end
end
z = Object.new
z.extend(m)
z.hello # => Hello from anonymous module
include vs prepend vs extend
include inserts the module after the class in the ancestor chain. prepend inserts it before the class. extend adds module methods as class methods:
module A
def hello; puts "Hello from A"; end
end
module B
def hello; puts "Hello from B"; end
end
class C
include A
include B
end
class D
prepend A
include B
end
C.ancestors # => [C, B, A, Object, ...]
D.ancestors # => [A, D, B, Object, ...]
C.new.hello # => Hello from B
D.new.hello # => Hello from A
module Commands
def pwd; puts "pwd"; end
end
class My
extend Commands
end
My.pwd # => pwd (class method)
self
self refers to the current object in context:
def return_self
self
end
return_self # => main
class A
def return_self
self
end
end
A.new.return_self # => #
class A
def some_method
hello = "var hello"
puts hello # => var hello
puts self.hello # => method hello
end
private
def hello
"method hello"
end
end
class B
def self.some_method
puts "Hello {self}"
end
end
B.some_method # => Hello B