Models in the context of an application’s business logic are objects that represent core entities and concepts. They encapsulate the data and behavior related to these entities and are usually persisted in some form of storage. In a typical Rails app, Active Record instances are often referred to as models. However, in this refactoring, we introduce a new Model layer that is distinct from Active Record and provides immutability.

One key difference between Models and Active Record instances (Records) is that Models are immutable. Once instantiated, Models hold their attributes immutably and do not have the capability to create or update information in the persistence layer. They represent the state of the entities at a specific point in time and serve as read-only representations of the data.

To hide Active Record completely from other areas of the app, Repositories are introduced. Repositories serve as the intermediaries between the Models and the persistence layer. They provide a subset of methods for querying and persisting data, abstracting away the complexity of Active Record operations. Instead of directly accessing Records, other parts of the app interact with Repositories, which in turn return read-only Models.

In Ruby 3.2, the new Data class was introduced as a way to define simple immutable value objects. A value object represents a specific value or concept in the application and is designed to be small, self-contained, and immutable. The Data class provides a clear and explicit way to define these value objects.

To create a Data object, we define a class using the Data class and specify the instance variables that the object will contain. For example, we can define our Bikes Model class with id and name:

module Bikes
  class Model < Data.define(:id, :name)
  end
end

I have to remember to update repository:

require_relative "../db/records/bike"
require_relative "./model"
require "active_record"

module Bikes
  class RecordNotFound < StandardError
  end

  class Repository
    def initialize
      setup_database
    end

    def all
      Db::Records::Bike.all.map { |record| to_model(record).to_h }
    end

    def create(name:)
      record = Db::Records::Bike.create(name: name)
      to_model(record).to_h
    end

    def find(id:)
      record = Db::Records::Bike.find(id)
      to_model(record).to_h
    rescue ActiveRecord::RecordNotFound
      raise RecordNotFound.new
    end

    def update(id:, params:)
      record = Db::Records::Bike.find(id)
      record.update(params)
      to_model(record).to_h
    rescue ActiveRecord::RecordNotFound
      raise RecordNotFound.new
    end

    def delete(id:)
      record = Db::Records::Bike.find(id)
      record.destroy!
    rescue ActiveRecord::RecordNotFound
      raise RecordNotFound.new
    end

    private

    def setup_database
      ActiveRecord::Base.configurations = YAML.load_file("db/configuration.yml")
      ActiveRecord::Base.establish_connection(ENV["RACK_ENV"].to_sym)
    end

    def to_model(record)
      Bikes::Model.new(id: record.id, name: record.name)
    end
  end
end

Once an instance of a Data object is created, its instance variables cannot be changed, ensuring that the object’s state remains constant. This immutability provides benefits in terms of code safety and predictability, as unexpected changes to the object’s state are prevented.

While Struct can also be used to define objects, the Data class offers additional safety checks. For example, Data prevents objects from being created with missing arguments, whereas Struct allows this. This enhances code safety and helps to prevent bugs. The explicitness of the Data class also makes it clear that the objects are intended to be immutable, contributing to code clarity.

In conclusion, the introduction of a Model layer separate from Active Record instances allows for greater encapsulation and immutability in the application’s business logic. By leveraging the Data class, simple immutable value objects can be defined, providing safety, performance, and clarity benefits. Understanding how Data works and its differences from other techniques, such as Struct, enables developers to make informed decisions when designing their codebase.

By embracing the Model layer and utilizing the Data class, developers can enhance the maintainability, readability, and reliability of their applications, ultimately leading to more robust and scalable software solutions.

The whole code where I added models into the app is on github