Dave Thomas’s “Stop Using Classes” talk at SFRuby has me rethinking how I use modules. His argument is simple: Ruby developers reach for classes by default when modules are often the better tool.
I kept writing these little singleton objects that don’t hold state. They’re just bags of behavior and data. Every time I reached for a class, I’d end up with an initialize that does nothing and a .new call that exists only because classes demand it. Dave’s talk gave me the push to stop doing that and think harder about modules.
The problem with extend self
The standard module-as-singleton pattern is extend self:
module Wordpress
extend self
def name = "WordPress"
def match?(doc) = doc.at_css('meta[name="generator"][content^="WordPress"]')
end
Wordpress.name # => "WordPress"
Wordpress.match?(doc) # => true/false
It works, but it’s boilerplate. Every module needs it. And if you want shared behavior across a family of these singleton modules, you’re stuck writing it into each one or reaching for metaprogramming. That’s exactly the kind of thing include should solve.
What I actually want
I’m building a framework detector for OpenGraph+ that inspects HTML documents and identifies what platform built them. Each framework is a singleton module that knows its name, its slug, and which CSS selectors identify it.
Dave’s talk got me thinking about what the ideal version of this looks like:
- Each framework is a module with instance-style method definitions
- Including a shared module auto-wires everything up
- All frameworks register themselves automatically
- The parent module provides a
detectmethod to find which framework matches
Here’s what I landed on:
module Framework
@all = []
def self.all = @all
def self.detect(body)
return nil if body.nil?
doc = Nokogiri::HTML(body)
all.find { it.match?(doc) }
end
def self.included(mod)
mod.extend(mod)
Framework.all << mod
end
def match?(doc) = selectors.any? { doc.at_css(it) }
module Wordpress
include Framework
def name = "WordPress"
def slug = "wordpress"
def selectors = [
'meta[name="generator"][content^="WordPress"]',
'link[href*="/wp-content/"]',
'link[href*="/wp-includes/"]'
]
end
module Shopify
include Framework
def name = "Shopify"
def slug = "shopify"
def selectors = [
'link[href*="cdn.shopify.com"]',
'script[src*="cdn.shopify.com"]',
'meta[name="shopify-checkout-api-token"]'
]
end
module Webflow
include Framework
def name = "Webflow"
def slug = "webflow"
def selectors = [
"html[data-wf-site]",
'meta[name="generator"][content="Webflow"]'
]
end
module Wix
include Framework
def name = "Wix"
def slug = "wix"
def selectors = [
'meta[name="generator"][content*="Wix"]',
'script[src*="parastorage.com"]'
]
end
module Astro
include Framework
def name = "Astro"
def slug = "astro"
def selectors = ['meta[name="generator"][content*="Astro"]']
end
module Jekyll
include Framework
def name = "Jekyll"
def slug = "jekyll"
def selectors = ['meta[name="generator"][content*="Jekyll"]']
end
module Middleman
include Framework
def name = "Middleman"
def slug = "middleman"
def selectors = ['meta[name="generator"][content*="Middleman"]']
end
module Nextjs
include Framework
def name = "Next.js"
def slug = "nextjs"
def selectors = ["script#__NEXT_DATA__", 'script[src*="/_next/"]']
end
module Nuxt
include Framework
def name = "Nuxt"
def slug = "nuxt"
def selectors = ["div#__nuxt", 'script[src*="/_nuxt/"]']
end
module Rails
include Framework
def name = "Rails"
def slug = "rails"
def selectors = ['meta[name="csrf-param"][content="authenticity_token"]']
end
module Laravel
include Framework
def name = "Laravel"
def slug = "laravel"
def selectors = ['input[name="_token"]']
end
module Django
include Framework
def name = "Django"
def slug = "django"
def selectors = ['input[name="csrfmiddlewaretoken"]']
end
end
The trick: mod.extend(mod)
The key line is inside self.included:
def self.included(mod)
mod.extend(mod)
Framework.all << mod
end
When Wordpress does include Framework, Ruby calls Framework.included(Wordpress). Inside that hook, mod.extend(mod) makes Wordpress extend itself. Its instance methods become module methods. Then it registers itself in the Framework.all array.
This is the same effect as extend self, but it happens automatically via the include. You write include Framework once and get both the singleton behavior and the registration for free.
The interface
The shared match? method is defined as an instance method on Framework:
def match?(doc) = selectors.any? { doc.at_css(it) }
When a module like Wordpress includes Framework, it gets match? as an instance method. Then mod.extend(mod) promotes it to a module method. Each framework only needs to define selectors and it gets match? for free.
Here’s what the interface looks like across all frameworks:
Framework::Wordpress.name # => "WordPress"
Framework::Wordpress.slug # => "wordpress"
Framework::Wordpress.selectors # => [...]
Framework::Wordpress.match?(doc) # => true/false
How it’s called
The primary entry point is Framework.detect:
framework = Framework.detect(response.body)
Pass it an HTML string and it parses the document, then iterates through every registered framework calling match? until one hits. It returns the matching module or nil.
From there you can grab whatever you need off the result:
if framework = Framework.detect(response.body)
puts framework.name # => "WordPress"
puts framework.slug # => "wordpress"
end
Since detect returns the module itself, you get the full interface. Use the slug to store it in a database, the name for display, or call match? again with a different document.
Where it gets used
The reason I built this is onboarding for OpenGraph+. When somebody connects their website, I need to figure out what framework they’re running so I can show them the right installation instructions. A Rails app needs a gem. A WordPress site needs a plugin. A Next.js site needs an npm package. Getting this wrong means a confused customer staring at docs that don’t apply to them.
So when a new website is added, a background job fetches the homepage and detects the framework:
class DetectFrameworkJob < ApplicationJob
def perform(website)
response = HTTP.get(website.url)
if framework = Framework.detect(response.body)
website.update(framework: framework.slug)
end
end
end
Once I know the framework, the onboarding flow shows the right instructions automatically. No dropdown where the customer picks “WordPress” or “Rails” from a list. No guessing. I look at their HTML and figure it out.
In a view, I use the slug to route them to the correct docs:
if framework = Framework.detect(website.cached_body)
render "onboarding/#{framework.slug}"
end
I also use it to display what a site is built with:
if framework = Framework.detect(website.cached_body)
tag.span framework.name, class: "badge"
end
And to get the full list of supported frameworks for a marketing page:
Framework.all.map(&:name)
# => ["WordPress", "Shopify", "Webflow", "Wix", "Astro", ...]
The nice thing about this pattern is adding a new framework to OpenGraph+ is just another module. Define the selectors, include Framework, and the onboarding flow picks it up immediately.
Why not classes?
I could build this with classes and instances, but I’d end up instantiating objects that don’t hold state. These frameworks are static definitions. They don’t change at runtime. A module that extends itself is the right level of abstraction. It’s a namespace, a singleton, and a bag of behavior all at once.
The include Framework pattern gives me that without any per-module boilerplate. Define the data methods, include the module, and it’s done.