Book Image

Polished Ruby Programming

By : Jeremy Evans
Book Image

Polished Ruby Programming

By: Jeremy Evans

Overview of this book

Anyone striving to become an expert Ruby programmer needs to be able to write maintainable applications. Polished Ruby Programming will help you get better at designing scalable and robust Ruby programs, so that no matter how big the codebase grows, maintaining it will be a breeze. This book takes you on a journey through implementation approaches for many common programming situations, the trade-offs inherent in each approach, and why you may choose to use different approaches in different situations. You'll start by refreshing Ruby fundamentals, such as correctly using core classes, class and method design, variable usage, error handling, and code formatting. Then you'll move on to higher-level programming principles, such as library design, use of metaprogramming and domain-specific languages, and refactoring. Finally, you'll learn principles specific to web application development, such as how to choose a database and web framework, and how to use advanced security features. By the end of this Ruby programming book, you’ll be a well rounded web developer with a deep understanding of Ruby. While most code examples and principles discussed in the book apply to all Ruby versions, some examples and principles are specific to Ruby 3.0, the latest release at the time of publication.
Table of Contents (23 chapters)
1
Section 1: Fundamental Ruby Programming Principles
8
Section 2: Ruby Library Programming Principles
17
Section 3: Ruby Web Programming Principles

Working with Struct – one of the underappreciated core classes

The Struct class is one of the underappreciated Ruby core classes. It allows you to create classes with one or more fields, with accessors automatically created for each field. So, say you have the following:

class Artist
  attr_accessor :name, :albums
  def initialize(name, albums)
    @name = name
    @albums = albums
  end
end

Instead of that, you can write a small amount of Ruby code, and have the initializer and accessor automatically created:

Artist = Struct.new(:name, :albums)

In general, a Struct class is a little lighter on memory than a regular class, but has slower accessor methods. Struct used to be faster in terms of both initialization and reader methods in older versions of Ruby, but regular classes and attr_accessor methods have gotten faster at a greater rate than Struct has. Therefore, for maximum performance, you may want to consider using regular classes and attr_accessor methods instead of Struct classes.

One of the more interesting aspects of Struct is how it works internally. For example, unlike the new method for most other classes, Struct.new does not return a Struct instance; it returns a Struct subclass:

Struct.new(:a, :b).class
# => Class

However, the new method on the subclass creates instances of the subclass; it doesn't create future subclasses. Additionally, if you provide a string and not a symbol as the first argument, Struct will automatically create the class using that name nested under its own namespace:

Struct.new('A', :a, :b).new(1, 2).class
# => Struct::A

A simplified version of the default Struct.new method is similar to the following. This example is a bit larger, so we'll break it into sections. If a string is given as the first argument, it is used to set the class in the namespace of the receiver; otherwise, it is added to the list of fields:

def Struct.new(name, *fields)
  unless name.is_a?(String)
    fields.unshift(name)
    name = nil
  end

Next, a subclass is created. If a class name was given, it is set as a constant in the current namespace:

  subclass = Class.new(self)
  if name
    const_set(name, subclass)
  end

Then, some internal code is run to set up the storage for the members of the subclass. Then, the new, allocate, [], members, and inspect singleton methods are defined on the subclass. Finally, some internal code is run to set up accessor instance methods for each member of the subclass:

  # Internal magic to setup fields/storage for subclass
  def subclass.new(*values)
    obj = allocate
    obj.initialize(*values)
    obj
  end
  # Similar for allocate, [], members, inspect
  # Internal magic to setup accessor instance methods
  subclass
end

Interestingly, you can still create Struct subclasses the normal way:

class SubStruct < Struct
end

Struct subclasses created via the normal way operate like Struct itself, not like Struct subclasses created via Struct.new. You can then call new on the Struct subclass to create a subclass of that subclass, but the setup is similar to a Struct subclass created via Struct.new:

SubStruct.new('A', :a, :b).new(1, 2).class
# => SubStruct::A

In general, Struct is good for creating simple classes that are designed for storing data. One issue with Struct is that the design encourages the use of mutable data and discourages a functional approach, by defaulting to creating setter methods for every member. However, it is possible to easily force the use of immutable structs by freezing the object in initialize:

A = Struct.new(:a, :b) do
  def initialize(...)
    super
    freeze
  end
end

There have been feature requests submitted on the Ruby issue tracker to create immutable Struct subclasses using a keyword argument to Struct.new or via the addition of a separate Struct::Value class. However, as of Ruby 3, neither feature request has been accepted. It is possible that a future version of Ruby will include them, but in the meantime, freezing the receiver in initialize is the best approach.