Explore padrões avançados de Proxy em JavaScript para interceptar e validar objetos. Aprenda a melhorar a qualidade, segurança e manutenibilidade do seu código.
Padrões de Proxy em JavaScript: Interceptação e Validação Avançada de Objetos
O objeto Proxy do JavaScript é um recurso poderoso que permite interceptar e personalizar operações fundamentais de objetos. Ele possibilita técnicas avançadas de metaprogramação, oferecendo maior controle sobre o comportamento dos objetos e abrindo possibilidades para padrões de projeto sofisticados. Este artigo explora vários padrões de Proxy, mostrando seus casos de uso em validação, interceptação e modificação de comportamento dinâmico. Vamos mergulhar em exemplos práticos para demonstrar como os Proxies podem melhorar a qualidade do código, a segurança e a manutenibilidade em seus projetos JavaScript.
Entendendo o Proxy do JavaScript
Em sua essência, um objeto Proxy envolve outro objeto (o alvo ou target) e intercepta as operações realizadas nesse alvo. Essas interceptações são tratadas por traps (armadilhas), que são métodos que definem comportamento personalizado para operações específicas, como obter uma propriedade, definir uma propriedade ou chamar uma função. A API de Proxy fornece um mecanismo flexível e extensível para modificar o comportamento padrão dos objetos.
Conceitos Chave
- Target (Alvo): O objeto original que o Proxy envolve.
- Handler (Manipulador): Um objeto que contém os métodos de trap. Cada trap corresponde a uma operação específica.
- Traps (Armadilhas): Métodos dentro do handler que interceptam e personalizam operações de objetos. Traps comuns incluem
get,set,applyeconstruct.
Criando um Proxy
Para criar um Proxy, você usa o construtor Proxy, passando o objeto alvo e o objeto handler como argumentos:
const target = {};
const handler = {
get: function(target, property, receiver) {
console.log(`Getting property: ${property}`);
return Reflect.get(target, property, receiver);
}
};
const proxy = new Proxy(target, handler);
proxy.name = "John"; // Registra no console: Getting property: name
console.log(proxy.name); // Registra no console: Getting property: name, e depois John
Traps Comuns do Proxy
Os Proxies oferecem uma variedade de traps para interceptar diversas operações. Aqui estão alguns dos traps mais comumente usados:
get(target, property, receiver): Intercepta o acesso a propriedades.set(target, property, value, receiver): Intercepta a atribuição de propriedades.has(target, property): Intercepta o operadorin.deleteProperty(target, property): Intercepta o operadordelete.apply(target, thisArg, argumentsList): Intercepta chamadas de função.construct(target, argumentsList, newTarget): Intercepta o operadornew.getPrototypeOf(target): Intercepta o métodoObject.getPrototypeOf().setPrototypeOf(target, prototype): Intercepta o métodoObject.setPrototypeOf().isExtensible(target): Intercepta o métodoObject.isExtensible().preventExtensions(target): Intercepta o métodoObject.preventExtensions().getOwnPropertyDescriptor(target, property): Intercepta o métodoObject.getOwnPropertyDescriptor().defineProperty(target, property, descriptor): Intercepta o métodoObject.defineProperty().ownKeys(target): Intercepta os métodosObject.getOwnPropertyNames()eObject.getOwnPropertySymbols().
Padrões de Proxy
Agora, vamos explorar alguns padrões práticos de Proxy e suas aplicações:
1. Proxy de Validação
Um Proxy de Validação impõe restrições às atribuições de propriedades. Ele intercepta o trap set para validar o novo valor antes de permitir que a atribuição prossiga.
Exemplo: Validando a entrada do usuário em um formulário.
const user = {};
const validator = {
set: function(target, property, value) {
if (property === 'age') {
if (!Number.isInteger(value) || value < 0 || value > 120) {
throw new Error('Idade inválida. A idade deve ser um número inteiro entre 0 e 120.');
}
}
target[property] = value;
return true; // Indica sucesso
}
};
const proxy = new Proxy(user, validator);
proxy.name = 'Alice';
proxy.age = 30;
console.log(user);
try {
proxy.age = 'invalid'; // Lança um erro
} catch (error) {
console.error(error.message);
}
Neste exemplo, o trap set verifica se a propriedade age é um inteiro entre 0 e 120. Se a validação falhar, um erro é lançado, impedindo que o valor inválido seja atribuído.
Exemplo Global: Este padrão de validação é essencial para garantir a integridade dos dados em aplicações globais, onde a entrada do usuário pode vir de diversas fontes e culturas. Por exemplo, a validação de códigos postais pode variar significativamente entre países. Um proxy de validação pode ser adaptado para suportar diferentes regras de validação com base na localização do usuário.
const address = {};
const addressValidator = {
set: function(target, property, value) {
if (property === 'postalCode') {
// Exemplo: Assumindo uma validação simples de código postal dos EUA
if (!/^[0-9]{5}(?:-[0-9]{4})?$/.test(value)) {
throw new Error('Código postal dos EUA inválido.');
}
}
target[property] = value;
return true;
}
};
const addressProxy = new Proxy(address, addressValidator);
addressProxy.postalCode = "12345-6789"; // Válido
try {
addressProxy.postalCode = "abcde"; // Inválido
} catch(e) {
console.log(e);
}
// Para uma aplicação mais internacional, você usaria uma biblioteca de validação mais sofisticada
// que poderia validar códigos postais com base no país do usuário.
2. Proxy de Registro (Logging)
Um Proxy de Registro intercepta o acesso e a atribuição de propriedades para registrar essas operações. É útil para depuração e auditoria.
Exemplo: Registrando acesso e modificação de propriedades.
const data = {
value: 10
};
const logger = {
get: function(target, property) {
console.log(`Getting property: ${property}`);
return target[property];
},
set: function(target, property, value) {
console.log(`Setting property: ${property} to ${value}`);
target[property] = value;
return true;
}
};
const proxy = new Proxy(data, logger);
console.log(proxy.value); // Registra: Getting property: value, e depois 10
proxy.value = 20; // Registra: Setting property: value to 20
Os traps get e set registram a propriedade que está sendo acessada ou modificada, fornecendo um rastro das interações com o objeto.
Exemplo Global: Em uma corporação multinacional, proxies de registro podem ser usados para auditar o acesso e as modificações de dados realizadas por funcionários em diferentes locais. Isso é crucial para fins de conformidade e segurança. Fusos horários podem precisar ser considerados nas informações de registro.
const employeeData = {
name: "John Doe",
salary: 50000
};
const auditLogger = {
get: function(target, property) {
const timestamp = new Date().toISOString();
console.log(`${timestamp} - [GET] Accessing property: ${property}`);
return target[property];
},
set: function(target, property, value) {
const timestamp = new Date().toISOString();
console.log(`${timestamp} - [SET] Setting property: ${property} to ${value}`);
target[property] = value;
return true;
}
};
const proxiedEmployee = new Proxy(employeeData, auditLogger);
proxiedEmployee.name; // Registra o timestamp e o acesso a 'name'
proxiedEmployee.salary = 60000; // Registra o timestamp e a modificação de 'salary'
3. Proxy Somente Leitura
Um Proxy Somente Leitura impede a atribuição de propriedades. Ele intercepta o trap set e lança um erro se for feita uma tentativa de modificar uma propriedade.
Exemplo: Tornando um objeto imutável.
const config = {
apiUrl: 'https://api.example.com'
};
const readOnly = {
set: function(target, property, value) {
throw new Error(`Não é possível definir a propriedade: ${property}. O objeto é somente leitura.`);
}
};
const proxy = new Proxy(config, readOnly);
console.log(proxy.apiUrl);
try {
proxy.apiUrl = 'https://newapi.example.com'; // Lança um erro
} catch (error) {
console.error(error.message);
}
Qualquer tentativa de definir uma propriedade no proxy resultará em um erro, garantindo que o objeto permaneça imutável.
Exemplo Global: Este padrão é útil para proteger arquivos de configuração que não devem ser modificados em tempo de execução, especialmente em aplicações distribuídas globalmente. A modificação acidental da configuração em uma região pode afetar todo o sistema.
const globalSettings = {
defaultLanguage: "en",
currency: "USD",
timeZone: "UTC"
};
const immutableHandler = {
set: function(target, property, value) {
throw new Error(`Não é possível modificar a propriedade somente leitura: ${property}`);
}
};
const immutableSettings = new Proxy(globalSettings, immutableHandler);
console.log(immutableSettings.defaultLanguage); // exibe 'en'
// Tentar alterar um valor lançará um erro
// immutableSettings.defaultLanguage = "fr"; // lança Erro: Não é possível modificar a propriedade somente leitura: defaultLanguage
4. Proxy Virtual
Um Proxy Virtual controla o acesso a um recurso que pode ser caro para criar ou recuperar. Ele pode atrasar a criação do recurso até que seja realmente necessário.
Exemplo: Carregamento tardio (lazy loading) de uma imagem.
const image = {
display: function() {
console.log('Exibindo imagem');
}
};
const virtualProxy = {
get: function(target, property) {
if (property === 'display') {
console.log('Criando imagem...');
const realImage = {
display: function() {
console.log('Exibindo imagem real');
}
};
target.display = realImage.display;
return realImage.display;
}
return target[property];
}
};
const proxy = new Proxy(image, virtualProxy);
// A imagem não é criada até que 'display' seja chamado.
proxy.display(); // Registra: Criando imagem..., e depois Exibindo imagem real
O objeto da imagem real só é criado quando o método display é chamado, evitando o consumo desnecessário de recursos.
Exemplo Global: Considere um site de e-commerce global que serve imagens de produtos. Usando um Proxy Virtual, as imagens podem ser carregadas apenas quando estiverem visíveis para o usuário, otimizando o uso da largura de banda e melhorando os tempos de carregamento da página, especialmente para usuários com conexões de internet lentas em diferentes regiões.
const product = {
loadImage: function() {
console.log("Carregando imagem de alta resolução...");
// Simula o carregamento de uma imagem grande
setTimeout(() => {
console.log("Imagem carregada");
this.displayImage();
}, 2000);
},
displayImage: function() {
console.log("Exibindo a imagem");
}
};
const lazyLoadProxy = {
get: function(target, property) {
if (property === "displayImage") {
// Em vez de carregar imediatamente, atrasa o carregamento
console.log("Solicitação para exibir imagem recebida. Carregando...");
target.loadImage();
return function() { /* Intencionalmente Vazio */ }; // Retorna uma função vazia para evitar a execução imediata
}
return target[property];
}
};
const proxiedProduct = new Proxy(product, lazyLoadProxy);
// Chamar displayImage aciona o processo de carregamento tardio
proxiedProduct.displayImage();
5. Proxy Revogável
Um Proxy Revogável permite que você revogue o proxy a qualquer momento, tornando-o inutilizável. Isso é útil para cenários sensíveis à segurança onde você precisa controlar o acesso a um objeto.
Exemplo: Concedendo acesso temporário a um recurso.
const target = {
secret: 'Isto é um segredo'
};
const handler = {
get: function(target, property) {
console.log('Acessando propriedade secreta');
return target[property];
}
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.secret); // Registra: Acessando propriedade secreta, e depois Isto é um segredo
revoke();
try {
console.log(proxy.secret); // Lança um TypeError
} catch (error) {
console.error(error.message); // Registra: Cannot perform 'get' on a proxy that has been revoked
}
O método Proxy.revocable() cria um proxy revogável. Chamar a função revoke() torna o proxy inutilizável, impedindo o acesso futuro ao objeto alvo.
Exemplo Global: Em um sistema distribuído globalmente, você pode usar um proxy revogável para conceder acesso temporário a dados sensíveis a um serviço em execução em uma região específica. Após um certo tempo, o proxy pode ser revogado para impedir o acesso não autorizado.
const sensitiveData = {
apiKey: "SUPER_SECRET_KEY"
};
const handler = {
get: function(target, property) {
console.log("Acessando dados sensíveis");
return target[property];
}
};
const { proxy: dataProxy, revoke: revokeAccess } = Proxy.revocable(sensitiveData, handler);
// Permite o acesso por 5 segundos
setTimeout(() => {
revokeAccess();
console.log("Acesso revogado");
}, 5000);
// Tenta acessar os dados
console.log(dataProxy.apiKey); // Registra a chave da API
// Após 5 segundos, isso lançará um erro
setTimeout(() => {
try {
console.log(dataProxy.apiKey); // Lança: TypeError: Cannot perform 'get' on a proxy that has been revoked
} catch (error) {
console.error(error);
}
}, 6000);
6. Proxy de Conversão de Tipo
Um Proxy de Conversão de Tipo intercepta o acesso a propriedades para converter automaticamente o valor retornado para um tipo específico. Isso pode ser útil para trabalhar com dados de diferentes fontes que podem ter tipos inconsistentes.
Exemplo: Convertendo valores de string para números.
const data = {
price: '10.99',
quantity: '5'
};
const typeConverter = {
get: function(target, property) {
const value = target[property];
if (typeof value === 'string' && !isNaN(Number(value))) {
return Number(value);
}
return value;
}
};
const proxy = new Proxy(data, typeConverter);
console.log(proxy.price + 1); // Registra: 11.99 (número)
console.log(proxy.quantity * 2); // Registra: 10 (número)
O trap get verifica se o valor da propriedade é uma string que pode ser convertida em um número. Se for o caso, ele converte o valor para um número antes de retorná-lo.
Exemplo Global: Ao lidar com dados provenientes de APIs com diferentes convenções de formatação (por exemplo, diferentes formatos de data ou símbolos de moeda), um Proxy de Conversão de Tipo pode garantir a consistência dos dados em toda a sua aplicação, independentemente da origem. Por exemplo, tratando diferentes formatos de data e convertendo todos para o formato ISO 8601.
const apiData = {
dateUS: "12/31/2023",
dateEU: "31/12/2023"
};
const dateFormatConverter = {
get: function(target, property) {
let value = target[property];
if (property.startsWith("date")) {
// Tenta converter formatos de data dos EUA e da UE para ISO 8601
if (property === "dateUS") {
const [month, day, year] = value.split("/");
value = `${year}-${month}-${day}`;
} else if (property === "dateEU") {
const [day, month, year] = value.split("/");
value = `${year}-${month}-${day}`;
}
return value;
}
return value;
}
};
const proxiedApiData = new Proxy(apiData, dateFormatConverter);
console.log(proxiedApiData.dateUS); // Exibe: 2023-12-31
console.log(proxiedApiData.dateEU); // Exibe: 2023-12-31
Melhores Práticas para Usar Proxies
- Use Proxies com Moderação: Proxies podem adicionar complexidade ao seu código. Use-os apenas quando fornecerem benefícios significativos, como validação aprimorada, registro ou controle sobre o comportamento do objeto.
- Considere o Desempenho: Os traps de Proxy podem introduzir sobrecarga. Faça o perfil do seu código para garantir que os Proxies não afetem negativamente o desempenho, especialmente em seções críticas de desempenho.
- Lide com Erros de Forma Elegante: Garanta que seus métodos de trap lidem com erros adequadamente, fornecendo mensagens de erro informativas quando necessário.
- Use a API Reflect: A API
Reflectfornece métodos que espelham o comportamento padrão das operações de objeto. Use métodosReflectdentro de seus métodos de trap para delegar ao comportamento original quando apropriado. Isso garante que seus traps não quebrem a funcionalidade existente. - Documente Seus Proxies: Documente claramente o propósito e o comportamento de seus Proxies, incluindo os traps que são usados e as restrições que são impostas. Isso ajudará outros desenvolvedores a entender e manter seu código.
Conclusão
Os Proxies do JavaScript são uma ferramenta poderosa para manipulação e interceptação avançada de objetos. Ao entender e aplicar vários padrões de Proxy, você pode melhorar a qualidade do código, a segurança e a manutenibilidade. Desde a validação da entrada do usuário até o controle do acesso a recursos sensíveis, os Proxies oferecem um mecanismo flexível e extensível para personalizar o comportamento dos objetos. Ao explorar as possibilidades dos Proxies, lembre-se de usá-los com moderação e documentar seu código detalhadamente.
Os exemplos fornecidos demonstram como usar Proxies de JavaScript para resolver problemas do mundo real em um contexto global. Ao compreender e aplicar esses padrões, você pode criar aplicações mais robustas, seguras e fáceis de manter que atendam às necessidades de uma base de usuários diversificada. Lembre-se de sempre considerar as implicações globais do seu código и adaptar suas soluções aos requisitos específicos de diferentes regiões e culturas.