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.