Ruby do Zero: Classes e Objetos — Estado, Comportamento e Encapsulamento

Capítulo 17

Tempo estimado de leitura: 8 minutos

+ Exercício

O que são Classes e Objetos (na prática)

Em Ruby, classe é um molde que descreve estado (dados) e comportamento (ações). Um objeto é uma instância dessa classe: um “exemplar” com valores próprios.

  • Estado: atributos como nome, preco, quantidade.
  • Comportamento: métodos como aplicar_desconto, subtotal, adicionar_item.
  • Encapsulamento: proteger regras internas e expor apenas o necessário (via métodos públicos), evitando que qualquer parte do sistema altere o objeto de forma inválida.

Criando uma classe: estrutura mínima

Uma classe começa com class e termina com end. Dentro dela, você define métodos e atributos.

class Produto
end

produto = Produto.new

Até aqui, Produto não tem estado nem comportamento. Vamos evoluir para algo útil.

Inicialização com initialize: definindo o estado inicial

O método initialize é chamado automaticamente quando você faz Produto.new(...). Use-o para garantir que o objeto nasce em um estado válido.

class Produto
  def initialize(nome, preco_em_centavos)
    @nome = nome
    @preco_em_centavos = preco_em_centavos
  end
end

p1 = Produto.new("Camiseta", 7990)

Variáveis com @ são variáveis de instância (estado do objeto). Cada objeto tem as suas.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Passo a passo: adicionando validações no initialize

Regras de negócio devem impedir estados inválidos. Por exemplo: preço não pode ser negativo e nome não pode ser vazio.

class Produto
  def initialize(nome, preco_em_centavos)
    raise ArgumentError, "nome inválido" if nome.nil? || nome.strip.empty?
    raise ArgumentError, "preço inválido" if preco_em_centavos.nil? || preco_em_centavos < 0

    @nome = nome
    @preco_em_centavos = preco_em_centavos
  end
end

Assim, qualquer tentativa de criar um produto inválido falha cedo, no ponto certo.

Atributos: attr_reader, attr_writer e attr_accessor

Em Ruby, você pode expor atributos com métodos gerados automaticamente:

  • attr_reader: cria apenas leitura (getter).
  • attr_writer: cria apenas escrita (setter).
  • attr_accessor: cria leitura e escrita.

Exemplo com leitura pública e escrita controlada:

class Produto
  attr_reader :nome, :preco_em_centavos

  def initialize(nome, preco_em_centavos)
    raise ArgumentError, "nome inválido" if nome.nil? || nome.strip.empty?
    raise ArgumentError, "preço inválido" if preco_em_centavos.nil? || preco_em_centavos < 0

    @nome = nome
    @preco_em_centavos = preco_em_centavos
  end
end

Por que não usar attr_accessor para tudo? Porque permitir escrita livre pode quebrar regras. Se o preço precisa de validação, é melhor criar um método específico para alterar preço com segurança.

Setter com regra (em vez de attr_writer)

class Produto
  attr_reader :nome, :preco_em_centavos

  def initialize(nome, preco_em_centavos)
    @nome = nome
    definir_preco(preco_em_centavos)
  end

  def definir_preco(novo_preco_em_centavos)
    raise ArgumentError, "preço inválido" if novo_preco_em_centavos.nil? || novo_preco_em_centavos < 0
    @preco_em_centavos = novo_preco_em_centavos
  end
end

Note o nome: definir_preco comunica intenção e permite validação.

Métodos de instância: comportamento do objeto

Métodos de instância operam sobre o estado do próprio objeto (usando @variaveis).

class Produto
  attr_reader :nome, :preco_em_centavos

  def initialize(nome, preco_em_centavos)
    @nome = nome
    @preco_em_centavos = preco_em_centavos
  end

  def preco_em_reais
    @preco_em_centavos / 100.0
  end
end

