Programação Orientada a Aspectos - Aspect-oriented programming

Na computação , a programação orientada a aspectos ( AOP ) é um paradigma de programação que visa aumentar a modularidade , permitindo a separação de interesses transversais . Ele faz isso adicionando comportamento adicional ao código existente (um conselho ) sem modificar o próprio código, em vez de especificar separadamente qual código é modificado por meio de uma especificação de " ponto de corte ", como "registrar todas as chamadas de função quando o nome da função começar com 'set ' " Isso permite que comportamentos que não são centrais para a lógica de negócios (como registro) sejam adicionados a um programa sem sobrecarregar o núcleo do código para a funcionalidade.

AOP inclui métodos de programação e ferramentas que suportam a modularização de interesses no nível do código-fonte, enquanto o desenvolvimento de software orientado a aspectos se refere a uma disciplina de engenharia inteira.

A programação orientada a aspectos envolve quebrar a lógica do programa em partes distintas (as chamadas preocupações , áreas coesas de funcionalidade). Quase todos os paradigmas de programação suportam algum nível de agrupamento e encapsulamento de interesses em entidades separadas e independentes, fornecendo abstrações (por exemplo, funções, procedimentos, módulos, classes, métodos) que podem ser usados ​​para implementar, abstrair e compor esses interesses. Algumas preocupações "atravessam" várias abstrações em um programa e desafiam essas formas de implementação. Essas preocupações são chamadas de preocupações transversais ou horizontais.

O registro exemplifica uma preocupação transversal porque uma estratégia de registro afeta necessariamente todas as partes registradas do sistema. O log, portanto, corta todas as classes e métodos registrados.

Todas as implementações de AOP têm algumas expressões transversais que encapsulam cada preocupação em um lugar. A diferença entre as implementações reside no poder, segurança e usabilidade dos construtos fornecidos. Por exemplo, interceptores que especificam os métodos para expressar uma forma limitada de corte transversal, sem muito suporte para segurança de tipo ou depuração. AspectJ tem várias dessas expressões e as encapsula em uma classe especial, um aspecto . Por exemplo, um aspecto pode alterar o comportamento do código de base (a parte não-aspecto de um programa) aplicando conselhos (comportamento adicional) em vários pontos de junção (pontos em um programa) especificados em uma quantificação ou consulta chamada ponto de corte ( que detecta se um determinado ponto de junção corresponde). Um aspecto também pode fazer mudanças estruturais compatíveis com o binário para outras classes, como adicionar membros ou pais.

História

AOP tem vários antecedentes diretos A1 e A2: protocolos de reflexão e metaobjeto , programação orientada ao assunto , Filtros de composição e Programação adaptativa.

Gregor Kiczales e colegas da Xerox PARC desenvolveram o conceito explícito de AOP e seguiram isso com a extensão AspectJ AOP para Java. A equipe de pesquisa da IBM buscou uma abordagem de ferramenta sobre uma abordagem de design de linguagem e, em 2001, propôs o Hyper / J e o Concern Manipulation Environment , que não têm sido amplamente utilizados.

Os exemplos neste artigo usam AspectJ.

O Microsoft Transaction Server é considerado a primeira aplicação principal de AOP, seguido por Enterprise JavaBeans .

Motivação e conceitos básicos

Normalmente, um aspecto está espalhado ou emaranhado como código, tornando-o mais difícil de entender e manter. Ele é espalhado em virtude da função (como registro) ser espalhada por uma série de funções não relacionadas que podem usar sua função, possivelmente em sistemas totalmente não relacionados, diferentes idiomas de origem, etc. Isso significa que alterar o registro pode exigir a modificação de todos os módulos afetados . Os aspectos ficam emaranhados não apenas com a função principal dos sistemas nos quais são expressos, mas também entre si. Isso significa que mudar uma preocupação envolve a compreensão de todas as preocupações emaranhadas ou ter algum meio pelo qual o efeito das mudanças possa ser inferido.

Por exemplo, considere um aplicativo bancário com um método conceitualmente muito simples para transferir um valor de uma conta para outra:

void transfer(Account fromAcc, Account toAcc, int amount) throws Exception {
  if (fromAcc.getBalance() < amount)
      throw new InsufficientFundsException();

  fromAcc.withdraw(amount);
  toAcc.deposit(amount);
}

