A Recap of Object Oriented Programming

Posted by Oh Boon Sim on May 30, 2019

Object Oriented Programming

As Avi rightly mentioned in one of his videos, OOP is a complex topic and requires lots of iteration and practice for the concepts to truly take hold in our minds. Moreover, with the immense breadth of this topic, I thought it would be useful to compile a summary of the relatively tougher concepts and interesting learning points we picked up in this module.

Object Oriented Programming vs Procedural Programming

“Object-oriented programming is a programming paradigm that uses abstraction (in the form of classes and objects) to create models based on the real world environment. An object-oriented application uses a collection of objects, which communicate by passing messages to request services. Objects are capable of passing messages, receiving messages and processing data. The aim of object-oriented programming is to try to increase the flexibility and maintainability of programs. Because programs created using an OO language are modular, they can be easier to develop, and simpler to understand after development

This pretty much echoes what we’ve been taught so far on OOP. One memorable quote from Avi was that an object is the combination of data and logic /procedures whereas in procedural programming, these are separate. In procedural programming, each time we define a method, an argument needs to be specified in order to take in the data we’re operating on. This can be troublesome as we would have to manage our data through proximity and arguments, and remember the appropriate data structures to be passed into those methods. However, in OOP, methods are defined within the scope of each class, we’re essentially teaching our objects to manage their own data.

Object Relationships

The “Belongs To” Relationship

In the lab above, we’ve used a Song class to illustrate the “belongs to” concept. What the lesson says is that songs (or song instances) can have multiple attributes, i.e. belong to these attributes (:title, :artist). In turn, many other song instances could belong to this same artist. So how do we model this dynamic?

To do this we need to first create two different classes – Song and Artist

class Song

  attr_accessor :title, :artist
	
	def initialize(title)
	  @title=title
	end
	
end
class Artist

  attr_accessor :name, :genre
	
	def initialize(name, genre)
	 @name=name
	 @genre=genre
	end
	
end

Once this is done, we can simply create an Artist instance and a Song instance:

drake = Artist.new("Drake", "rap")
hotline_bling = Song.new("Hotline Bling")

We then assign the artist instance to hotline_bling’s instance variable of @artist with the following:

hotline_bling.artist = drake

As a result, the song now belongs to the artist instance of “drake”, and in turn “drake” has other attributes that we can call, for example:

hotline_bling.artist.genre => “rap”
hotline_bling.artist.name => “Drake”

The “Has Many Relationship

In the “belongs to” example above, each song instance belongs to an artist. The “has many” relationship is the inverse of this, i.e. each artist has many songs. Essentially what we want to achieve is this:

jay_z.songs 

to return an array of jay_z’s songs where jay_z is the instance of the Artist class.

To do this, we have to build our code in the Artist class:

class Artist
  attr_accessor :name
 
  def initialize(name)
    @name = name
    @songs = []
  end
 
  def add_song_by_name(name, genre)
    song = Song.new(name, genre)
    @songs << song
    song.artist = self
  end
 
  def songs
    @songs
  end
end

Over here we’re doing a few things:

@songs = []

We have initialized the Song instance with an empty array represented by the instance variable of @songs.

song = Song.new(name, genre)
@songs << song

In #add_song_by_name method, we have created a new instance of the Song class called “song” which is initialized with a @name and @genre. We then add this new Song instance to the array of @songs.

song.artist = self

We then assign the song’s @artist to our new instance of Artist (“self”) that we are creating in this code.

The result is that we will have an array (@songs) which consists of the song instances that we have created through the #add_song_by_name method, all of which have “self” assigned as its artist. In other words, our new Artist instance now has many songs and genres associated with it.

Class Inheritance

class Child < Parent
	
     def method
	     #code to overwrite parent’s method
     end
	
end

The above code will cause the Child class to inherit all methods and attributes from the Parent (a.k.a. super) class. The option to overwrite the Parent’s methods is also available if we rewrite the parent method in the Child class with a different function.

Using ‘super’ to supercharge inheritance

class Student < User
  def log_in
     super
     @in_class = true
  end
end

Using ‘super’ allows us to inherit all functionality in the parent’s #log_in method, eliminating the need for us to re-type the whole method if we wanted to add some more functionality into our #log_in method for the Student class.

Keyword Argument

Before Keyword Argument:

