The letter A styled as Alchemists logo. lchemists
Published May 1, 2026 Updated May 1, 2026
Cover
Ruby Classes

Classes are at the heart of Object-Oriented Programming and Ruby makes them effortless to use. They provide the following benefits:

  • Composition: Allow you to leverage the Dependency Inversion Principle (DIP) — the "D" in SOLID design — by injecting dependencies via your constructor or methods. Prefer composition over inheritance because this maximizes your ability to reuse your components to build robust architectures.

  • Inheritance: Allow you to subclass and inherit behavior from a superclass and/or use multiple inheritance to enhance your class with common functionality.

  • Encapsulation: Allows you to wrap data, state, and/or behavior (methods) within a single object. Additionally, you gain the ability to abstract common functionality without having to understand the intricate details.

  • Modularity: Promote module design which leads to improved testability and maintainability since your object is highly focused on doing one thing well. In other words, the Single Responsibility Principle (SRP) which is the "S" in SOLID design.

  • Reusability: Allow you to make multiple instances of your class without having to repeat yourself in terms of constantly writing the same implementation over and over.

  • Polymorphism: Allow you to use objects from different classes which can respond to the same method calls in their own appropriate ways, making your code more flexible and extensible (a.k.a. Duck Typing).

We’ll discuss the above in more detail but, before we do that, it’s important we take a moment to talk about the categories that classes fall into.

Categories

At a high level, there are three main categories for classes. These are not official categories are useful for this discussion. They are:

  • Basic: Any object created via the class keyword (or anonymously via Class.new), which is one of only a handful of Ruby Keywords, to implement.

  • Whole Value: Created via Data (see Ruby Data) or Struct (see Ruby Structs). You can also use the Wholeable gem for situations in which you can’t quite use a Data or a Struct but want to make your existing class a whole value object instead.

  • Functional: Created via the class keyword but the difference — and this is important — is that they adhere to the Command Pattern by implementing the #call method. This humble method unlocks the world of functional programming in Ruby by allowing you to blend objects and functions together. This is also key to Function Composition where you can compose classes along with procs, lambdas, and methods to maximum effect.

ℹ️ Technically, the Functional category is a specific pattern within the Basic category. You could say the same for Whole Values since they are a subclasses of Object except they do behave differently than basic classes which makes them important to distinguish.

Sadly, few Ruby engineers know about the latter two categories and tend know about basic classes more. Nothing wrong with basic classes as you can go far with them. The problem is that they are often abused in the most grotesque ways. Glad you’re here because we can do better!

Much has been written about whole value objects already so we won’t rehash them in this discussion in order to remain focused on comparing/contrasting basic versus functional classes instead.

Basic versus Functional

Basic classes are vast and encapsulate a wide range of creational, structural, and behavior patterns such as adapters, clients, presenters, factories, observers, visitors, and so much more. For example, consider the following presenter:

require "forwardable"

module Demo
  class Place
    extend Forwardable

    delegate %i[name created_at] => :record

    def initialize record
      @record = record
    end

    def label = name.capitalize

    def human_created_at = created_at.strftime "%B %d, %Y (%A) at %H:%M %Z"

    private

    attr_reader :record
  end
end

The above wraps a record object with additional behavior (i.e. decorations) for presentation purposes. You need this when you want to keep your data and presentation layers separate in your application. This allows you to avoid overloading your record with presentation logic that is only used by your views.

Functional classes, on the other hand, adhere to the Command Pattern by implementing the #call method. Example:

require "http"

module Demo
  class Pinger
    def initialize http: HTTP, kernel: Kernel
      @http = http
      @kernel = kernel
    end

    def call uri
      http.get(uri).then do |response|
        response.status.success? ? kernel.puts("Site is up.") : kernel.puts("Site is down.")
      end
    end

    private

    attr_reader :http, :kernel
  end
end

The key difference between these two objects is that the former has several methods which expose data and/or behavior while the latter is purely functional in that you message a single public method, call, and get a result. No mutations or side effects. Only data in, data out.

The above examples are definitely not exhaustive and only represent a small subset of what is possible. The goal isn’t to explain all patterns and styles, only for you to study the shape of these objects. We can now compare/contrast by walking through the benefits that classes provide.

Composition

With both examples, you see an adherence to DIP where all dependencies are injected via the constructor.

With the presenter, a record is required because you can’t build a presenter with nothing to wrap. That would defeat the purpose.

With the command, default dependencies are injected so you can construct an instance with minimal effort. This is important because you want to create an instance you can reuse for different URIs to ping. This doesn’t stop you from injecting http and kernel dependencies with different behavior either. This is most apparent when writing specs. For example, we can use RSpec to stub HTTP and spy on Kernal like so:

