March 7, 2015 by Daniel P. Clark

Configuration With a Singleton Instance

I was considering how I might want to further implement my gem and incorporate configuration settings for it.  I wanted only one instance of my configuration and it needed to be part of my module.  So I’ve come up with a solution I like that allows me to do this and even have a true “sense” of untouchable constants.

I’ve found OpenStruct to be for my liking in this situation as it allows for any configuration (variable) to be set on it.  I’ve also found I can set “unalterable” variables… at least through standard assignment options.

Here’s the basics of it:

require 'ostruct'

class MyProject::MyConfig < OpenStruct
end

module MyProject
  class << self
    def config
      @config ||= MyConfig.new
    end
  end
end

The reason I created the class MyProject::MyConfig just to inherit OpenStruct is that I want my configuration to be identified by a class name that indicates it is a config object that belongs to my project.  In module MyProject I use attr_reader for config because we are getting the OpenStruct instance handed back to us and we can still invoke method/value assignment on it.  I use the single class variable @config for the internal singleton value because it’s not going anywhere (No need to use @@config).  The method config is defined on the singleton instance of the module MyProject via the class << self.  So to set any value all I need to do is MyProject.config.new_value = 42.

MyProject.config.new_value = 42
MyProject.config.new_value
# => 42

MyProject.config.hair = :awesome
MyProject.config.hair
# => :awesome

MyProject.config.hair = :bad_hair_day
MyProject.config.hair
# => :bad_hair_day

If I want to set a constant value that I don’t want to change then all I have to do is define the method within the OpenStruct instance.  You can do it in the class, or open it up with the singleton opener.

class MyProject::MyConfig < OpenStruct
  def no_touchy
    :always_this
  end
end
x = MyProject::MyConfig.new
x.no_touchy
# => :always_this
x.no_touchy = 493872
x.no_touchy
# => :always_this

class << x
  def also_no_touchy
    :cant_touch_this
  end
end
x.also_no_touchy
# => :cant_touch_this
x.also_no_touchy = :batman
x.also_no_touchy
# => :cant_touch_this

If you want a default value that can be updated; then you may implement that method with a method_missing(:method_name) || :value .  As soon as you assign a new value it will choose the updated value.

class Example < OpenStruct
  def max_limit
    method_missing(:max_limit) || 42
  end
end
x = Example.new
x.max_limit
# => 42
x.max_limit = 493872
x.max_limit
# => 493872

Summary

So now you can have a single instance configuration that’s easily updatable and you can set values that you don’t want to change.  I do realize that the “constants” in this case aren’t defined the way the language normally defines constants.  But in this situation it’s what works… and it works better than constants that don’t truly stay constant when reassigned ;-).

I hope this was informative and enjoyable for you!  Please feel free to comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!

God Bless!
-Daniel P. Clark

Image by murray 9000 via the Creative Commons Attribution-NonCommercial-NoDerivs 2.0 Generic License

#config#configuration#initializer#module#openstruct#ruby#singleton#variable

