RubyMiddleTechnical
Что такое Struct и OpenStruct в Ruby и когда их стоит использовать?
Struct — быстрый генератор классов с фиксированными именованными атрибутами, поддерживающий ==, to_a, members. OpenStruct — динамический объект с произвольными атрибутами, медленнее и небезопаснее. В Ruby 3.2+ предпочитайте Data.define для иммутабельных значений.
Struct
Struct — встроенный генератор классов значений с заданным набором полей. Генерирует конструктор, геттеры/сеттеры, ==, to_a, to_h, members, each.
Point = Struct.new(:x, :y)
p1 = Point.new(1, 2)
p1.x # => 1
p1.to_h # => {x: 1, y: 2}
p1.to_a # => [1, 2]
p2 = Point.new(1, 2)
p1 == p2 # => true (сравнивает по значению)
# Keyword-аргументы (Ruby 2.5+)
Point = Struct.new(:x, :y, keyword_init: true)
p = Point.new(x: 10, y: 20)
Struct с методами
Person = Struct.new(:first_name, :last_name) do
def full_name
"#{first_name} #{last_name}"
end
def to_s
full_name
end
end
alice = Person.new("Alice", "Smith")
puts alice.full_name # => "Alice Smith"
OpenStruct
OpenStruct из stdlib (require 'ostruct') — объект с динамически создаваемыми атрибутами. Атрибуты задаются при первом присваивании:
require 'ostruct'
user = OpenStruct.new(name: "Bob", age: 30)
user.name # => "Bob"
user.email = "bob@example.com" # новый атрибут
user.respond_to?(:email) # => true
# Опасность: опечатка не вызывает ошибку
user.naem # => nil (а не NoMethodError!)
Data.define (Ruby 3.2+)
Иммутабельный аналог Struct — все атрибуты только для чтения, объект автоматически заморожен:
Coordinate = Data.define(:lat, :lng)
c = Coordinate.new(lat: 55.75, lng: 37.62)
c.lat # => 55.75
c.frozen? # => true
# c.lat = 1 # => FrozenError
# with — создать копию с изменёнными полями
c2 = c.with(lat: 56.0)
Сравнение
- Struct: фиксированные поля, изменяемый, быстрый, имеет
==по значению. Для DTO, value objects, простых записей. - OpenStruct: динамические поля, медленный (использует
method_missing+ хэш), нет статической проверки. Избегайте в production-коде. - Data.define: фиксированные поля, иммутабельный, keyword-only, идеален для событий/команд/value objects.
Когда использовать
Struct— когда нужен быстрый класс-запись с небольшим числом полей и сравнением по значению (координаты, результаты запросов, тесты).OpenStruct— прототипирование, тесты (фейковые объекты), скрипты. Никогда в hot path production-кода.Data.define— иммутабельные value objects: деньги, координаты, доменные события.
Подводные камни
OpenStructработает черезmethod_missingи динамически определяет методы — первый доступ к атрибуту значительно медленнее последующих.- Опечатка в имени атрибута
OpenStructвозвращаетnilбез ошибки, что порождает трудноуловимые баги. Structбезkeyword_init: trueпринимает позиционные аргументы — лёгко перепутать порядок при большом числе полей.- Сравнение (
==) уStructсравнивает только поля, не учитывая подкласс: два разных Struct с одинаковыми полями не равны, но экземпляры одного Struct с одними значениями — равны. Struct.newбез присвоения константе создаёт анонимный класс — его нельзя десериализовать (Marshal) и сложно отлаживать.- В Ruby < 3.2 нет
Data.define— используйте frozen Struct или Dry::Struct из гема. - Наследование от
Structработает, но добавляет неочевидные сложности с==иmembers.
Common mistakes
- Сводить struct vs openstruct к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: объектная динамическая модель Ruby, где почти всё является объектом, а методы ищутся через ancestor chain.
- Не отделять validation, authorization, transaction boundary и business logic.
- Менять похожие API местами без учёта семантики ошибок и ownership.
What the interviewer is testing
- Объясняет struct vs openstruct через конкретную точку lifecycle в Ruby.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.