No entanto, esse método de transferência ignora certas considerações que um aplicativo implantado exigiria: ele carece de verificações de segurança para verificar se o usuário atual tem autorização para executar esta operação; uma transação de banco de dados deve encapsular a operação para evitar a perda acidental de dados; para diagnóstico, a operação deve ser registrada no log do sistema, etc.

Uma versão com todas essas novas preocupações, por exemplo, poderia ser mais ou menos assim:

void transfer(Account fromAcc, Account toAcc, int amount, User user,
    Logger logger, Database database) throws Exception {
  logger.info("Transferring money...");
  
  if (!isUserAuthorised(user, fromAcc)) {
    logger.info("User has no permission.");
    throw new UnauthorisedUserException();
  }
  
  if (fromAcc.getBalance() < amount) {
    logger.info("Insufficient funds.");
    throw new InsufficientFundsException();
  }

  fromAcc.withdraw(amount);
  toAcc.deposit(amount);

  database.commitChanges();  // Atomic operation.

  logger.info("Transaction successful.");
}

Neste exemplo, outros interesses ficaram emaranhados com a funcionalidade básica (às vezes chamada de preocupação de lógica de negócios ). Transações, segurança e registro, todos exemplificam preocupações transversais .

Agora, considere o que acontece se de repente precisarmos mudar (por exemplo) as considerações de segurança para o aplicativo. Na versão atual do programa, as operações relacionadas à segurança aparecem espalhadas por vários métodos, e tal mudança exigiria um grande esforço.

O AOP tenta resolver este problema permitindo que o programador expresse preocupações transversais em módulos autônomos chamados aspectos . Os aspectos podem conter conselhos (código unido a pontos especificados no programa) e declarações entre tipos (membros estruturais adicionados a outras classes). Por exemplo, um módulo de segurança pode incluir conselhos que realizam uma verificação de segurança antes de acessar uma conta bancária. O pointcut define os momentos ( pontos de junção ) em que se pode acessar uma conta bancária, e o código no corpo do aviso define como a verificação de segurança é implementada. Dessa forma, o cheque e os locais podem ser mantidos em um só lugar. Além disso, um bom corte de pontos pode antecipar mudanças posteriores no programa, portanto, se outro desenvolvedor criar um novo método para acessar a conta bancária, o conselho se aplicará ao novo método quando ele for executado.

Portanto, para o exemplo acima, implementando o registro em um aspecto:

aspect Logger {
  void Bank.transfer(Account fromAcc, Account toAcc, int amount, User user, Logger logger)  {
    logger.info("Transferring money...");
  }

  void Bank.getMoneyBack(User user, int transactionId, Logger logger)  {
    logger.info("User requested money back.");
  }

  // Other crosscutting code.
}

Pode-se pensar no AOP como uma ferramenta de depuração ou como uma ferramenta de nível de usuário. O conselho deve ser reservado para os casos em que você não pode alterar a função (nível de usuário) ou não deseja alterar a função no código de produção (depuração).

Modelos de ponto de junção

O componente relacionado ao conselho de uma linguagem orientada a aspectos define um modelo de ponto de junção (JPM). Um JPM define três coisas:

  1. Quando o conselho pode ser executado. Eles são chamados de pontos de junção porque são pontos em um programa em execução onde comportamentos adicionais podem ser unidos de forma útil. Um ponto de junção precisa ser endereçável e compreensível por um programador comum para ser útil. Ele também deve ser estável em todas as mudanças inconseqüentes do programa para que um aspecto seja estável em tais mudanças. Muitas implementações de AOP suportam execuções de métodos e referências de campo como pontos de junção.
  2. Uma maneira de especificar (ou quantificar ) pontos de junção, chamados de pontos de corte . Os pontos de corte determinam se um determinado ponto de junção é compatível. As linguagens de pointcut mais úteis usam uma sintaxe como a linguagem base (por exemplo, AspectJ usa assinaturas Java) e permitem a reutilização por meio de nomenclatura e combinação.
  3. Um meio de especificar o código a ser executado em um ponto de junção. AspectJ chama esse conselho e pode executá-lo antes, depois e em torno dos pontos de junção. Algumas implementações também oferecem suporte a coisas como definir um método em um aspecto em outra classe.

Os modelos de ponto de junção podem ser comparados com base nos pontos de junção expostos, como os pontos de junção são especificados, as operações permitidas nos pontos de junção e os aprimoramentos estruturais que podem ser expressos.

