Factory Pattern with auto-detecting classes in Ruby
In object-oriented programming, the factory pattern is a design pattern that provides an interface for creating objects. This pattern allows objects to be created without specifying the exact class of the object being created. This allows for more flexibility in the code and simplifies the creation process. In this article, we'll explore how to use the Factory Pattern in Ruby and how to use regular expressions to auto-detect classes.
The Factory Pattern in Ruby
The factory pattern in Ruby is implemented using a factory class that creates objects of other classes. The factory class has a method that takes some parameters and returns an object of a class. This method can be called from anywhere in the code, and the actual class created is determined by the parameters passed to it.
Here's an example of a factory class in Ruby that creates objects of various types of notifications:
module Notifications
class Base
attr_reader :options
def initialize(options:)
@options = options
end
def call
raise NotImplementedError
end
end
class Email < Base
def call
puts 'Email sent!'
# do this and that
end
end
class Sms < Base
def call
puts 'SMS sent!'
# do this and that
end
end
class Push < Base
def call
puts 'PUSH sent!'
puts 'special debug msg' if options[:special_debug]
# do this and that
end
end
class Send
def self.call(method: :email, options: {})
Factory.build(method, options).call
end
end
class Factory
REGISTERED_TYPES = {
/email/ => Notifications::Email,
/sms/ => Notifications::Sms,
/push/ => Notifications::Push,
}.freeze
class << self
def build(method, options)
klass = method.to_s.split.map(&:capitalize).join('')
# if Module.const_defined?(klass) # strict call when necessary
# Module.const_get(klass).new(options: options)
if detect_type(klass) # try to detect
detect_type(klass).new(options: options)
else
raise NotImplementedError, "#{self.name}##{__method__} - class #{Module.nesting[-1]}::#{klass} has not been implemented yet"
end
end
def detect_type(name)
REGISTERED_TYPES.detect { |regex, type| return type if regex.match(name.downcase) }
end
end
end
end
# user_typed_value = 'email' # => "Email sent!"
# user_typed_value = :sms # => "SMS sent!"
# user_typed_value = 'asdf123' # => Uncaught exception: Notifications::SenderFactory#build - class Notifications::Asdf123 has not been implemented yet
# user_typed_value = :object # => Uncaught exception: Notifications::SenderFactory#build - class Notifications::Object has not been implemented yet
# user_typed_value = 'push message sender' # => "PUSH sent! special debug msg"
user_typed_value = :push # => "PUSH sent! special debug msg"
Notifications::Send.call(method: user_typed_value, options: { special_debug: true })
In this example, we have a Factory
class with a build method that takes a method
and options
parameter. The method
parameter is the type of object to build, and the options
parameter is a hash options
to pass to the object. The build
method then creates an instance of the appropriate class based on the method
parameter.
Interestingly, we can automatically detect classes using regular expressions based on the class name. This makes the factory more flexible and eliminates the need to specify the exact class name. However, it can be dangerous if we are not trying to be strict with a particular class listing, so using a strict class is much safer.
Overall, the factory pattern provides a flexible and extensible way to create objects in Ruby. Abstracting the creation process into a separate class allows for easier maintenance and modification of the code base. The use of regular expressions to determine class types provides a powerful and flexible way to match class names to their corresponding implementations.