3 Ways to Create Classes in Ruby

5 min read

Classes are first-class objects in Ruby. All classes happen to be instances of Class. In other words, classes are Class objects just like 'hello' and 'world' are String objects.

Out of the box, there are 3 ways to create classes in Ruby.

Use the class Keyword

In the vast majority of the cases, you would use the class keyword to create a class. This is usually called a class definition:

class Customer
  attr_reader :full_name, :card_type, :card_number

  def initialize(full_name, card_type, card_number)
    @full_name = full_name
    @card_type = card_type
    @card_number = card_number
  end
end

# Creating and using a Customer object:
customer = Customer.new('Thuva Tharma', 'visa', 1234)

customer.full_name #=> Thuva Tharma
customer.card_type #=> visa
customer.card_number #=> 1234

If you're a Rubyist, you've seen and done this so many times. So, let's move on.

Use Class.new

Just like how we created a customer with Customer.new in the example above, we can create a class with Class.new:

Customer = Class.new do
  attr_reader :full_name, :card_type, :card_number

  def initialize(full_name, card_type, card_number)
    @full_name = full_name
    @card_type = card_type
    @card_number = card_number
  end
end

This is functionally equivalent to regular class definition, but more explicit.

By convention, classes are referenced by constants in Ruby. This is why we're assigning the object returned by Class.new to Customer constant, but we can assign it to a regular variable as well. On the other hand, if you don't use a constant in a regular class definition, you will get an error:

SyntaxError: class/module name must be CONSTANT

Now, why would you ever want to use Class.new over regular class definition? Let's look at creating a custom error class (for good reason, custom error classes should inherit from StandardError):

class CustomerError < StandardError
end

# This usually gets shortened as:
class CustomerError < StandardError; end

You can pass in an argument to Class.new to specify the super class for the class you're creating. With this in mind, you can define CustomerError more elegantly:

CustomerError = Class.new(StandardError)

This is such a minor improvement, but a good use-case nonetheless. Another use-case for Class.new might be in testing. If you need to define classes only to be used by your tests, you don't want to pollute the global namespace with the test class constants. You can use Class.new to store the test classes in local or instance variables.

Use Struct

Struct is available as part of Ruby's standard library, and you don't need to require anything to use it. Struct lets you create classes for data container objects without any boilerplate:

Customer = Struct.new(:full_name, :card_type, :card_number)

This has all the features of Customer class we created with regular class definition as well as using Class.new above. In addition, the customer objects will have equality as per value object semantics:

customer1 = Customer.new('Thuva Tharma', 'visa', 1234)
customer2 = Customer.new('Thuva Tharma', 'visa', 1234)
customer3 = Customer.new('Thuva Tharma', 'amex', 1234)

customer1 == customer2 #=> true
customer1 == customer3 #=> false

On the other hand, we lose arity checking for arguments passed in to Customer.new. In other words, you don't get ArgumentError if you pass in less arguments:

# full_name, card_type, card_number are all nil
Customer.new

# card_type, card_number are nil
Customer.new('Thuva Tharma')

# card_number is nil
Customer.new('Thuva Tharma', 'visa')

# full_name, card_type, card_number are all present
Customer.new('Thuva Tharma', 'visa', 1234)

This is not good. But, if you limit using Struct to create inner classes for value objects, it's still safe and useful:

class Customer
  # Internal: Class to create credit card value objects
  CreditCard = Struct.new(:holder, :type, :number)

  attr_reader :full_name, :card

  def initialize(full_name, card_type, card_number)
    @full_name = full_name
    @card = CreditCard.new(full_name, card_type, card_number)
  end
end

Conclusion

In Ruby, you can create classes using the class keyword (class definition), Class.new, and Struct. Although you would use regular class definition in the vast majority of cases, Class.new and Struct would sometimes come in handy if you know how to use them.