Introducao
O Adapter (Adaptador) e um padrao estrutural que permite que interfaces incompativeis trabalhem juntas. Ele atua como um “tradutor” entre dois tipos que nao se comunicam diretamente.
Em Rust, o Adapter e especialmente relevante por causa da regra do orfao (orphan rule): voce nao pode implementar uma trait estrangeira em um tipo estrangeiro. O Newtype Pattern e a solucao idiomatica de Rust para esse problema, criando um wrapper fino que permite implementar qualquer trait em qualquer tipo.
Alem disso, as traits From e Into da biblioteca padrao sao o mecanismo principal
de adaptacao em Rust, permitindo conversoes seguras e ergonomicas entre tipos.
Problema
Voce esta integrando uma biblioteca externa de geocodificacao com seu sistema interno.
A biblioteca usa seu proprio tipo GeoPoint { latitude: f64, longitude: f64 }, mas
seu sistema usa Coordenada { lat: f64, lon: f64 }. Voce nao pode modificar nenhum
dos dois tipos, e precisa que eles trabalhem juntos.
// Biblioteca externa (voce nao controla esse codigo)
mod geo_lib {
pub struct GeoPoint {
pub latitude: f64,
pub longitude: f64,
}
pub fn calcular_distancia(a: &GeoPoint, b: &GeoPoint) -> f64 {
// Formula de Haversine simplificada
let dlat = (b.latitude - a.latitude).to_radians();
let dlon = (b.longitude - a.longitude).to_radians();
let a_calc = (dlat / 2.0).sin().powi(2)
+ a.latitude.to_radians().cos()
* b.latitude.to_radians().cos()
* (dlon / 2.0).sin().powi(2);
let c = 2.0 * a_calc.sqrt().asin();
6371.0 * c // raio da Terra em km
}
}
// Seu sistema interno (seu codigo)
mod meu_sistema {
pub struct Coordenada {
pub lat: f64,
pub lon: f64,
}
pub trait Localizavel {
fn coordenada(&self) -> Coordenada;
}
}
// PROBLEMA: GeoPoint e Coordenada sao incompativeis!
// Nao da pra passar Coordenada para calcular_distancia()
// Nao da pra implementar Localizavel para GeoPoint (regra do orfao)
Solucao em Rust
1. Newtype Pattern como Adapter
O Newtype cria um wrapper com custo zero em tempo de execucao:
mod geo_lib {
#[derive(Debug)]
pub struct GeoPoint {
pub latitude: f64,
pub longitude: f64,
}
pub fn calcular_distancia(a: &GeoPoint, b: &GeoPoint) -> f64 {
let dlat = (b.latitude - a.latitude).to_radians();
let dlon = (b.longitude - a.longitude).to_radians();
let a_calc = (dlat / 2.0).sin().powi(2)
+ a.latitude.to_radians().cos()
* b.latitude.to_radians().cos()
* (dlon / 2.0).sin().powi(2);
6371.0 * 2.0 * a_calc.sqrt().asin()
}
}
/// Coordenada do nosso sistema interno
#[derive(Debug, Clone, Copy)]
pub struct Coordenada {
pub lat: f64,
pub lon: f64,
}
/// Trait do nosso sistema
pub trait Localizavel {
fn coordenada(&self) -> Coordenada;
fn distancia_ate(&self, outro: &dyn Localizavel) -> f64 {
let a = self.coordenada();
let b = outro.coordenada();
let ga = geo_lib::GeoPoint {
latitude: a.lat,
longitude: a.lon,
};
let gb = geo_lib::GeoPoint {
latitude: b.lat,
longitude: b.lon,
};
geo_lib::calcular_distancia(&ga, &gb)
}
}
/// ADAPTER: Newtype que envolve GeoPoint
/// Custo zero - mesmo layout de memoria que GeoPoint
#[derive(Debug)]
pub struct GeoPointAdapter(pub geo_lib::GeoPoint);
impl Localizavel for GeoPointAdapter {
fn coordenada(&self) -> Coordenada {
Coordenada {
lat: self.0.latitude,
lon: self.0.longitude,
}
}
}
/// Conversao direta: Coordenada -> GeoPoint
impl From<Coordenada> for geo_lib::GeoPoint {
fn from(coord: Coordenada) -> Self {
geo_lib::GeoPoint {
latitude: coord.lat,
longitude: coord.lon,
}
}
}
/// Conversao direta: GeoPoint -> Coordenada
impl From<geo_lib::GeoPoint> for Coordenada {
fn from(point: geo_lib::GeoPoint) -> Self {
Coordenada {
lat: point.latitude,
lon: point.longitude,
}
}
}
/// Exemplo de tipo do nosso sistema
#[derive(Debug)]
pub struct Restaurante {
pub nome: String,
pub coordenada: Coordenada,
}
impl Localizavel for Restaurante {
fn coordenada(&self) -> Coordenada {
self.coordenada
}
}
fn main() {
let restaurante = Restaurante {
nome: "Fogo de Chao".to_string(),
coordenada: Coordenada {
lat: -23.5505,
lon: -46.6333,
},
};
// Ponto externo adaptado para nosso sistema
let ponto_externo = GeoPointAdapter(geo_lib::GeoPoint {
latitude: -22.9068,
longitude: -43.1729,
});
let dist = restaurante.distancia_ate(&ponto_externo);
println!(
"Distancia de {} ate o ponto: {:.1} km",
restaurante.nome, dist
);
// Conversao via From/Into
let coord = Coordenada { lat: -15.7801, lon: -47.9292 };
let geo_point: geo_lib::GeoPoint = coord.into();
println!(
"GeoPoint convertido: ({}, {})",
geo_point.latitude, geo_point.longitude
);
}
2. Adapter com From/Into para Conversoes de Tipos
use std::collections::HashMap;
/// Formato de erro da biblioteca externa
#[derive(Debug)]
pub struct ErroExterno {
pub code: i32,
pub message: String,
pub details: Option<String>,
}
/// Nosso formato de erro interno
#[derive(Debug)]
pub struct ErroApp {
pub tipo: TipoErro,
pub mensagem: String,
pub contexto: HashMap<String, String>,
}
#[derive(Debug)]
pub enum TipoErro {
Validacao,
NaoEncontrado,
Autorizacao,
Interno,
}
/// Adapter: converte erro externo para nosso formato
impl From<ErroExterno> for ErroApp {
fn from(externo: ErroExterno) -> Self {
let tipo = match externo.code {
400..=499 if externo.code == 404 => TipoErro::NaoEncontrado,
400..=499 if externo.code == 401 || externo.code == 403 => TipoErro::Autorizacao,
400..=499 => TipoErro::Validacao,
_ => TipoErro::Interno,
};
let mut contexto = HashMap::new();
contexto.insert("codigo_original".to_string(), externo.code.to_string());
if let Some(detalhes) = externo.details {
contexto.insert("detalhes".to_string(), detalhes);
}
ErroApp {
tipo,
mensagem: externo.message,
contexto,
}
}
}
fn operacao_externa() -> Result<String, ErroExterno> {
Err(ErroExterno {
code: 404,
message: "Recurso nao encontrado".to_string(),
details: Some("ID: 12345".to_string()),
})
}
fn main() {
// O operador ? automaticamente converte ErroExterno -> ErroApp via From
let resultado: Result<String, ErroApp> =
operacao_externa().map_err(|e| e.into());
match resultado {
Ok(dados) => println!("Sucesso: {}", dados),
Err(erro) => {
println!("Erro ({:?}): {}", erro.tipo, erro.mensagem);
println!("Contexto: {:?}", erro.contexto);
}
}
}
Diagrama
NEWTYPE ADAPTER:
+------------------+ +-------------------+
| Seu Codigo | | Biblioteca |
| | | Externa |
| trait Localizavel| | |
| coordenada() | | GeoPoint |
| distancia() | | latitude: f64 |
+--------+---------+ | longitude: f64 |
| +---------+---------+
| |
| +---------------------+ |
+--->| GeoPointAdapter |<-+
| (Newtype wrapper) |
| |
| impl Localizavel |
| converte campos |
+---------------------+
Custo: ZERO em tempo de execucao!
GeoPointAdapter tem o mesmo layout de memoria que GeoPoint.
FROM/INTO ADAPTER:
ErroExterno ---> From<ErroExterno> for ErroApp ---> ErroApp
| | |
code: 404 match code { tipo: NaoEncontrado
message: "..." 400..499 => ... mensagem: "..."
details: ... } contexto: {...}
Exemplo do Mundo Real
Adaptando diferentes formatos de resposta de APIs externas para um formato interno unificado:
use std::collections::HashMap;
/// Nosso formato interno unificado para um usuario
#[derive(Debug)]
pub struct Usuario {
pub id: String,
pub nome: String,
pub email: String,
pub avatar_url: Option<String>,
pub provedor: String,
}
// === API do GitHub ===
pub mod github {
#[derive(Debug)]
pub struct GitHubUser {
pub id: u64,
pub login: String,
pub email: Option<String>,
pub avatar_url: String,
pub name: Option<String>,
}
}
// === API do Google ===
pub mod google {
#[derive(Debug)]
pub struct GoogleProfile {
pub sub: String,
pub given_name: String,
pub family_name: String,
pub email: String,
pub picture: String,
}
}
// === API propria (legado) ===
pub mod legado {
use std::collections::HashMap;
#[derive(Debug)]
pub struct UsuarioLegado {
pub campos: HashMap<String, String>,
}
}
/// Adapter: GitHub -> Usuario
impl From<github::GitHubUser> for Usuario {
fn from(gh: github::GitHubUser) -> Self {
Usuario {
id: format!("github:{}", gh.id),
nome: gh.name.unwrap_or_else(|| gh.login.clone()),
email: gh.email.unwrap_or_else(|| format!("{}@users.noreply.github.com", gh.login)),
avatar_url: Some(gh.avatar_url),
provedor: "github".to_string(),
}
}
}
/// Adapter: Google -> Usuario
impl From<google::GoogleProfile> for Usuario {
fn from(gp: google::GoogleProfile) -> Self {
Usuario {
id: format!("google:{}", gp.sub),
nome: format!("{} {}", gp.given_name, gp.family_name),
email: gp.email,
avatar_url: Some(gp.picture),
provedor: "google".to_string(),
}
}
}
/// Adapter: Legado -> Usuario (com TryFrom para tratar erros)
impl TryFrom<legado::UsuarioLegado> for Usuario {
type Error = String;
fn try_from(u: legado::UsuarioLegado) -> Result<Self, Self::Error> {
let id = u.campos.get("id")
.ok_or("Campo 'id' ausente no sistema legado")?;
let nome = u.campos.get("nome_completo")
.or_else(|| u.campos.get("nome"))
.ok_or("Campo 'nome' ausente no sistema legado")?;
let email = u.campos.get("email")
.ok_or("Campo 'email' ausente no sistema legado")?;
Ok(Usuario {
id: format!("legado:{}", id),
nome: nome.clone(),
email: email.clone(),
avatar_url: u.campos.get("foto").cloned(),
provedor: "legado".to_string(),
})
}
}
/// Servico que trabalha com nosso tipo unificado
fn exibir_perfil(usuario: &Usuario) {
println!("--- Perfil ---");
println!(" ID: {}", usuario.id);
println!(" Nome: {}", usuario.nome);
println!(" Email: {}", usuario.email);
println!(" Avatar: {:?}", usuario.avatar_url);
println!(" Provedor: {}", usuario.provedor);
}
fn main() {
// Usuario do GitHub
let gh_user = github::GitHubUser {
id: 12345,
login: "maria-dev".to_string(),
email: Some("maria@exemplo.com".to_string()),
avatar_url: "https://avatars.githubusercontent.com/u/12345".to_string(),
name: Some("Maria Silva".to_string()),
};
let usuario_gh: Usuario = gh_user.into(); // From automatico
exibir_perfil(&usuario_gh);
// Usuario do Google
let google_profile = google::GoogleProfile {
sub: "1234567890".to_string(),
given_name: "Joao".to_string(),
family_name: "Santos".to_string(),
email: "joao@gmail.com".to_string(),
picture: "https://lh3.googleusercontent.com/foto.jpg".to_string(),
};
let usuario_google: Usuario = google_profile.into();
exibir_perfil(&usuario_google);
// Usuario do sistema legado (pode falhar)
let legado_user = legado::UsuarioLegado {
campos: HashMap::from([
("id".to_string(), "99".to_string()),
("nome_completo".to_string(), "Ana Costa".to_string()),
("email".to_string(), "ana@empresa.com.br".to_string()),
]),
};
match Usuario::try_from(legado_user) {
Ok(usuario) => exibir_perfil(&usuario),
Err(e) => println!("Erro na conversao: {}", e),
}
}
Quando Usar
- Integrar bibliotecas externas cujos tipos nao sao compativeis com seu sistema
- Regra do orfao impede implementar traits diretamente - use Newtype
- Migracoes graduais - adaptar APIs legadas para novas interfaces
- Unificar formatos de multiplas fontes externas (APIs, bancos, arquivos)
- Conversoes de tipo com
From/Into/TryFrom/TryInto
Quando NAO Usar
- Tipos que voce controla - modifique diretamente em vez de criar wrappers
- Muitos metodos para delegar - o boilerplate de Newtype pode ser excessivo
- Conversoes simples que uma funcao livre resolve melhor
- Performance critica onde a indirecao (mesmo que zero-cost) complica a otimizacao
Variacoes em Rust
1. Newtype com Deref (delegacao automatica)
use std::ops::Deref;
/// Newtype que delega metodos automaticamente via Deref
pub struct Celsius(f64);
impl Deref for Celsius {
type Target = f64;
fn deref(&self) -> &f64 {
&self.0
}
}
// Agora Celsius tem todos os metodos de f64:
// celsius.abs(), celsius.round(), etc.
2. Adapter com closure
/// Adapter funcional: transforma qualquer funcao para uma interface
fn adaptar_comparador<T>(
comparar: impl Fn(&T, &T) -> bool,
) -> impl Fn(&T, &T) -> std::cmp::Ordering {
move |a, b| {
if comparar(a, b) {
std::cmp::Ordering::Less
} else if comparar(b, a) {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}
}
3. Adapter via trait com implementacao padrao
/// Trait com metodo adaptador padrao
pub trait Serializavel {
fn para_json(&self) -> String;
/// Adaptador embutido: converte para outro formato
fn para_csv(&self) -> String {
// Implementacao padrao que adapta JSON para CSV
let json = self.para_json();
format!("adaptado_de_json:{}", json)
}
}
Padroes Relacionados
- Decorator - Decorator adiciona funcionalidade; Adapter muda a interface
- Facade - Facade simplifica uma interface complexa; Adapter torna compativel
- Strategy - Strategies podem ser adaptadas via Newtype para diferentes interfaces
- Factory - Factory pode retornar adapters para tipos externos
Conclusao
O Adapter em Rust se beneficia enormemente do Newtype Pattern, que e uma idioma central
da linguagem. O custo zero em tempo de execucao do Newtype torna o Adapter praticamente
“gratis” em termos de performance. As traits From, Into, TryFrom e TryInto da
biblioteca padrao fornecem um mecanismo padronizado e ergonomico para adaptacao de tipos.
A regra do orfao, que a principio parece restritiva, na verdade guia o desenvolvedor
a criar abstraccoes bem definidas e desacopladas.