def happy_birthday(name, current_age)
   puts "Happy Birthday, #{name}"
   current_age += 1
   puts "You are now #{current_age} years old"
end

After Keyword Argument:

def happy_birthday(name: "Beyonce", current_age: 31)
  puts "Happy Birthday, #{name}"
  current_age += 1
  puts "You are now #{current_age} years old"
end

Keyword arguments eliminate the need for us to remember the sequence that arguments have to be passed into a certain method. They allow us to name the arguments that we pass in as keys in a hash. Then, the method body uses the values of those keys, referenced by name. So even if we were to change the order of our key/value pairs, the method would still work as intended. E.g. the code below would still output the same result as the above:

def happy_birthday(current_age: 31, name: "Beyonce")
  puts "Happy Birthday, #{name}"
  current_age += 1
  puts "You are now #{current_age} years old"
end

Interesting Learning Points - Infinite Loop

So this was a problem I encountered while attempting the Music Library CLI Lab When I tried running the 004_songs_and_artists_spec.rb test file, it resulted in an infinite loop which looked like this:

     Failure/Error: song.artist=self if song.artist==nil
     
     SystemStackError:
       stack level too deep
     # ./lib/artist.rb:36:in `add_song'
     # ./lib/song.rb:38:in `artist='
     # ./lib/artist.rb:36:in `add_song'
     # ./lib/song.rb:38:in `artist='
     # ./lib/artist.rb:36:in `add_song'
     # ./lib/song.rb:38:in `artist='
     # ./lib/artist.rb:36:in `add_song'
     # ./lib/song.rb:38:in `artist='
     # ./lib/artist.rb:36:in `add_song'
     # ./lib/song.rb:38:in `artist='
     # ./lib/artist.rb:36:in `add_song'
     # ./lib/song.rb:38:in `artist='
     # ./lib/artist.rb:36:in `add_song'
     # ./lib/song.rb:38:in `artist='
     # ./lib/artist.rb:36:in `add_song'

This was arising from my setter method #artist= in my Song class, where I referenced the #add_song method which lies within the Artist class.

  def artist=(artist)
    artist.add_song(self) unless artist.songs.include?(self)
  end

The solution to this infinite loop is to add “@artist= artist” within the #artist= setter method, which aims to assign the argument to the instance variable of @artist within the Song class. So the correct solution should be:

  def artist=(artist)
    @artist=artist #this is important to prevent infinite loop
    artist.add_song(self) unless artist.songs.include?(self)
  end

Why is that? What I’m going to do here is compare the first scenario with the second one. But first, here is what the #add_song method looks like within the Artist class.

  def add_song(song)
    song.artist=self if song.artist==nil
    @songs << song unless songs.include?(song)
  end

What this method does is take in a Song instance as the argument and assign its @artist to be an instance of itself if the song does not have an artist already, hence the reason for the “if” condition. Now notice that “song.artist=self” is actually the setter method within the Song class, so this would bring us back to the #artist= setter method within the Song class, which then brings us back to the #add_song within the Artist class, creating an infinite loop.

This is why “@artist=artist” is important because once the artist instance is assigned to @artist within the Song class, when the #add_song method is called, the setter method of “song.artist=self” will not be called because song.artist is no longer ‘nil’. This then prevents an infinite loop from happening.

Methods to Know:

The send method

This is a form of metaprogramming which comes in handy when scraping data from websites that are ever-changing.

Basically:

object.send(key=, value)

is the same as

object.key = value

As illustrated in the Twitter Example, this comes in handy when the objects we’re trying to scrape don’t stay constant and we don’t want to keep changing our code to accommodate the changing attributes that we’re trying to scrape. In essence, we’re trying to abstract away our class’ dependency on specific attributes. This metaprogramming method enables us to tell our class to accept some unspecified number and type of attributes and assign those accordingly within our class.

class User
  attr_accessor :name, :user_name, :age, :location, :bio
 
  def initialize(attributes)
    attributes.each {|key, value| self.send(("#{key}="), value)}
  end
end

The tap method

def create_by_name(name)
   self.new.tap do |i|
   i.name = name
   end
end

is the same as

def create_by_name(name)
   i=self.new
   i.name=name
   i
end

So what #Tap does is assign the variable within the pipes as the object, carries out the the functionality within the block and then returns the object at the end of the block.