Heading image for post: Hey Everyone, I found a use for a Singleton

Ruby

Hey Everyone, I found a use for a Singleton

Profile picture of Matt Polito

I wrote software for a long time without really knowing what a Singleton is, so let's all get on the same page.

What's a Singleton?

Although its usage prevalence depends on the language you're developing in, the general definition is such:

A pattern that restricts the instantiation of a class to one object.

There can be only one

This definition seems fairly straightfoward, but when does a singleton actually become valuable? Here’s my answer: use singletons when you need an object specifically to hold state.

In Ruby, this pattern is available to us right from the standard library.

require 'singleton'

Alright, now we know that Singleton is to be used when we only want one instance of a class and it is availabe to us with Ruby. I found that this aligns very nicely when needing configuration done for your gems or other libraries. In many applications there are external services being used. I find it very helpful to make sure that each of these services are always wrapped in my own interface even if already utilizing a service wrapper library.

Let's start with the configuration class and then go into how it could be used.

require 'singleton'

module AppName
  module PaymentGatewayName
    class Configuration
      include Singleton

      attr_accessor :base_uri, :subscription_id,
        :merchant_pin, :server_host, :account_id
    end
  end
end

You see that we've made a Configuration class that has the inclusion of the Singleton library and some accessors to store data.

Since singletons are only for the purpose of having one instance of a class, we are not able to instantiate normally using the normal constructor #new.

We gain access to the singleton instance by calling #instance

Configuration.instance

We now have our config object, let's use it! Before I stated that this is good for the configuration of external services so let's build a wrapper for a service.

Here is what we're going to build:

module AppName
  module PaymentGatewayName
    module API
      def self.configure
        yield(configuration) if block_given?
      end

      def self.configuration
        Configuration.instance
      end

      def self.connection
        url = configuration.base_url
        @connection ||= Faraday.new(url) do |conn|
          conn.request :url_encoded
          conn.response :ox
          conn.adapter Faraday.default_adapter
        end
      end
    end
  end
end

Start with getting your Configuration:

module AppName
  module PaymentGatewayName
    module API
      ...

      def self.configuration
        Configuration.instance
      end

      ...
    end
  end
end

Storing this in an appropriately named method seems logical and I see no reason to memoize this as there will only ever be the one instance.

Next we'll define how the library will get configured:

module AppName
  module PaymentGatewayName
    module API
      def self.configure
        yield(configuration) if block_given?
      end

      ...
    end
  end
end

We define a method that takes a block and yields that block to our #configuration from before. From an outside point of view it will appear that we are configuring for the API and not for an outside state holder.

That's about it, we can now define methods that use our Configuration singleton.

Being an external API, it makes sense that we'll need to have a connection to it. So let's do that:

module AppName
  module PaymentGatewayName
    module API
      ...

      def self.connection
        url = configuration.base_url
        @connection ||= Faraday.new(url) do |conn|
          conn.request :url_encoded
          conn.response :ox
          conn.adapter Faraday.default_adapter
        end
      end
    end
  end
end

Here is a final view of our API class:

module AppName
  module PaymentGatewayName
    module API
      def self.configure
        yield(configuration) if block_given?
      end

      def self.configuration
        Configuration.instance
      end

      def self.connection
        url = configuration.base_url
        @connection ||= Faraday.new(url) do |conn|
          conn.request :url_encoded
          conn.response :ox
          conn.adapter Faraday.default_adapter
        end
      end
    end
  end
end

What this provides for us is a clear, concise point to configure the API.

AppName::PaymentGatewayName::API.configure do |config|
  config.account_id      = ENV.fetch("PAYMENTGATEWAY_ACCOUNT_ID")
  config.base_uri        = ENV.fetch("PAYMENTGATEWAY_BASE_URI")
  config.merchant_pin    = ENV.fetch("PAYMENTGATEWAY_MERCHANT_PIN")
  config.server_host     = ENV.fetch("PAYMENTGATEWAY_SERVER_HOST")
  config.subscription_id = ENV.fetch("PAYMENTGATEWAY_SUBSCRIPTION_ID")
end

This is something I would put in an initializer if utilizing it in a Rails app. That way there is only one place that has configuration data for this API.

As stated when we started this journey, a singleton is only ever going to be one instance of a class. That means that while you may find this to be a great option for this scenario, not all of your libraries would be well suited for this kind of configuration. I feel like the configuration pattern is a good one but that the use of a singleton would be a mistake for services that you may use in your app a few times in different ways. In those cases when you wanted to change an option for a different case of using the same library, you'd actually be changing the use of it entirely... not just for that one case.

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project