Ruby Struct to wbudowana klasa, która udostępnia użyteczne funkcjonalności. Możesz ją używać zarówno w logice biznesowej jaki i w kodzie testów. W tym artykule przejdę poprzez możliwości, które udostępnia Struct, porównam ją z innymi alternatywnymi rozwiązaniami oraz postaram się pokazać mniej znane ale nadal przydatne właściwości.

Podstawy

Employee = Struct.new(:first_name, :last_name)
employee = Employee.new("John", "Doe")
employee.first_name # => "John"
employee.last_name # => "Doe"

Jak możesz zauważyć, Struct zachwouje się jak prosta klasa Ruby. Powyższy kod działa tak samo jak ten zdefiniowany poniżej:

class Employee
  attr_reader :first_name, :last_name
 
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end
 
employee = Employee.new("John", "Doe")
...

Co jeżeli chcemy zdefiniować metodę #full_name w klasie Employee? Możemy to także wykonać z pomocą Struct:

Employee = Struct.new(:first_name, :last_name) do
  def full_name
    "#{first_name} #{last_name}"
  end
end
 
employee = Employee.new("John", "Doe")
employee.full_name # => "John Doe"

Kiedy używać Struct

Struct bardzo często używa się aby sformatować dane do bardziej ustrukturyzowanej formy lub jako zamiennika zwykłych klas w testach.

  • Tymczasowe struktury danych – najpopularniejszym przykładem jest przypadek danych geolokalizacyjnych z których możemy stworzyć obiekt Address, którego atrybuty będą zawierały te same dane co hash otrzymany w odpowiedzi.
  • Testowanie – tak długo jak Struct nawiązuje do tych samych metod czy atrybutów co testowany obiekt, możesz go użyć jeżeli w danej sytuacji ma to sens. Możesz rozważyć taką „podmianę” gdy testujesz wstrzykiwanie zależnośći (eng. dependency injection).

Kiedy unikać Struct

Unikaj dziedziczenia z Struct kiedy tylko możesz. Umyślnie przypisałem Struct do stałej zamiast użyć poniższego kodu:

class Employee < Struct.new(:first_name, :last_name)
  def full_name
    "#{first_name} #{last_name}"
  end
end

Kiedy twoja klasa dziedziczy z Struct, możesz nie zdawać sobie sprawy z poniższych następstw:

  • Argumenty nie są wymagane – jeżeli jeden z argumentów to obiekt, może dojść do sytuacji w której otrzymasz błąd ponieważ będziesz chciał wywołać na nim jakąś metodę a sam obiekt nie zostanie podany
  • Atrybuty są zawsze publiczne – taka enkapsulacja jest daleka od idealnej dopóki właśnie na tym ci nie zależy
  • Instancje są takie same gdy ich atrybuty mają tą samą wartośćEmployee.new == Employee.new
  • Struct nie chce być rodzicem – dziedziczenie z tej struktury tworzy nieużywaną anonimową klasę. Oficjalna dokumentacja wspomina o tym

Inne właściwości

Uzyskaj dostęp do atrybutów w dowolny sposób:

person = Struct.new(:first_name).new("John")
person.first_name # => "John"
person[:first_name] # => "John"
person["first_name"] # => "John"

Użyj operatora równości:

Person = Struct.new(:first_name)
Person.new("John") == Person.new("John") # => true

Iteruj przez wartości oraz pary:

Person = Struct.new(:first_name, :last_name)
person = Person.new("John", "Doe")
# Wartosci
 
person.each do |value|
  puts value
end
# >> "John"
# >> "Doe"
 
# Pary
 
person.each_pair do |key, value|
  puts "#{key}: #{value}"
end
# >> "first_name: John"
# >> "last_name: Doe"

„Dokop” się do wartości:

Address = Struct.new(:city)
Person = Struct.new(:name, :address)
address = Address.new("New York")
person = Person.new("John Doe", address)
 
person.dig(:address, :city) # => "New York"

Alternatywy

Hash

Hash jest często identyfikowany jako alternatywa dla Struct. Jest szybszy w użyciu ale ma gorszą wydajność (do wersji 2.5).

OpenStruct

OpenStruct jest wolniejszy ale bardziej elastyczny. Używając tej struktury, możesz przypisywać atrybuty dynamicznie a samo tworzenie obiektu nie wymaga podawania atrybutów. Możesz także przekazać hash z atrybutami:

employee = OpenStruct.new(first_name: "John", last_name: "Doe")
employee.first_name # => "John"
employee.age = 30
employee.age # => 30

Standardowa klasa

Częste używanie takiego zapisu może stać się uciążliwe i to największy minus tego rozwiązania:

class Employee
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
 
  def full_name
    "#{first_name} #{last_name}"
  end
end

Gemy

Jeżeli nie chcesz używać Struct lub alternatyw, zawsze możesz użyć gemu. https://github.com/tcrayford/Values oraz Smart Init to tylko niektóre z nich a jest ich o wiele więcej. Kwestia ta jednak zasługuje na osobny artykuł więc nie zapomnij zapisać się do newslettera aby zostać powiadomiony o publikacji następnej części.

Porównanie alternatyw

Testy zostały przeprowadzone na wersji 2.5:

Nazwa Nieistniejące atrybuty Dynamicznie dodaj atrybut Wydajność (niższy indeks jest lepszy)
Struct wyrzuca błąd nie 1
OpenStruct zwraca nil tak 3
Hash zwraca nil tak 2

Testy wydajnościowe:

Poniższy kod został użyty do porównania wydajności dostępnych rozwiązań:

Benchmark.bm 10 do |bench|
  bench.report "Hash: " do
    10_000_000.times do { name: "John Doe", city: "New York" } end
  end
 
  bench.report "Struct: " do
    klass = Struct.new(:name, :age)
    10_000_000.times do klass.new("John Doe", "New York") end
  end
 
  bench.report "Open Struct: " do
    10_000_000.times do OpenStruct.new(name: "John Doe", city: "New York") end
  end
end

Wyniki:

user system total real
Hash 7.380000 0.060000 7.440000 7.456928
Struct 3.280000 0.010000 3.290000 3.283013
Open Struct 19.270000 0.120000 19.390000 19.415336

Podsumowanie:

Open Struct to najwolniejsze i najbardziej elastycznie rozwiązanie w naszym porównaniu. Struct wydaje się być więc najlepszym wyborem.

Zdjęcie autorstwa Clem Onojeghuo z serwisu Pexels

Dodaj komentarz