Skip to content

Instantly share code, notes, and snippets.

@arjunrajkumar
Last active March 18, 2022 07:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save arjunrajkumar/177dea0b1e0e6092ac806772512641ab to your computer and use it in GitHub Desktop.
Save arjunrajkumar/177dea0b1e0e6092ac806772512641ab to your computer and use it in GitHub Desktop.

Ruby notes

Everything is an object

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!

Monkey Patching:

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: @@

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

Metaprogramming is the concept of writing code that writes code.

Method Missing

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.

Duck typing with respond_to

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)

Defining methods dynamically with define_method

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment