Skip to content

Classes e Interfaces

Soyuz suporta OOP com uma restrição deliberada: sem herança entre classes. Comportamento compartilhado é expresso exclusivamente via interfaces e composição.


Interfaces definem contratos. São um conjunto de assinaturas de método que uma classe deve implementar.

interface Persistivel {
fn salvar(self) -> Result[Unit]
fn deletar(self, id: Int) -> Result[Unit]
}
interface Serializavel {
fn toJson(self) -> String
fn fromJson(json: String) -> Result[Self]
}
interface Auditavel {
fn log(self) -> String
}

Interfaces não têm implementação — apenas assinaturas. Isso as mantém como contratos puros.

interface Repositorio[T] {
fn buscar(self, id: Int) -> Option[T]
fn salvarTodos(self, items: List[T]) -> Result[Unit]
}
interface Comparavel[T] {
fn comparar(self, outro: T) -> Int // -1, 0, ou 1
}

Classes combinam dados e comportamento. Podem implementar múltiplas interfaces, mas não herdam de outras classes.

class Usuario : Persistivel, Serializavel {
val id: Int
val nome: String
var idade: Int
var email: String
fn salvar(self) -> Result[Unit] {
// implementação
return Ok(())
}
fn deletar(self, id: Int) -> Result[Unit] {
return Ok(())
}
fn toJson(self) -> String {
return "{\"id\": $(self.id), \"nome\": \"$(self.nome)\"}"
}
fn fromJson(json: String) -> Result[Usuario] {
// implementação
return Err(ParseErro { input: json })
}
}

Classes são instanciadas com a sintaxe de record literal:

val u = Usuario {
id: 1,
nome: "Vand",
idade: 28,
email: "vand@soyuz.dev"
}

Classes podem ter campos val (imutáveis) e var (mutáveis):

class Produto {
val id: Int // imutável após criação
val nome: String // imutável após criação
var preco: Float // pode ser alterado
var estoque: Int // pode ser alterado
}

Quando um método modifica um campo var, o self é mutável implicitamente. Não há mut self:

class ContaBancaria {
val titular: String
var saldo: Float
fn depositar(self, valor: Float) -> Result[Float] {
if valor <= 0.0 {
return Err(ValorInvalidoErro { valor: valor })
}
self.saldo = self.saldo + valor // OK — saldo é var
return Ok(self.saldo)
}
fn titular(self) -> String {
return self.titular // OK — acesso somente leitura
}
}

Todos os métodos recebem self explicitamente como primeiro parâmetro.

Por padrão, métodos são privados ao módulo — só podem ser chamados por código no mesmo arquivo. Use pub para torná-los acessíveis de fora da classe:

class Retangulo {
val largura: Float
val altura: Float
pub fn area(self) -> Float { // acessível de fora
return self.largura * self.altura
}
pub fn perimetro(self) -> Float { // acessível de fora
return 2.0 * (self.largura + self.altura)
}
pub fn escalar(self, fator: Float) -> Retangulo { // acessível de fora
return Retangulo {
largura: self.largura * fator,
altura: self.altura * fator
}
}
fn calcularDiagonal(self) -> Float { // privado — helper interno
return self.largura * self.largura + self.altura * self.altura
}
}

Assim como funções globais, métodos em classes podem ser sobrecarregados:

class Usuario {
fn promover(self) { ... }
fn promover(self, nivel: Int) { ... }
}

Métodos também suportam pattern matching em parâmetros:

class Parser {
fn parsear(self, Some(token)) -> Result[Node] = self.parseToken(token)
fn parsear(self, None) -> Result[Node] = Err(TokenEsperadoErro {})
}

Soyuz deliberadamente não tem herança. Essa decisão tem razões concretas:

Herança cria acoplamento frágil. Mudanças na classe pai quebram silenciosamente as filhas. Com interfaces, o contrato é explícito e estável.

Hierarquias profundas são difíceis de raciocinar. Quando você lê objeto.metodo(), precisa navegar pela árvore de herança para entender o que executa. Com interfaces, você sabe exatamente qual classe implementa o quê.

Composição é mais flexível. Se você precisa reutilizar comportamento, componha:

// Ao invés de herdar de Logger
class ServicoDeUsuario : Persistivel {
val logger: Logger // composição
val repositorio: UsuarioRepositorio // composição
fn criar(self, dados: NovosUsuarioDados) -> Result[Usuario] {
self.logger.info("Criando usuário: $(dados.nome)")
return self.repositorio.salvar(dados)
}
}

class Repositorio[T : Entidade] : Store[T] {
var items: List[T] = []
fn adicionar(self, item: T) -> Result[Unit] {
self.items.add(item)
return Ok(())
}
fn buscar(self, id: Int) -> Option[T] {
return self.items.find(fn(i: T) => i.id == id)
}
fn todos(self) -> List[T] {
return self.items
}
}
// Uso
val repos = Repositorio[Usuario] { items: [] }

Uma classe pode implementar quantas interfaces forem necessárias:

class EventoAuditavel : Persistivel, Serializavel, Auditavel, Comparavel[EventoAuditavel] {
val id: Int
val tipo: String
val timestamp: Int
var processado: Bool
fn salvar(self) -> Result[Unit] { return Ok(()) }
fn deletar(self, id: Int) -> Result[Unit] { return Ok(()) }
fn toJson(self) -> String { return "{}" }
fn fromJson(json: String) -> Result[EventoAuditavel] { return Err(ParseErro { input: json }) }
fn log(self) -> String { return "Evento $(self.id): $(self.tipo)" }
fn comparar(self, outro: EventoAuditavel) -> Int {
if self.timestamp < outro.timestamp { return -1 }
if self.timestamp > outro.timestamp { return 1 }
return 0
}
}

recordclass
Campos mutáveisNuncavar campos
MétodosNãoSim
Implementa interfacesNãoSim
PassagemPor referência readonlyPor referência
IgualdadeDeep equality automáticaDefinida pela classe
Uso típicoDTOs, dados purosServiços, entidades com comportamento
// Record — dados puros, sem comportamento
record EnderecoDTO {
rua: String
cidade: String
cep: String
}
// Class — entidade com comportamento e estado
class Pedido : Persistivel {
val id: Int
var status: StatusPedido
var itens: List[Item]
fn confirmar(self) -> Result[Unit] {
if self.itens.isEmpty() {
return Err(PedidoVazioErro {})
}
self.status = StatusPedido.Confirmado
return Ok(())
}
fn salvar(self) -> Result[Unit] { return Ok(()) }
}