3 Ways to Create Classes in Ruby
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.