p = Produto.new("Caneca", 2590)
puts p.preco_em_reais

Um bom sinal de design: métodos pequenos, com nomes descritivos e responsabilidade clara.

Modelagem prática: Produto e Carrinho com regras de negócio

Vamos modelar um cenário simples: um carrinho que recebe itens (produto + quantidade), calcula subtotal, aplica cupom e calcula total. A ideia é mostrar estado, comportamento e encapsulamento.

1) Criando Produto com desconto controlado

Regra: desconto percentual deve estar entre 0 e 30% (por exemplo, para evitar descontos “livres” em qualquer parte do sistema).

class Produto
  attr_reader :nome, :preco_em_centavos

  DESCONTO_MAXIMO_PERCENTUAL = 30

  def initialize(nome, preco_em_centavos)
    raise ArgumentError, "nome inválido" if nome.nil? || nome.strip.empty?
    raise ArgumentError, "preço inválido" if preco_em_centavos.nil? || preco_em_centavos <= 0

    @nome = nome
    @preco_em_centavos = preco_em_centavos
  end

  def aplicar_desconto(percentual)
    validar_percentual_de_desconto(percentual)

    desconto = (@preco_em_centavos * percentual / 100.0).round
    @preco_em_centavos -= desconto
  end

  private

  def validar_percentual_de_desconto(percentual)
    raise ArgumentError, "percentual inválido" if percentual.nil?
    raise ArgumentError, "desconto deve ser entre 0 e #{DESCONTO_MAXIMO_PERCENTUAL}" if percentual < 0 || percentual > DESCONTO_MAXIMO_PERCENTUAL
  end
end

A validação ficou em um método private, porque é um detalhe interno: quem usa Produto só precisa saber que existe aplicar_desconto.

2) Criando ItemDeCarrinho para encapsular produto + quantidade

Em vez de guardar hashes soltos no carrinho, criamos um objeto com responsabilidade clara.

class ItemDeCarrinho
  attr_reader :produto, :quantidade

  def initialize(produto, quantidade)
    raise ArgumentError, "produto inválido" if produto.nil?
    raise ArgumentError, "quantidade inválida" if quantidade.nil? || quantidade <= 0

    @produto = produto
    @quantidade = quantidade
  end

  def incrementar(quantidade_extra)
    raise ArgumentError, "quantidade extra inválida" if quantidade_extra.nil? || quantidade_extra <= 0
    @quantidade += quantidade_extra
  end

  def subtotal_em_centavos
    @produto.preco_em_centavos * @quantidade
  end
end

Repare como o carrinho não precisa saber calcular subtotal de item: isso é responsabilidade do item.

3) Criando Carrinho com regras e métodos pequenos

Regras de exemplo:

  • Não aceitar quantidade <= 0.
  • Se o produto já existe no carrinho, somar quantidades (em vez de duplicar item).
  • Cupom percentual entre 0 e 20%.
class Carrinho
  CUPOM_MAXIMO_PERCENTUAL = 20

  def initialize
    @itens = []
    @cupom_percentual = 0
  end

  def adicionar_produto(produto, quantidade = 1)
    raise ArgumentError, "quantidade inválida" if quantidade.nil? || quantidade <= 0

    item = encontrar_item_por_produto(produto)

    if item
      item.incrementar(quantidade)
    else
      @itens << ItemDeCarrinho.new(produto, quantidade)
    end
  end

  def aplicar_cupom(percentual)
    validar_cupom(percentual)
    @cupom_percentual = percentual
  end

  def subtotal_em_centavos
    @itens.sum { |item| item.subtotal_em_centavos }
  end

  def desconto_em_centavos
    (subtotal_em_centavos * @cupom_percentual / 100.0).round
  end

  def total_em_centavos
    subtotal_em_centavos - desconto_em_centavos
  end

  def itens
    @itens.dup
  end

  private

  def encontrar_item_por_produto(produto)
    @itens.find { |item| item.produto == produto }
  end

  def validar_cupom(percentual)
    raise ArgumentError, "cupom inválido" if percentual.nil?
    raise ArgumentError, "cupom deve ser entre 0 e #{CUPOM_MAXIMO_PERCENTUAL}" if percentual < 0 || percentual > CUPOM_MAXIMO_PERCENTUAL
  end
