Compondo seu comportamento: herança, Chain of Responsibility e Interceptors
6 min6 minutos de leitura
6 min6 minutos de leitura
São diversos os momentos em que temos a tentação de usar herança para implementar funcionalidades de maneira rápida. Um exemplo simples é o polêmico caso de Properties e Hashtable em Java.
Alguns padrões também costumam ser implementados através de herança são cadeias de responsabilidade, decorators, template method, filtros/interceptadores, entre outros. O exemplo a seguir mostra a adição de comportamento através de herança próxima ao que acontece com uma Servlet, ou ainda um InputStream que delega para outro:
Autor(a)
gas
Inscreva-se em nossa Newsletter
Fique por dentro de conteúdos, insights e oportunidades do universo tech. Receba novidades e lançamentos direto no seu e-mail.
Encadear comportamentos através de herança de classes apresenta diversos problemas de acoplamento e dificulta a customização de um processo.
Se eu desejasse um servidor com log mas sem as características de Cache, deveria criar uma quarta classe, LoggingWithoutCacheServer que herda de Servere reescrever o código de log. Costuma-se então sugerir a utilização de herança para evitar copiar-e-colar de código, mas podemos ver que somente com ela isso não é verdade.
Em Ruby temos uma abordagem semelhante de reutilização de comportamento através da herança de classe ou através da inclusão de módulos:
moduleServiceProviderdefservice(req) Response.new "<html>get para #{req.path}</html>"endendmoduleCacheableProviderdefservice(req) res = super(req) res.headers```"Cache-control"
= "public, max-age=7200" res end end
module LoggingProvider def service(req) Logger.debug "Requesting #{req.path}" super(req) end end
E agora podemos descrever nosso serviço através da inclusão dos módulos, na ordem que for necessária:
Se desejamos uma outra ordem de execução qualquer, basta mudar a ordem ou adicionar novos módulos que delegam para super quando desejado. Aqui ainda temos problemas de herança apresentados no post de 2006: o acoplamento da classe Server com os módulos é muito forte e, pior ainda, criamos dependências implicitas entre os módulos que antes não se conheciam: se um dos providers de log possui um método chamado info e o outro provider possui outro método info, sua aplicação não funcionará como o esperado.
A inclusão de módulos para compor a herança, seja em tempo de codificação ou execução, mantem o acoplamento alto. Note que ambas as abordagens permitem a adição de novo comportamento ou encadeamento dos mesmos visando minimizar a atualização na classe filha, mas acabando por necessitar que o autor de cada módulo conheça os detalhes dos outros, aumentando o acoplamento.
moduleRackclassCustomLogdefinitialize app @app = app enddefcall env status, headers, body = @app.call env if env```'HTTP\_METHOD'
=='HEAD' body = nil end ```status, headers, body
endendend
Em linguages funcionais como Javascript ou com suporte a ponteiros de função, como C, podemos criar o mesmo tipo de composição de comportamento:
server = function (req) { //return nova resposta }
function support\_log(base) { return function(req) { var res = base(req); console.log("logando o resultado"); return req; } }
function support\_head(base) { return function(req) { var res = base(req); if(req.method == "HEAD"){ res.body = ""; } return res; } }
function parse(req) { return support\_head(support\_log(server))(req); }
Ao mesmo tempo, encontramos uma dificuldade na hora de implementar a composição: como compartilhar objetos para serem acessados entre diferentes comportamentos que foram adicionados ao nosso objeto original?
A solução mais encontrada em todas as linguagens mencionadas é a criação de um escopo novo contendo todos os objetos a serem compartilhados (contexto), um objeto que funciona como um mapa. Em linguagens com tipagem estática isso pode implicar em um perigo a mais, um cast forçado:
Uma outra solução pouco utilizada e que poderia aumentar a legibilidade em troca de menos visibilidade é fazer o lazy load de acesso a variável através do method_missing, algo como:
defmethod\_missing(sym, args) return env```sym
if env.include?(sym.to\_s) super(sym, args) end
Compor um comportamento através de outros é fundamental para diminuir a complexidade de métodos e classes. Podemos fazer isso através de herança, pagando-se um preço alto, ou utilizar chain of responsability, interceptors e outros padrões, que podem ser implementados puramente através de interfaces e composição.