I actually intended to add this post to old article about the same title. However, I don’t want to make an article too long. Regarding to this principle, there are so many things to learn. Because software design is almost about managing changes. You will see this one as the test at many interviews. The interviewers will give you a small challenge. Yes! pretty easy to solve. Because they just want to see how good you apply this principle. The first requirement might be simple. But be careful. Any time you write code, you need to think that it will never change. We don’t write code to change but we write code to extend later. We make a room for changes.
I won’t tell you anythings new. Because the principle is so clear: Closing the change and Open for extending. However, in order to take advantage of it we need samples and practices. Let me show you how rails, especially ActiveJob implemented this one.
ActiveJob
At first glance, when I look at “perform” method which we need to implement every time we create a new job class. I wondered why we implement instance method but we involve it like class method? Simply like this.
module ImagesJob
class DeleteImage < ApplicationJob
queue_as :default
def perform(local_images)
local_images.each do |local_image|
File.delete(local_image) if File.exists?(local_image)
end
end
end
end
Obviously, perform method here is instance method. But how do we involve?
DeleteImage.perform_later(images)
Where does perform_later method come from? This is how it works in rails. Job class which includes Enqueuing module will involve “new” method to create an instance inside class method “perform_later”. We still involve “new” for creating instance but this instance method is hidden in class method. So that we have two options. We can put its instance as a parameter or we delegate job_or_instantiate method to instantiate.
module Enqueuing
extend ActiveSupport::Concern
# Includes the +perform_later+ method for job initialization.
module ClassMethods
# Push a job onto the queue. The arguments must be legal JSON types
# (string, int, float, nil, true, false, hash or array) or
# GlobalID::Identification instances. Arbitrary Ruby objects
# are not supported.
#
# Returns an instance of the job class queued with arguments available in
# Job#arguments.
def perform_later(*args)
job_or_instantiate(*args).enqueue
end
private
def job_or_instantiate(*args) # :doc:
args.first.is_a?(self) ? args.first : new(*args)
end
end
end
This module is included in Base class.
class Base
include Enqueuing
end
This is how we learn from above code. We don’t use its instance as argument option. We put `self.new` directly in perform_later method.
class ActiveJob
module ClassMethods
def perform_later(args = {})
self.new.perform(args)
end
end
def perform(*)
raise NotImplementedError
end
extend ClassMethos
end
class MyJob < ActiveJob
def perform(name)
puts name
end
end
MyJob.perform_later("Will")
# output: Will
ActionMailer
Using method_missing hook method to involve instance method of subclass. We have to use this hook. Because inherited hook can’t recognise the subclass’s public methods. It’s because the order of involving. Below is my code to simulate how ActionMailer works.
class Mailer
def self.inherited(subclass)
subclass.class_eval do
def self.method_missing(name, *args)
methods = self.public_instance_methods - Object.methods
if methods.include?(name)
self.new.send(name)
else
raise "Not found #{name} with #{args}"
end
end
end
end
end
class MyMail < Mailer
def extra_method____________________________________________
puts "Im new dude"
end
end
MyMail.extra_method____________________________________________
# output: Im new dude
In fact, ruby on rails uses a lot of hooks methods. Understanding hook methods will help us gain more insights of rails.