Featured image of post Object Oriented Ruby

Object Oriented Ruby

Learn how to separate files on Ruby app

Everything is an Object

There are relatively few primitive and many things in Ruby are expressed in terms of objects and methods.

An object is a collection of data and methods. Methods return the object’s data or manipulate it.

Ruby is dynamic typing. Variable can refer to objects of different types.

Terms

The terms related to Object Oriented in Ruby

  • Instance Variable
  • Instance Method
  • Object Initialization
  • Destruction
  • Accessor
  • Virtual Attributes
  • Class Method
  • Class Variable

Instance Variable @

We are setting an instance variable called title. Instance variable in Ruby are denoted with @. They are called instance variable because each instance of the book class is going to have its own copy of title string.

1
2
3
4
5
class Book
  def title=(s)
    @title = s
  end
end

Before we call title= method, the title variable doesn’t exist in the object. The equal sign in the method name may look unusual coming from other languages. It’s there to indicate that the method is a write accessor and to allow you to write b.title = the name of the book

Let create a book instance b = Book.new and put new title b.title = "How to Code". There should be no problem. But when we try to access the title by run b.title. It will produce an error.

new is so-called constructor, and it is and example of a class method. We can think of a new method as a factory which stamps out an object based on the blueprint defined by a class.

Open Classes

Now we’re going to add a class to read book title by running

1
2
3
4
5
class Book
  def title
    @title
  end
end

This feature is called Open Classes.

Accessor

Writing all the read and write accessor methods (getter and setter) like previous manually would be quite an exercise in typing. Fortunately, we have a convenient alternative. We can use attr_accessor to provide read and write accessors for an instance variable. Both code below are equivalent.

accessor method

If we want to limit the user to write only, we can use attr_write :title. If we want to limit user to read only, we can use attr_read :title.

We can use attr to multiple fields by separating them with coma , like attr_accessor :title, :author, :pub_year.

When you use accessors inside a method in a class use self. before the variable.

1
2
3
4
5
6
7
8
class Book
  attr_accessor :small_cover, :large_cover

  def cover_url=(url)
    self.small_cover = url + "-small.jpg"
    self.large_cover = url + "-large.jpg"
  end
end

More about self read this article https://www.rubyguides.com/2020/04/self-in-ruby/

Initialize

Is a method for creating instance by passing argument to constructor method new.

Creating new object with acessor method like code below is cumbersome and error prone.

1
2
3
4
b = Book.new
b.title = "Code"
b.author = "Ruby Red"
b.pub_year = 2020

We can write simpler using Book.new(title: "Code", author: "Ruby Red", pub_year: 2020). To use this method we should define initialize method in the class like

1
2
3
4
5
6
7
class Book
  def initialize(title:, author:, pub_year:)
    @title = title
    @author = author
    @pub_year = pub_year
  end
end

Virtual Attribute

If an accessor method doesn’t directly get or set an instance variable, it defines a virtual attribute.

Class Methods

  • is called on a class instead of an object
  • new is an example of a class method
  • is independent of specific object state, but still related to the entity modeled by a class.

For example, we want to add search feature in Book class and the convenient would be to call Book.find. To define a class method, you need to prefix its name with self dot self.

Class Variable @@

Class variable maintain class-level state which can be referred to by class methods. For example, we might want to keep track of the number of search queries for books.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Book
  @@search_count = 0

  def self.find(title)
    @@search_count += 1
    Book.new(title: "Code", author: "Ruby Red", pub_year: 2020)
  end
  # accessor to read @@search_count
  def self.search_count
    @@search_count
  end

end

A class variable is denoted with a double @@ and initialized in the body of the class. There is only one copy of a class variable per class and it’s shared between all objects. A class variable isn’t visible outside the class so we have to write accessor class method for it if we need to access it from the outside.

Class Instance Variable

What happens with class instance variable is that the parent class and each of the subclasses has its own copy of the variable but only one per class.

Class Instance Variable

Let’s define a collection class with a class instance variable for search_count.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Collection
  @search_count = 0

  def self.find
    @search_count += 1
  end

  # read accessor
  def self.search_count
    @search_count
  end
end

As with class variables, I’am setting the variable in line 2, but note that single @ instead of double. Instance variables which are set in the class body or in a class method become variables in the class itself rather than in objects.

Now we can create a specialized collection class with its own search_count, for example, for book series. And like with class variable, the class instance variable also needs to be initialized in each subclass.

1
2
3
class Series < Collection
  @search_count = 0
end

Now try to run Collection.find once, and Series.find twice. Then for Collection.search_count will result 1 and for Series.search_count yield 2.

Operators

An interesting feature of Ruby is that many operators are implemented as method calls on objects. They just have a little bit of syntatic sugar on top so using them doesn’t look like calling methods.

operator method

Let’s see if this can be applied to the Collection class. The collection contains an array of book objects, and we might want to provide convenient array-like access to it without exposing the actual book array, because we don’t want it manipulated in arbitrary ways. We can do it by defining a square brackets operator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Collection
  attr_reader :name

  def initialize(name)
    @name = name
    @books = []
  end

  def []index
    @books[index]
  end

end

Adding an append operator may be handy for populating Collections with books.

1
2
3
4
5
class Collection
  def <<(book)
    @books << book
  end
end

Now you can add collection with this way

1
2
3
c = Collection.new("Software")
c << Book.new(title: "Code", author: "Ruby Red", pub_year: 2020)
c << Book.new(title: "Hacking", author: "Hack with Ruby", pub_year: 2013)

It’s even possible to define unary operators for a class. For example, we could redefine the not operator to return false when a Collection is empty

1
2
3
4
5
class Collection
  def !
    @books.empty?
  end
end

This would allow you to test the Collection state in conditionals like this.

1
2
3
4
c = Collection.new("Empty")
if !c 
  puts "Collection #{c.name} is empty."
end

Class Equality

Sometimes we may need to check whether an object is an instance of a particular class. For example, when generating the HTML for different types of collections, you might use a case expression to branch on collection class

1
2
3
4
5
6
7
8
def collection_html(collection)
  case collection
  when Series
    # return series-specific HTML
  when Collection
    # return generic collection HTML
  end
end

This means that the collection class has to be compared with the classes specified in the when clauses.

There are a couple of other options:

  • kind_of? or is_a, they check instance of parent as well, example: series.kind_of(Book)
  • instance_of?_, is more strict, only work if it in the same class, example: book.instance_of(Book)
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy