Everything - including integers, strings are objects.
For example, in Ruby: -10.abs => 10
Only because '-10' is an object we could call the 'abs' method on it - instead of calling it like a normal function abs(-10). Similarly, "arjun".length is possible because "arjun" is an object.
Pretty cool!
Ruby and monkey patching. I like how you can modify a class in Ruby and you can add methods to it. And because everything in Ruby is an object, you can modify almost everything.
This makes the language more readable. For example because even an integer is an object in ruby, and you can call different methods on it. eg. above 10.hours. In most other languages you would have to write hours(10) to do something similar. But it wouldn’t be so readable.
This could be dangerous, but it has a lot of uses. And this is what Rails does with ActiveSupport (adding weeks, months etc).
Class variables are shared by the class, and all of its subclasses. It’s like a global variable where if you change it in one, you change it in all of the subclasses.
Useful when you want to set something like this is an Active Record instance which is always going to use Postgres to talk to the database, where you want all the children to use the same connection to talk to the database.
Metaprogramming is the concept of writing code that writes code.
Normally when you access a objects method you would have to have deined the method before. With method missing, you can hijack this and let the code handle cases where you haven't set the methods before.
For e.g.
class Shop
attr_accessor :attributes
def initialize
@attributes = {}
end
def method_missing(column, *args, &block)
if column.end_with?("=")
@attributes[column.to_s[0..-2].to_sym] = args.first
else
@attributes[column]
end
end
end
shop = Shop.new
p shop.shopify_domain
p shop.shopify_domain = "dropzone.myshopify.com"
p shop.shopify_domain
Running the above code would return the following:
arjunrajkumar@ARJUNs-MacBook-Pro desktop % ruby method_missing.rb
nil
"dropzone.myshopify.com"
"dropzone.myshopify.com"
In the above code, method_missing intercepts the call and in the first case retunrs nil. Then it sets the shopify_domain
to the assigned value, and lastly we are printing the assigned value instead of nil. This is really helpful, as now we can interact with objects that does not necesarily have the methods defined right away.
The above implementation of method_missing is good, but the problem is that ideally we only want to apply method_missing to certain allowed methods. Because we are not checking for type, we could run into errors by using method_missing for all methods.
For e.g. if we tried to call upcase
on an integer, the code would run, instead of it giving an error. Instead we want it to raise a NoMethodError
. To fix this we can use respond_to
Also if we checked if shop.respond_to?(:shopify_domain)
it would return false
. This is wrong as we know that shop responds to shopify_domain.
class Shop
attr_accessor :attributes, :allowed
def initialize
@attributes = {}
@allowed = [:shopify_domain]
end
def method_missing(column, *args, &block)
if !respond_to_missing(column)
super
elsif column.end_with?("=")
@attributes[column.to_s[0..-2].to_sym] = args.first
else
@attributes[column]
end
end
def respond_to_missing?(column, *)
allowed.include?(column.to_s.gsub("=", "").to_sym)
end
end
Now it correctly states that shop responds to shopify_domain
but does not respond to upcase
We can verify this in the following way:
shop = Shop.new
p shop.method(:shopify_domain)
p shop.method(:shopify_domain=)
p shop.method(:upcase)
Running this code will return
arjunrajkumar@ARJUNs-MacBook-Pro desktop % ruby method_missing.rb
#<Method: Shop#shopify_domain(*)>
#<Method: Shop#shopify_domain=(*)>
Traceback (most recent call last):
1: from method_missing.rb:28:in `<main>'
method_missing.rb:28:in `method': undefined method `upcase' for class `Shop' (NameError)
We could use method_missing
and create methods like I have done above - but Rails does it in a cleaner and faster way. Rails uses define_method
to create methods on the go.
class Shop
@@attributes = [:shopify_domain]
@@attributes.each do |name|
define_method(name) do
@attributes[name]
end
define_method(:"#{name}=") do |value|
@attributes[name] = value
end
end
def initialize
@attributes = {}
end
end
By running the above code you can see that we are dynamically creating methods using meta programming.
This is how Rails Active Record creates methods from the database.
In the code below, I am making a connection to my database, getting the tables and column names, and dynamically creating methods for them.
require 'pg'
module ActiveRecord
class Base
def self.table_name
"#{name.downcase}s"
end
def self.columns
@columns ||= connection.exec("SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '#{table_name}';").to_a
end
def self.connection
@connection ||= PG.connect(dbname: 'drop_zone_development')
end
def self.inherited(base)
base.class_eval do
columns.each do |column|
name = column["column_name"]
define_method(name) do
@attributes[name]
end
define_method(:"#{name}=") do |value|
@attributes[name] = value
end
end
end
end
def initialize
@attributes = {}
end
end
end
class Shop < ActiveRecord::Base
end
class User < ActiveRecord::Base
end
shop = Shop.new
shop.shopify_domain = "new_testing.myshopify.com"
p shop.shopify_domain
By using self.inherited(base)
we are telling that we only want to run the inherited
code when a class inherits from ActiveRecord::Base.
So, now we are dynamically creating methods based on the database columns. We have created classes in Ruby that represents tables inside the database!