Structs in Ruby
What are Structs?
You’ve probably seen Structs in some languages and may be wondering what they are, where they come from, and what they’re used for. Structs originate from C and are data structures used to group attributes. Ruby, inspired by this concept, created its own implementation of Struct, which, in my opinion, is superior.
In Ruby, a Struct is a class that creates new subclasses in which we can insert attributes and even methods. Therefore, the result is an object. I like to describe Structs in Ruby as simplified classes.
Creating a Struct
Creating a Struct in Ruby is very simple. Let’s create the initial structure now:
Struct.new("Fruits", :name, :price)
=> Struct::Fruits
Note that irb returned the Fruit class that was created.
A simplified way to create a Struct is to assign it directly to a constant that will be the class name:asse:
Fruits = Struct.new(:name, :price)
This way, the Fruit
class is created without the Struct::
namespace, which, in my opinion, is more elegant.
Now we can instantiate the class to assign values to name
and price
:
banana = Fruits.new("Banana", "5.00")
Let’s check what the “banana” variable returns:
=> #<struct Fruits name="Banana", price="5.00">
It’s an object, so we can access the attributes:
banana.price
=> "5.00"
We can also assign new values:
banana.price = 8.00
banana.price
=> 8.0
Creating Methods in a Struct
Let’s make things more interesting by creating Structs with methods:
Fruit = Struct.new(:name, :price) do
def announce
"Buy #{name} for just $#{price}!"
end
end
banana = Fruit.new("banana", "5.00")
banana.announce=> "Buy banana for just $5.00!"
Passing a Struct Instead of a Hash
It’s common to need to pass many data arguments when instantiating a class. However, it’s not a good practice to pass more than 3 arguments. See the example:
BuyFruit.new("Banana", "8.0", "Marcos", :credit_card).call
This can become confusing and hard to understand. Typically, the first solution that comes to mind is to pass a hash to resolve it:
BuyFruit.new(fruit_name: "Banana", fruit_price: "8.0", customer: "Marcos", payment_method: :credit_card).call
We can improve this:
detail = {
fruit_name: "Banana",
fruit_price: "8.0",
customer: "Marcos",
payment_method: :credit_card
}
BuyFruit.new(detail).call
It’s acceptable, but I’d like to propose something even better. Let’s encapsulate this data in a Struct instead of a Hash:
Details = Struct.new(:fruit_name, :fruit_price, :customer, :payment_method)
detail = Details.new("Banana", "8.0", "Marcos", :credit_card)
BuyFruit.new(detail).call
Going Further
We can further separate the data to make it more semantic:
class BuyFruit
def initialize(fruit, customer)
@fruit = fruit
@customer = customer
end
def call
StockRemove.new(@fruit).call
PaymentGateway.new(@fruit, @customer).call
end
end
Fruits = Struct.new(:name, :price, keyword_init: true)
Customers = Struct.new(:name, :payment_method, keyword_init: true)fruit = Fruits.new(name: "Banana", price: "8.0")
customer = Customers.new(name: "Marcos", payment_method: :credit_card)BuyFruit.new(fruit, customer).call
Note that we’ve written more code than simply passing the parameters. However, besides making the code more semantic, you gain the advantage of not having to pass individual data through multiple layers of the application. What you’re passing are just two objects. The BuyFruit class doesn’t need to know what’s inside these objects; it simply passes them to more concrete classes.
In Conclusion
In summary, Structs in Ruby offer a powerful and elegant way to create data structures and objects with attributes, methods, and clear semantics. By encapsulating data in Structs and using them efficiently, you can simplify your code, make it more readable, and facilitate the passing of information between layers of an application, contributing to more organized and effective development.