topics #fedora #git #linux #ruby

Learn Ruby: 5. Object-Oriented Programming

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