Modelo de ponto de junção de AspectJ

  • Os pontos de junção em AspectJ incluem método ou chamada ou execução de construtor, a inicialização de uma classe ou objeto, acesso de leitura e gravação de campo, manipuladores de exceção, etc. Eles não incluem loops, super chamadas, cláusulas de lançamento, várias instruções, etc.
  • Os pontos de corte são especificados por combinações de designadores de pontos de corte primitivos (PCDs).

    PCDs "Kinded" correspondem a um tipo específico de ponto de junção (por exemplo, execução de método) e tendem a receber como entrada uma assinatura semelhante a Java. Um desses pontos de corte se parece com este:

     execution(* set*(*))
    

    Este ponto de corte corresponde a um ponto de junção de execução de método, se o nome do método começar com " set" e houver exatamente um argumento de qualquer tipo.

    Os PCDs "dinâmicos" verificam os tipos de tempo de execução e as variáveis ​​de ligação. Por exemplo,

      this(Point)
    

    Este pointcut corresponde quando o objeto atualmente em execução é uma instância de classe Point. Observe que o nome não qualificado de uma classe pode ser usado por meio da pesquisa de tipo normal do Java.

    Os PCDs de "escopo" limitam o escopo léxico do ponto de junção. Por exemplo:

     within(com.company.*)
    

    Este pointcut corresponde a qualquer ponto de junção em qualquer tipo no com.companypacote. O *é uma forma dos curingas que podem ser usados para combinar muitas coisas com uma assinatura.

    Os pontos de corte podem ser compostos e nomeados para reutilização. Por exemplo:

     pointcut set() : execution(* set*(*) ) && this(Point) && within(com.company.*);
    
    Este ponto de corte corresponde a um ponto de junção de execução de método, se o nome do método começar com " set" e thisfor uma instância do tipo Pointno com.companypacote. Ele pode ser referido com o nome " set()".
  • O conselho especifica a execução em (antes, depois ou ao redor) de um ponto de junção (especificado com um ponto de corte) determinado código (especificado como código em um método). O tempo de execução AOP invoca Advice automaticamente quando o pointcut corresponde ao ponto de junção. Por exemplo: after (): set () {Display.update (); } Isso especifica efetivamente: "se o set()pointcut corresponder ao ponto de junção, execute o código Display.update()após a conclusão do ponto de junção."

Outros modelos de ponto de junção em potencial

Existem outros tipos de JPMs. Todas as linguagens de aconselhamento podem ser definidas em termos de seu JPM. Por exemplo, uma linguagem de aspecto hipotético para UML pode ter o seguinte JPM:

  • Os pontos de junção são todos os elementos do modelo.
  • Os pontos de corte são alguma expressão booleana que combina os elementos do modelo.
  • Os meios de afetar esses pontos são uma visualização de todos os pontos de junção correspondentes.

Declarações entre tipos

As declarações entre tipos fornecem uma maneira de expressar preocupações transversais que afetam a estrutura dos módulos. Também conhecido como classes abertas e métodos de extensão , isso permite que os programadores declarem em um lugar membros ou pais de outra classe, normalmente para combinar todo o código relacionado a uma preocupação em um aspecto. Por exemplo, se um programador implementou a preocupação crosscutting display-update usando visitantes em vez disso, uma declaração entre tipos usando o padrão de visitante pode ter a seguinte aparência em AspectJ:

  aspect DisplayUpdate {
    void Point.acceptVisitor(Visitor v) {
      v.visit(this);
    }
    // other crosscutting code...
  }

Este trecho de código adiciona o acceptVisitormétodo à Pointclasse.

É um requisito que quaisquer adições estruturais sejam compatíveis com a classe original, para que os clientes da classe existente continuem a operar, a menos que a implementação do AOP possa esperar controlar todos os clientes o tempo todo.

Implementação

Os programas AOP podem afetar outros programas de duas maneiras diferentes, dependendo das linguagens e ambientes subjacentes:

  1. um programa combinado é produzido, válido na língua original e indistinguível de um programa comum para o intérprete final
  2. o intérprete ou ambiente final é atualizado para compreender e implementar os recursos de AOP.