Comments

  1. Sergey Avseyev
    March 8, 2015 - 9:38 am

    when defining only one class method, it will be more clear to use ‘def self.config’ form

    why don’t you want to catch all sets to ‘constant’ value using ‘undef :no_touchy=’? It will raise undefined method error

    • Daniel P. Clark
      March 8, 2015 - 1:37 pm

      Thanks Sergey!

      I believe ‘def self.config‘ wouldn’t be a good solution for me. The reason is that it defines a method on the module that can be inherited. That isn’t what I wanted. I wanted only one instance of config to exist so I opened up the anonymous singleton that exists on the module. The use of the shovel operator << allows us to open anonymous singletons that belong to each object and are never inherited.

      With OpenStruct every undefined method gets handed to method_missing which it has implemented to work like a Hash store.

      
      require 'ostruct'
      
      x = OpenStruct.new
      
      class << x
        undef :apple=
      end
      # NameError: undefined method `apple=' for class `OpenStruct'
      # from (irb):12:in `singleton class'
      
      class << x
        attr_accessor :apple
        def apple
          "green"
        end
      
        undef :apple=
      end
      x.apple
      # => "green" 
      x.apple= 3
      # => 3 
      x.apple
      # => "green"
      

      Because of OpenStruct’s use of method_missing; undef will not produce an “undefined method error”.

  2. tdg5
    March 9, 2015 - 6:27 am

    Hey Dan,

    Solid introduction to configuration with a singleton instance! Though you will sometimes see people treat configuration objects similar to the one you describe as a more traditional singleton rather than as a singleton instance, singleton instance is definitely the way to go. A singleton instance is much more testable than an actual singleton and in my experience it is not infrequent that it becomes necessary to have another instance of the configuration object, typically for a subclass of the class originally requiring configuration.

    In regard to the “max_limit” example you provide for handling default values, it’s a minor optimization, but you might consider using the __method__ method when calling method_missing instead of referring to :max_limit directly. It’s a little DRYer which means fewer places that the name “max_limit” would need to be changed if you decided to rename the method and less opportunity for weird bugs to occur. In more concrete terms, that would look like:

    
    def max_limit
      method_missing(__method__) || 42
    end
    

    It might be DRYer still to implement your own version of method_missing if you have a lot of accessors that have default values. For example, something along the lines of:

    
    def method_missing(method_name, *args)
      default_method = "default_#{method_name}"
      super || respond_to?(default_method) ? send(default_method) : nil
    end
    

    Unfortunately, all these options run into trouble if the user-defined configuration value is nil or false. If support for falsy values is needed in a situation where the configuration value also has a default, it’d be worth looking into the inner workings of OpenStruct and seeing what options are available to determine if a key has been defined or if there’s a means of calling something along the lines of Hash#fetch as this can be a better option as demonstrated in this post on the matter by Avdi Grimm. Anyway, please don’t take these suggestions as a criticism, I definitely prefer to keep things simpler where it is possible, but edge cases often emerge.

    Thanks for sharing!

    • Daniel P. Clark
      March 9, 2015 - 11:13 am

      Thanks Danny!

      I hadn’t used the __method__ method before. Thanks for the tip! And I like your suggestion on defining your own method_missing. Definitely will keep default values cleaner.

      You were right about looking into the internals of OpenStruct. As to allowing nil or false; the internal Hash is @table so something like this works well.

      
      def fetch(key, &block)
        @table.fetch(key.to_sym, &block)
      end
      

      Now max_limit can be implemented as so.

      
      def max_limit
        fetch(__method__) { 42 }
      end
  3. Andrew Kozin
    March 10, 2015 - 7:48 am

    Hi, Daniel!

    Thanks for the post, it’s really helpful.

    One thing I cannot figure out is the purpose of `attr_reader :config` in the `MyProject` module. AFAIK, the only place it will work is the class including (or extended by) `MyProject`.

    But it simply return `nil` ’cause no setter has been defined.

    What it is needed for? Could you put some example?

    • Daniel P. Clark
      March 10, 2015 - 8:15 am

      Good catch! Actually it’s not needed and is a bit misleading. In the process of figuring out how to implement this feature I had put that in and just left it there. But in fact it does nothing for the configurable object. So I apologize to you for that.

      The only setter methods available should be for the OpenStruct instance created on the Object config which only resides with the singleton instance on MyProject.

  4. Robert Fletcher
    March 12, 2015 - 3:58 pm

    I use the singleton strategy like this, but the problem with using OpenStruct is that it fails silently for invalid options. So if I type MyProject.config.mispleled_thing = 'foo' I’ll be none the wiser. Instead I go the long hand route and create a class with its own accessors for the configurations I want to be able to set. Another convenience I like to add is a block method for tidier configuration in my code:

    
    def configure
      yield(config)
    end
    
    # and:
    
    MyClass.configure do |config|
      config.my_cool_option = 'this'
      config.my_other_option = 'that'
    end
    
    

    I like to refer to other libraries such as RSpec for ideas on this:

    https://github.com/rspec/rspec-core/blob/master/lib/rspec/core.rb#L75
    https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/configuration.rb

    • Daniel P. Clark
      March 12, 2015 - 6:30 pm

      I see your point. I guess it depends on how strict/tight you want to control it. I’m open to learning better ways. How does the yield(config) work? Are my_cool_option and my_other_option predefined on MyClass before the configure block is called?

      • Robert Fletcher
        March 12, 2015 - 7:22 pm

        I guess I glossed over that a bit. Here’s a more complete example:

        
        # my_project/config.rb
        module MyProject
          class Config
            attr_accessor :my_cool_option, :my_other_option
          end
        end
        
        # my_project.rb
        module MyProject
          def self.config
            @config ||= Config.new
          end
        
          def self.configure
            yield(config)
          end
        
          def self.reset
            @config = nil
          end
        
          def config # this is for easy access within the module
            MyProject.config
          end
        end
        
        

        Then you can use it in two different ways:

        
        # single option style:
        MyProject.config.my_cool_option = 'this'
        
        # or option block style:
        MyProject.configure do |config|
          config.my_cool_option = 'this'
          config.my_other_option = 'that'
        end
        
        

        yield(config) just passes the Config instance into the block.

        • Daniel P. Clark
          March 12, 2015 - 7:36 pm

          Very nice! Thanks for sharing it!

Leave a Reply

Your email address will not be published / Required fields are marked *