Ruby: Different ways of assigning multiple attributes

Updated . Posted . Visible to the public. Repeats.

This card is a short summary on different ways of assigning multiple attributes to an instance of a class.

Using positional parameters

Using parameters is the default when assigning attributes. It works good for a small number of attributes, but becomes more difficult to read when using multiple attributes.

Example:

class User
  def initialize(salutation, first_name, last_name, street_and_number, zip_code, city, phone_number, email, newsletter)
    @salutation = salutation
    @first_name = first_name
    @last_name = last_name
    @street_and_number = street_and_number
    @zip_code = zip_code
    @city = city
    @phone_number = phone_number
    @email = email
    @newsletter = newsletter
  end
end

User.new(
  'Mr.',
  'John',
  'Doe',
  'Potsdamer Platz 1',
  '10117',
  'Berlin',
  '+49 0151 1122334455',
  'john.doe@example.com',
  true
)

Using keyword arguments

Using keyword arguments is easier for others to instantiate the class without knowing the correct attribute order in the constructor. On the other hand people try to avoid long lines and breaking method arguments on new lines is used seldom.

Example:

class User
  def initialize(salutation:, first_name:, last_name:, street_and_number:, zip_code:, city:, phone_number:, email:, newsletter:)
    @salutation = salutation
    @first_name = first_name
    @last_name = last_name
    @street_and_number = street_and_number
    @zip_code = zip_code
    @city = city
    @phone_number = phone_number
    @email = email
    @newsletter = newsletter
  end
end

User.new(
  salutation: 'Mr.',
  first_name: 'John',
  last_name: 'Doe',
  street_and_number: 'Potsdamer Platz 1',
  zip_code: '10117',
  city: 'Berlin',
  phone_number: '+49 0151 1122334455',
  email: 'john.doe@example.com',
  newsletter: true
)

Example with breaking method arguments on multiple lines:

class User
  def initialize(
    salutation:,
    first_name:,
    last_name:,
    street_and_number:,
    zip_code:,
    city:,
    phone_number:,
    email:,
    newsletter:
  )
    @salutation = salutation
    @first_name = first_name
    @last_name = last_name
    @street_and_number = street_and_number
    @zip_code = zip_code
    @city = city
    @phone_number = phone_number
    @email = email
    @newsletter = newsletter
  end
end

Using ActiveType::Object or ActiveModel::Attributes

Enhancing a class with ActiveType::Object or ActiveModel::Attributes makes the attributes clearer visible and adds the ability for validations and type casting. On the other hand there is no build-in way to ensure that e.g. all attributes need to be present when initializing an object.

Example:

class User
  include ActiveModel::Attributes
  
  attribute :salutation
  attribute :first_name
  attribute :last_name
  attribute :street_and_number
  attribute :zip_code
  attribute :city
  attribute :phone_number
  attribute :email
  attribute :newsletter, :boolean
end

User.new(
  salutation: 'Mr.',
  first_name: 'John',
  last_name: 'Doe',
  street_and_number: 'Potsdamer Platz 1',
  zip_code: '10117',
  city: 'Berlin',
  phone_number: '+49 0151 1122334455',
  email: 'john.doe@example.com',
  newsletter: true
)

Using a hash argument

Using a hash argument allows you to assign multiple attributes without any kind of definition. But it reduces the ability for others to understand the necessary or allowed arguments at a first glance. Therefore sometimes people delete the attributes from the hash and raise an exception in case attributes are present after the initialization. It's possible to use Hash#fetch to ensure an attribute must be present.

Example:

class User
  def initialize(**attributes)
    @salutation = attributes.delete(:salutation)
    @first_name = attributes.delete(:first_name)
    @last_name = attributes.delete(:last_name)
    @street_and_number = attributes.delete(:street_and_number)
    @zip_code = attributes.delete(:zip_code)
    @city = attributes.delete(:city)
    @phone_number = attributes.delete(:phone_number)
    @email = attributes.delete(:email)
    @newsletter = attributes.delete(:newsletter)

    if attributes.present?
      raise(ArgumentError, "Invalid attributes found #{attributes.inspect}")
    end
  end
end

User.new(
  salutation: 'Mr.',
  first_name: 'John',
  last_name: 'Doe',
  street_and_number: 'Potsdamer Platz 1',
  zip_code: '10117',
  city: 'Berlin',
  phone_number: '+49 0151 1122334455',
  email: 'john.doe@example.com',
  newsletter: true
)

Using a struct with keyword_init

Using a struct with keyword_init gives you the benefit of default attribute accessors. But it makes it harder to read in case you need to add custom methods as block argument or modify the values during the initialization.

Example:

class User < Struct.new(
    :salutation,
    :first_name,
    :last_name,
    :street_and_number,
    :zip_code,
    :city,
    :phone_number,
    :email,
    :newsletter,
    keyword_init: true
  )
end

User.new(
  salutation: 'Mr.',
  first_name: 'John',
  last_name: 'Doe',
  street_and_number: 'Potsdamer Platz 1',
  zip_code: '10117',
  city: 'Berlin',
  phone_number: '+49 0151 1122334455',
  email: 'john.doe@example.com',
  newsletter: true
)

Tip

In Ruby 3.2+ you can also use Data Show archive.org snapshot as a convenient way to define simple classes for value-alike objects.

Using ordered options or open struct

With some subtile differences Show archive.org snapshot the OrderedOptions and OpenStruct can help when assigning multiple attributes. They both have the disadvantage that their content is arbitrary, but they are a lightweight way to pass data through different layers.

Example 1:

require 'ostruct'

user = OpenStruct.new(
  salutation: 'Mr.',
  first_name: 'John',
  last_name: 'Doe',
  street_and_number: 'Potsdamer Platz 1',
  zip_code: '10117',
  city: 'Berlin',
  phone_number: '+49 0151 1122334455',
  email: 'john.doe@example.com',
  newsletter: true
)

Example 2:

class User
  def initialize(salutation:, first_name:, last_name:, street_and_number:, zip_code:, city:, phone_number:, email:, newsletter:)
    @salutation = salutation
    @first_name = first_name
    @last_name = last_name
    @street_and_number = street_and_number
    @zip_code = zip_code
    @city = city
    @phone_number = phone_number
    @email = email
    @newsletter = newsletter
  end
end

user_attributes = OpenStruct.new(
  salutation: 'Mr.',
  first_name: 'John',
  last_name: 'Doe',
  street_and_number: 'Potsdamer Platz 1',
  zip_code: '10117',
  city: 'Berlin',
  phone_number: '+49 0151 1122334455',
  email: 'john.doe@example.com',
  newsletter: true
)

User.new(**user_attributes)
Last edit
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Emanuel to makandra dev (2024-07-19 08:05)