A dificuldade de mudar os ambientes significa que a maioria das implementações produz programas de combinação compatíveis por meio de um tipo de transformação de programa conhecido como entrelaçamento . Um criador de aspectos lê o código orientado a aspectos e gera código orientado a objetos apropriado com os aspectos integrados. A mesma linguagem AOP pode ser implementada por meio de uma variedade de métodos de entrelaçamento, portanto, a semântica de uma linguagem nunca deve ser entendida em termos de implementação de entrelaçamento. Apenas a velocidade de uma implementação e sua facilidade de implementação são afetadas pelo método de combinação usado.

Os sistemas podem implementar a combinação de nível de origem usando pré-processadores (como C ++ foi implementado originalmente no CFront ) que requerem acesso aos arquivos de origem do programa. No entanto, a forma binária bem definida do Java permite que os criadores de bytecode trabalhem com qualquer programa Java na forma de arquivo .class. Os tecelões de bytecode podem ser implantados durante o processo de construção ou, se o modelo de tecelagem for por classe, durante o carregamento da classe. A AspectJ começou com a tecelagem em nível de origem em 2001, entregou um tecelão de bytecode por classe em 2002 e ofereceu suporte avançado de tempo de carregamento após a integração da AspectWerkz em 2005.

Qualquer solução que combine programas em tempo de execução deve fornecer visualizações que os separem adequadamente para manter o modelo segregado do programador. O suporte de bytecode do Java para vários arquivos de origem permite que qualquer depurador passe por um arquivo .class tecido corretamente em um editor de código-fonte. No entanto, alguns descompiladores de terceiros não podem processar código tecido porque esperam código produzido por Javac em vez de todos os formulários de bytecode suportados (consulte também § Críticas , abaixo).

A tecelagem no tempo de implantação oferece outra abordagem. Isso basicamente implica pós-processamento, mas em vez de corrigir o código gerado, essa abordagem de entrelaçamento cria subclasses de classes existentes para que as modificações sejam introduzidas por sobreposição de método. As classes existentes permanecem intocadas, mesmo em tempo de execução, e todas as ferramentas existentes (depuradores, criadores de perfil, etc.) podem ser usadas durante o desenvolvimento. Uma abordagem semelhante já foi comprovada na implementação de muitos servidores de aplicativos Java EE , como o WebSphere da IBM .

Terminologia

A terminologia padrão usada na programação orientada a aspectos pode incluir:

Preocupações transversais
Embora a maioria das classes em um modelo OO execute uma função única e específica, elas geralmente compartilham requisitos secundários comuns com outras classes. Por exemplo, podemos querer adicionar log às classes dentro da camada de acesso a dados e também às classes na camada UI sempre que um thread entra ou sai de um método. Outras preocupações podem estar relacionadas à segurança, como controle de acesso ou controle de fluxo de informações . Mesmo que cada classe tenha uma funcionalidade primária muito diferente, o código necessário para executar a funcionalidade secundária geralmente é idêntico.
Adendo
Este é o código adicional que você deseja aplicar ao seu modelo existente. Em nosso exemplo, este é o código de registro que desejamos aplicar sempre que o thread entrar ou sair de um método.
Pointcut
Este é o termo dado ao ponto de execução na aplicação em que a preocupação transversal deve ser aplicada. Em nosso exemplo, um corte de ponto é alcançado quando o thread entra em um método e outro corte de ponto é alcançado quando o segmento sai do método.
Aspecto
A combinação do pointcut e do conselho é denominada um aspecto. No exemplo acima, adicionamos um aspecto de log ao nosso aplicativo definindo um pointcut e dando o conselho correto.

Comparação com outros paradigmas de programação

Aspectos surgiram da programação orientada a objetos e reflexão computacional . As linguagens AOP têm funcionalidade semelhante, mas mais restrita do que os protocolos de metaobjetos . Aspectos estão intimamente relacionados a conceitos de programação como assuntos , mixins e delegação . Outras maneiras de usar paradigmas de programação orientada a aspectos incluem Filtros de Composição e a abordagem de hiperslices . Desde pelo menos a década de 1970, os desenvolvedores têm usado formas de interceptação e remendo de despacho que se assemelham a alguns dos métodos de implementação para AOP, mas nunca tiveram a semântica que as especificações transversais fornecem escrita em um só lugar.

Os designers consideraram maneiras alternativas de conseguir a separação do código, como os tipos parciais do C # , mas essas abordagens carecem de um mecanismo de quantificação que permite alcançar vários pontos de junção do código com uma instrução declarativa.

