Modules as singletons
The singleton pattern in object-oriented programming is a design pattern that restricts the instantiation of a class to a single instance and provides a global point of access to that instance. In Ruby, this pattern can be implemented using classes, but it can also be achieved more simply using modules. Since modules are objects, they are effectively singletons by default!
A beautiful way of implementing a module as a singleton is by using extend self. Here's an example of one of my favorite use cases for this pattern: a simple HTTP client.
require "net/http"
require "json"
module Typicode
extend self
BASE_URI = URI("https://jsonplaceholder.typicode.com")
HTTP_ERROR = Class.new(StandardError)
def get(path) = request("GET", path)
def post(path, params = nil) = request("POST", path, params)
def patch(path, params = nil) = request("PATCH", path, params)
def put(path, params = nil) = request("PUT", path, params)
def delete(path) = request("DELETE", path)
private
def request(method, path, params = nil)
response = Net::HTTP.start(BASE_URI.host, BASE_URI.port, use_ssl: true) do |http|
http.send_request(method, path, params&.to_json, headers)
end
if response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)
else
raise HTTP_ERROR, "#{response.code} for #{method} /#{path}"
end
end
def headers
@headers ||= {
"Content-Type" => "application/json",
"Accept" => "application/json",
}.freeze
end
end
Typicode.get("/posts/1")
# => { "id" => 1, "title" => "sunt aut facere...", ... }
Typicode.post("/posts", title: "foo", body: "bar", userId: 1)
# => { "id" => 101, "title" => "foo", "body" => "bar", ... }
Typicode.patch("/posts/1", title: "updated title")
# => { "id" => 1, "title" => "updated title", ... }
Typicode.delete("/posts/1")
# => {}
Typicode.get("/invalid")
# => raises Typicode::HTTP_ERROR: "404 for GET /invalid"
What extend self does is make all instance methods of the module also available as module methods. This saves us the trouble of having to prefix all our method definitions with self. to make them module methods. It also allows private to work as expected, which is not the case for module methods defined with self..
Another common way to achieve the same effect is by adding a class << self block and defining the methods inside it, but I tend to prefer the more minimal extend self, especially since it avoids another level of indentation.
Note that we can use instance variables in our module as we would in any other object, so our singletons can still have some internal state if we need it. We have to be mindful of thread safety if they are used in a multi-threaded context.
BTW, I only used memoization of headers in the example to demonstrate that we can have some internal state in our module, it could have been a constant instead. In real life, I commonly find myself merging ENV variables into the headers though, e.g. API tokens, at which point memoization makes more sense.
History
As both extend and self have been part of Ruby since its inception, so has extend self.