Via Láctea

Tirei essa do meu celular, mas é uma foto da Via Láctea. Também tem um pouco de perspectiva artística. :)

A classe Data foi adicionada na versão 3.2 do Ruby para representar objetos imutáveis que contêm um conjunto simples de valores. Ela é semelhante à classe Struct do Ruby, mas possui uma API mais simples e restrita.

Vou tentar explicar o que é a classe Data e como ela pode ser útil para de uma perspectiva prática visando conceitos que estão relacionados com o paradigma funcional como imutabilidade, composição e funções puras.

Criando uma classe Data

Para criar uma classe Data, você precisa definir um nome para a classe e uma lista de atributos. Você pode fazer isso usando o método Data.define:

Person = Data.define(:name, :age)

Com isso, você pode criar uma nova instância da classe Person passando os valores para os atributos:

person = Person.new("John", 30)

Você pode acessar os atributos de uma instância da classe Data usando os métodos de acesso:

person.name # => "John"
person.age # => 30

Imutabilidade

A classe Data é imutável, o que significa que você não pode alterar os valores dos atributos de uma instância. Se você tentar, uma exceção será lançada:

person.name = "Jane" # => RuntimeError: can't modify frozen Data

Composição

Você pode usar a classe Data para criar novas classes que representam objetos mais complexos. Por exemplo.

Address = Data.define(:street, :city, :state, :zip)
# => Address

address = Address.new("Avenida Frei Serafim", "Teresina", "PI", "64001-020")
# => #<data Address street="Avenida Frei Serafim", city="Teresina", state="PI", zip="64001-020">

Person = Data.define(:name, :age, :address)
# => Person

person = Person.new("John", 30, address)
# => #<data Person name="John", age=30, address=#<data Address street="Avenida Frei Serafim", city="Teresina", state="PI", zip="64001-020">>

Características funcionais

Person = Data.define(:name, :age) 

module PersonModule
  def self.older_than?(person, age)
    person.age > age
  end 
end 

# Usando o módulo
person = Person.new('John', 30) 

is_older = PersonModule.older_than?(person, 25) 

puts "#{person.name} é mais velho que 25 anos? #{is_older}" 
# => John é mais velho que 25 anos? true

Neste exemplo conseguimos perceber bem conceitos como:

  • High Order Functions: No exemplo, o módulo com uma função older_than?, é usada como “cidadã de primeira classe” no código, ou seja, ela pode ser passada como parâmetro para outra função. Ex.:
def do_something_with_person(person, function)
  function.call(person)
end

do_something_with_person(person, PersonModule.older_than?)
  • Imutabilidade: Como a classe Data é imutável, não é possível alterar os valores dos atributos de uma instância. Isso garante que as funções puras não alterem o estado do objeto. Ex.:
def increment_age(person)
  person.age += 1
end

increment_age(person) # => RuntimeError: can't modify frozen Data
  • Composição: A classe Data pode ser usada para criar novas classes que representam objetos mais complexos. Isso permite que você crie objetos compostos por outros objetos, mas que não possuem uma relação de herança. Como no exemplo acima, onde a classe Person possui um atributo do tipo Address. :)

Caso de uso

A class data pode ser usada para representar objetos que são retornados por funções como interfaces de acesso a dados. Isso remete ao conceito de “Data Transfer Objects” (DTOs). Ex.:

module UserModel
  # Representação resumida de um usuário
  User = Data.define(:id, :name, :email)

  # Imaginemos que aqui temos uma consulta ao banco de dados para encontrar o usuário com o ID especificado
  def self.find(id)
    user_data = database.query("SELECT * FROM users WHERE id = #{id}")     
    User.new(user_data)
  end

  # Imaginemos que aqui temos uma consulta ao banco de dados para criar um novo usuário.
  def self.create(name, email)
    result = database.query("INSERT INTO users (name, email) VALUES (#{name}, #{email})")
    User.new(id: result.insert_id, name: name, email: email)
  end
end
require 'user_model' 

# Busca o usuário com o ID 1 
user = UserModel.find(1) puts user.name 
# => "João"

# Cria um novo usuário com o nome "João" e o email "[email protected]" 
new_user = UserModel.create("João", "[email protected]") 

puts new_user.id
# => 1

Isso traz alguns insights interessantes:

  • Diminui a quantidade de dados que precisam ser passados entre as camadas da aplicação.
  • Garante que os dados que estão sendo passados entre as camadas estão no formato correto.
  • Estabelece contratos entre as camadas e etc.

Mas isso é assunto para outro post. :)

Conclusão

Eu não diria que é necessário se prender a esses conceitos para escrever um código funcional em Ruby, talvez você nem esteja pensando nisso, mas é interessante conhecer as ferramentas que o Ruby oferece. A classe Data é uma delas.

É interessante perceber que o time de desenvolvimento do Ruby está sempre preocupado em como melhorar a linguagem e trazer novas ferramentas para o desenvolvedor. A classe Data é um exemplo disso.

Referências