end

Detalhes importantes de encapsulamento:

  • itens retorna @itens.dup para evitar que código externo faça carrinho.itens << ... e burle regras.
  • Métodos auxiliares (encontrar_item_por_produto, validar_cupom) são private porque não fazem parte da API pública do carrinho.

4) Usando as classes juntas (fluxo completo)

camiseta = Produto.new("Camiseta", 7990)
caneca   = Produto.new("Caneca", 2590)

camiseta.aplicar_desconto(10)

carrinho = Carrinho.new
carrinho.adicionar_produto(camiseta, 2)
carrinho.adicionar_produto(caneca)
carrinho.aplicar_cupom(15)

puts carrinho.subtotal_em_centavos
puts carrinho.desconto_em_centavos
puts carrinho.total_em_centavos

Encapsulamento com private e protected

private: esconder detalhes internos

private torna métodos acessíveis apenas dentro do próprio objeto. Isso ajuda a:

  • Evitar que outras partes do sistema chamem validações diretamente.
  • Reduzir a “superfície” pública da classe (API menor, mais fácil de manter).
  • Forçar o uso de métodos públicos que garantem regras.

No exemplo, validar_cupom e validar_percentual_de_desconto são privados.

protected: colaboração entre objetos do mesmo tipo

protected é útil quando objetos da mesma classe (ou subclasses) precisam acessar um detalhe interno uns dos outros, mas você não quer expor isso publicamente.

Exemplo: comparar carrinhos por um “código interno” sem expor esse código fora da classe.

class Carrinho
  def initialize
    @itens = []
    @codigo_interno = gerar_codigo
  end

  def mesmo_codigo?(outro)
    codigo_interno == outro.codigo_interno
  end

  protected

  def codigo_interno
    @codigo_interno
  end

  private

  def gerar_codigo
    rand(1000..9999)
  end
end

Aqui, outro.codigo_interno funciona dentro da classe, mas código externo não consegue chamar carrinho.codigo_interno.

Boas práticas de design: métodos pequenos e nomes descritivos

Ao modelar classes:

  • Prefira métodos que expressem intenção: aplicar_cupom, subtotal_em_centavos, incrementar.
  • Evite “métodos Deus” (muito longos). Extraia validações e cálculos para métodos privados.
  • Proteja invariantes: se um objeto não pode ter estado inválido, valide no initialize e em métodos de alteração.
  • Exponha o mínimo: use attr_reader por padrão e crie setters com regra quando necessário.

Checklist prático para criar uma classe bem encapsulada

ItemPerguntaExemplo
EstadoQuais dados o objeto precisa guardar?@preco_em_centavos, @itens
InicializaçãoComo garantir que nasce válido?validar no initialize
API públicaQuais ações o mundo externo pode fazer?adicionar_produto, aplicar_cupom
Regras internasO que deve ficar escondido?validar_cupom como private
ColaboraçãoObjetos do mesmo tipo precisam acessar algo interno?protected para comparação

Agora responda o exercício sobre o conteúdo:

Em Ruby, qual prática ajuda a manter o encapsulamento de um objeto ao expor seus dados e permitir alterações com regras?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Encapsulamento pede expor o mínimo necessário: leitura com attr_reader e mudanças via métodos que validam regras (ex.: preço não negativo). Isso evita que código externo deixe o objeto em estado inválido.

Próximo capitúlo

Ruby do Zero: Métodos de Classe, self e Colaboração entre Objetos

Arrow Right Icon
Capa do Ebook gratuito Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código
81%

Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.