Embora possa parecer não relacionado, em testes, o uso de simulações ou stubs requer o uso de técnicas de AOP, como conselhos e assim por diante. Aqui, os objetos de colaboração são, para fins de teste, uma preocupação transversal. Assim, as várias estruturas de objetos simulados fornecem esses recursos. Por exemplo, um processo chama um serviço para obter um valor de saldo. No teste do processo, de onde vem a quantidade não importa, apenas que o processo utilize o saldo de acordo com as necessidades.

Problemas de adoção

Os programadores precisam ser capazes de ler o código e entender o que está acontecendo para evitar erros. Mesmo com a educação adequada, entender questões transversais pode ser difícil sem o suporte adequado para visualizar a estrutura estática e o fluxo dinâmico de um programa. No início de 2002, AspectJ começou a fornecer plug-ins IDE para apoiar a visualização de interesses transversais. Esses recursos, bem como o assistente de código de aspecto e a refatoração agora são comuns.

Dado o poder do AOP, se um programador cometer um erro lógico ao expressar o corte transversal, isso pode levar a uma falha generalizada do programa. Por outro lado, outro programador pode alterar os pontos de junção em um programa - por exemplo, renomeando ou movendo métodos - de maneiras que o criador de aspectos não antecipou, com consequências imprevistas . Uma vantagem da modularização de interesses transversais é permitir que um programador afete todo o sistema facilmente; como resultado, esses problemas se apresentam como um conflito de responsabilidade entre dois ou mais desenvolvedores por uma determinada falha. No entanto, a solução para esses problemas pode ser muito mais fácil na presença de AOP, uma vez que apenas o aspecto precisa ser alterado, enquanto os problemas correspondentes sem AOP podem ser muito mais espalhados.

Crítica

A crítica mais básica do efeito do AOP é que o fluxo de controle é obscurecido, e que não é apenas pior do que o tão difamado GOTO , mas é de fato intimamente análogo à declaração COME FROM . O esquecimento da aplicação , que é fundamental para muitas definições de AOP (o código em questão não tem nenhuma indicação de que um conselho será aplicado, o que é especificado em seu lugar no ponto de corte), significa que o conselho não é visível, em contraste com um conselho explícito chamada de método. Por exemplo, compare o programa COME FROM:

 5 INPUT X
10 PRINT 'Result is :'
15 PRINT X
20 COME FROM 10
25      X = X * X
30 RETURN

com um fragmento AOP com semântica análoga:

main() {
    input x
    print(result(x))
}
input result(int x) { return x }
around(int x): call(result(int)) && args(x) {
    int temp = proceed(x)
    return temp * temp
}

Na verdade, o ponto de corte pode depender da condição de tempo de execução e, portanto, não ser estaticamente determinístico. Isso pode ser mitigado, mas não resolvido, por meio de análise estática e suporte IDE, mostrando quais avisos são potencialmente correspondentes.

As críticas gerais são que o AOP pretende melhorar "tanto a modularidade quanto a estrutura do código", mas alguns argumentam que, ao invés disso, enfraquece esses objetivos e impede o "desenvolvimento independente e a compreensibilidade dos programas". Especificamente, a quantificação por pointcuts quebra a modularidade: "deve-se, em geral, ter conhecimento do programa completo para raciocinar sobre a execução dinâmica de um programa orientado a aspectos." Além disso, embora seus objetivos (questões transversais de modularização) sejam bem compreendidos, sua definição real não é clara e não é claramente distinguida de outras técnicas bem estabelecidas. Os interesses de corte cruzado potencialmente cortam um ao outro, exigindo algum mecanismo de resolução, como pedido. Na verdade, alguns aspectos podem se aplicar a eles próprios, levando a problemas como o paradoxo do mentiroso .

As críticas técnicas incluem que a quantificação dos pontos de corte (definindo onde os conselhos são executados) é "extremamente sensível a mudanças no programa", o que é conhecido como o problema do ponto de corte frágil . Os problemas com pontos de corte são considerados intratáveis: se substituirmos a quantificação de pontos de corte por anotações explícitas, obtém -se a programação orientada a atributos , que é simplesmente uma chamada de sub-rotina explícita e sofre o mesmo problema de espalhamento que o AOP foi projetado para resolver.

Implementações

As seguintes linguagens de programação implementaram AOP, dentro da linguagem ou como uma biblioteca externa:

Veja também

Notas e referências

Leitura adicional

links externos