RSpec.describe Pinger do
  subject(:pinger) { described_class.new http:, kernel: }

  let(:http) { class_double HTTP }
  let(:kernel) { class_spy Kernel }

  describe "#call" do
    it "prints site is up" do
      allow(http).to receive(:get).and_return(HTTP::Response)

      expect(kernel).to have_received(:puts).with("Site is up.")
    end

    it "prints site is down" do
      allow(http).to receive(:get).and_return(HTTP::Response)

      expect(kernel).to have_received(:puts).with("Site is down.")
    end
  end
end

Notice, by allowing dependencies to be injected, we can inject a spy for testing purposes.

A common mistake is hard coding dependencies directly within the body of the class. This leads to a Connascence of Name (CoN) situation where the class becomes brittle and hard to test because you have to keep altering the implementation to adjust behavior. With dependency injection, you avoid CoN while providing room for different dependencies to alter behavior.

Inheritance

Inheritance is a double edged sword because subclassing, or worse, creating a deep hierarchy of subclasses, leads to highly coupled and brittle code. Same goes for multiple inheritance where too much behavior is injected into a class thus breaking SRP where the object has confusing behavior that is hard to test.

With the presenter, multiple inheritance is used to delegate messages to the injected record via the Forwardable module. This is a good use of multiple inheritance because we are enhancing the object without effecting the external behavior of the object or violating SRP.

We could lean into multiple inheritance further by using the Initable gem to clean up the examples as shown below:

Presenter

require "forwardable"
require "initable"

module Demo
  class Place
    include Initable[%i[req record]]
    extend Forwardable

    delegate %i[name created_at] => :record

    def label = name.capitalize

    def human_created_at = created_at.strftime "%B %d, %Y (%A) at %H:%M %Z"
  end
end

Command

require "http"
require "initable"

module Demo
  class Pinger
    include Initable[http: HTTP, kernel: Kernel]

    def call uri
      http.get(uri).then do |response|
        response.status.success? ? kernel.puts("Site is up.") : kernel.puts("Site is down.")
      end
    end
  end
end

Notice how the original implementations require less code by using Initable to automatically DRY up and apply the Barewords Pattern for us. With the presenter, we now have multiple inheritance via Forwardable and Initable. What’s significant is we are not violating SRP by exposing additional public behavior. Instead, we are privately simplifying the implementation while writing less code! This is a good example of multiple inheritance applied appropriately.

You can definitely take this to extremes by including, extending, and/or prepending multiple modules which leads to a form of Dissociative Identity Disorder with your objects due to confusing behavior that is hard to reason about and test. Please avoid this.

Encapsulation

Both examples adhere to the Barewords Pattern by ensuring the Object API is private by default to prevent leaky abstractions. When implementing classes — and this is important — always default to making your attributes private by default. Only expose data when you are absolutely certain it’s needed.

Did you notice you can’t access the record directly in the presenter above? Same goes for the command where you can’t access any of the injected primitives either. This is good encapsulation!

A properly encapsulated objects does two things:

  • Data: In the basic example, the record is the encapsulated data. In the functional example, the data is the URI passed in via the #call method. The only dependencies are HTTP and Kernel which can be swapped out with different implementations at object initialization.

  • State: Only the functional example has state — but it’s brief — in that it makes an HTTP request and, for a short moment, has a response object which contains the status. Upon subsequent requests, the response is created anew, only to check status, and then is scheduled for garbage collection after.

Modularity

In both the presenter and command examples, both are highly modular in that they one thing well: present or perform an action. That’s it.

With the presenter, yes, you have multiple methods you can interact with but they are all focused on presenting your record in manner that is visually pleasing within your views.

With the command, you have a single Object API: #call. You give it a URI and it answers with a messaging letting you know if your site is up or down. Even better, you can use Function Composition to pipe the output of this command to other commands for further manipulation as needed. This is the beauty of functional programming because, once you have the building blocks in place, you can mix and match in any order to build more sophisticated solutions.

Reusability

Both examples have varying degrees of reusability. With the presenter, you can wrap any record that has the same attributes without changing the original design of your presenter.

With the command example, you can always supply a different uri to the same instance since the uri is what will change the most, not your instance. Even better, you can inject different dependencies to create new instances with slightly different behavior with no change to your original implementation either.

Avoidances

The greatest strength, and weakness, of Ruby is extreme flexible. Ruby treats you with respect by assuming you know what you are doing. Unfortunately, many programmers don’t fully grasp this — or worse, are an Expert Beginner — which turns a beautiful language into an absolute abomination to read and maintain.

To paraphrase Uncle Ben from Spiderman: "With great power comes great responsibility." Use the following to avoid common pitfalls and wield your power responsibly:

The above will vastly improve the quality of your code and reduce your maintenance burden to boot.

Conclusion

You’ve learned the power of Ruby classes and the potentially they can bring to your design and architecture. This includes the power of functional programming in Ruby by blending object-oriented with functional programming for maximum effect.

Be diligent in your design, apply the right objects when appropriate, and you’ll have few regrets. Enjoy!