Código-Exemplo (Sample Code)
3.4 Diagrama de Classes
O diagrama de classes UML do Architecture Configurator é mostrado na figura 3-9. Todos os elementos estão presentes: componentes, representados pelas classes Cliente e Servidor, além de Servidor Handler; conectores, representados pela classe Conector Handler; portas, representadas pela classe Porta; configuração representada pela classe de mesmo nome.
A classe Conector Handler representa os conectores configurados para intermediar a interação entre os componentes. Cada classe Conector Handler definida na implementação de uma configuração arquitetural trata, preferencialmente, de um requisito não-funcional apropriado.
3.5 Aspectos
Programar a configuração é selecionar, especificar e organizar topologicamente os componentes e conectores de um sistema de software. A partir de uma ADL o projetista define os tipos para portas, conectores e componentes e configura sua aplicação instanciando componentes e conectores, associando portas a componentes e interligando as instâncias de componentes às instâncias de conectores.
As ADLs possuem instruções específicas para realizar as tarefas citadas acima: instruções para definir tipos, instruções para instanciar e instruções para ligar componentes e conectores.
conector sucessor cfg Interface Servidor Cliente Configuração (Descrição/Execução) Conector
Handler ServidorHandler
getLink() setLink() forward() handle() handle() handle() pre() pos() cfg sucessor = cfg->getlink() sucessor->handle() conector = cfg->getlink() conector->handle() this.pre() this.forward() this.pos() Servidor serviço1() serviço2() Handler Porta Configurador serviço1() serviço2()
Figura 3-9 Diagrama de classes completo do design pattern Architecture Configurator.
O design pattern proposto nesta dissertação privilegia o aspecto de implementação da configuração arquitetural, ou seja, a partir de uma configuração determinada apresentamos um modelo que sirva de base à sua implementação.
O modelo pressupõe a preexistência da configuração de determinada aplicação. Isso não impede contudo que a configuração seja modificada, tendo portanto um aspecto dinâmico. A classe Configuração pode fornecer operações que permitam criar novos componentes e conectores, refazer ou criar novas ligações, etc. Tanto a classe abstrata Handler quanto as classes concretas que representam os conectores podem, se necessário, invocar tais operações de Configuração, dando assim uma conotação dinâmica à configuração.
A mudança da arquitetura em tempo de execução é portanto possível partindo-se do design pattern Architecture Configurator. Não importa a funcionalidade das classes concretas que representam os conectores, pois sempre haverá a interceptação para o tratamento das requisições e das respostas, e o encadeamento destas classes de conectores. Isso mostra que nossa proposta de design pattern atinge seu objetivo: servir de base para a implementação da configuração de arquiteturas. Modificações, como as citadas, podem ser feitas. Contudo a base permanece e mostra que o design pattern é geral e entrega uma solução a um problema específico e recorrente.
Outro aspecto importante do Architecture Configurator é o suporte do mesmo à construção de aplicações baseadas na programação em meta-nível (meta-level
programming). Existe uma correspondência imediata entre programação em meta-nível
e programação da configuração [LLS99]. Ambos paradigmas têm a seguinte correspondência [LLS99]:
componentes do nível base ↔ componentes pontos de reificação ↔ portas
componentes do meta-nível ↔ conectores
Programar em meta-nível pressupõe a existência de componentes em dois níveis: nível base e meta-nível. Os componentes do nível base podem ser representados, no design pattern proposto, pelos componentes que requerem e/ou fornecem serviços. Quanto ao meta-nível, seus componentes podem ser concebidos através da composição de conectores herdados da classe Handler. A comutação entre o nível base e o meta-nível é chamada reificação. Duas classes no design pattern Architecture Configurator realizam a reificação: Interface Servidor e Handler. A primeira, através da
interceptação, provoca a comutação do nível base (componente requisitando serviço) para o meta-nível (conectores). A classe abstrata Handler, por sua vez, faz com que seja possível a comutação do meta-nível (conectores) para o nível base (componente fornecendo serviço). As portas neste contexto caracterizam o que se chama, na programação em meta-nível, de pontos de reificação.
Architecture Configurator pode portanto auxiliar na integração de ambos
paradigmas. A figura 3-10 resume as correspondências entre programação da configuração, programação em meta-nível e o respectivo design pattern.
programação da configuração programação em meta-nível design pattern Architecture Configurator
Componentes componentes nível base classes Cliente e Servidor
Portas pontos de reificação classe Porta; operações ou métodos
Conectores componentes meta-nível classe Handler e classes concretas herdeiras
Figura 3-10 Correspondência entre as abstrações programação da configuração, programação do meta-nível e o design pattern Architecture Configurator.
3.6 Conclusão
Este capítulo apresentou a proposta de um design pattern baseado no paradigma de configuração. Os elementos fundamentais foram discutidos – portas, componentes, conectores e configuração – e correlacionados aos conceitos de
arquitetura de software. O modelo foi apresentado na forma de um diagrama de classes e aspectos foram discutidos quanto à programação da configuração.
O design pattern parte do problema de interação de componentes em uma aplicação mantendo a separação de requisitos e por conseguinte a reutilização de software. Sua solução foi originada da observação quanto a padrões de recorrência na implementação da configuração e de conectores.
O próximo capítulo descreve o design pattern Architecture Configurator quanto ao contexto no qual atua, ao problema que propõe-se a resolver e à solução proposta. A descrição apresenta mais detalhes relativos aos elementos do modelo da figura 3-9 e aos mecanismos empreendidos. Mostra ainda exemplos de aplicações, formas de implementação e exemplos de codificação em linguagem Java.
66
&
' !
( #
O capítulo anterior apresentou a proposta de um design pattern que serve de base à implementação da configuração arquitetural de componentes e conectores. A descrição das classes, suas funções e correspondências aos conceitos de arquitetura de software foram expostos.
Neste capítulo apresentamos a descrição completa do design pattern denominado Architecture Configurator, à qual segue o estilo mostrado no capítulo 2. Por se tratar de um design pattern acrescentamos os elementos Estrutura, Participantes, Colaborações e Implementação na apresentação da solução do problema, os quais são oriundos do formato GoF, precursor na definição de design patterns. No elemento Estrutura e Participantes mostramos, além do diagrama de classes e de objetos, os
cartões CRC ou CRC cards. Incluímos ainda o elemento Exemplo, o qual apresenta um exemplo ilustrativo do problema descrito.
4.1 Descrição
Esta seção apresenta a descrição do design pattern Architecture
Configurator. Ele possui quatorze elementos, quais sejam: Nome, Contexto, Exemplo,
Problema, Forças, Solução, Estrutura e Participantes, Colaborações, Implementação, Conseqüências, Patterns Relacionados, Usos Conhecidos, e Código-Exemplo.
Nome
Architecture Configurator.
Contexto
A arquitetura de uma aplicação envolve componentes e um ou mais conectores. Os componentes interagem uns com os outros, requerendo e/ou fornecendo serviços, e os conectores intermediam essa interação. Um componente, neste contexto, refere-se a um processo, objeto, procedure, ou qualquer pedaço de código ou dados identificável.
Exemplo
Como exemplo consideremos uma possível arquitetura para a aplicação Produtor-Consumidor com buffer limitado (PCB) apresentada na figura 4-1. Ela é composta de três componentes (Produtor, Consumidor e Buffer) e um conector
(Guarda). Os componentes interagem, requerendo ou fornecendo algum tipo de serviço, e possuem, cada um, sua funcionalidade intrínseca (requisitos funcionais): o Produtor produz itens requerendo o serviço específico para armazenar itens no Buffer (put); o Buffer armazena/recupera itens fornecendo os respectivos serviços (put/get); o Consumidor consome itens requerendo o serviço específico para retirar itens do Buffer (get). Produz Item Consome Item estado put get Armazena Item Retira Item Consumidor Produtor Buffer Guarda portas
Figura 4-1 Arquitetura da aplicação Produtor-Consumidor com buffer limitado. Os componentes Produtor, Consumidor e Buffer agem como se o conector Guarda não existisse.
Os conector Guarda tem a função de intermediar a interação entre os componentes tratando da sincronização de acesso ao Buffer, considerado um requisito não-funcional. Guarda retarda a execução de Consumidor se Buffer estiver vazio e retarda a execução de Produtor se Buffer estiver cheio.
O conector intercepta, analisa e encaminha as requisições feitas por Produtor e Consumidor através de pontos de acesso chamados portas. As portas estão presentes tanto nos componentes quanto no conector e podem ser de entrada ou de saída. Uma porta de entrada representa o fornecimento de um serviço e uma porta de saída representa a requisição de um serviço. Na arquitetura da figura 4-1, Buffer possui duas portas de entrada, Produtor e Consumidor possuem, cada um, uma porta de saída e
o conector possui duas portas de entrada e três portas de saída. O conector Guarda intercepta as requisições vindas de Produtor e Consumidor através de suas duas portas de entrada e checa o estado do Buffer através de sua porta de saída ligada à porta de entrada estado de Buffer. Conforme o estado obtido, o conector encaminha a requisição normalmente através das outras portas de saída.
As portas dos componentes representam serviços que poderão ser interceptados para o respectivo tratamento não-funcional. Tal interceptação é realizada de forma transparente em relação aos componentes envolvidos, ou seja, os componentes ignoram quaisquer mecanismos de intermediação existentes entre eles.
A descrição da arquitetura da figura 4-1 parte da definição dos tipos correspondentes aos componentes (Produtor, Consumidor e Buffer), do tipo correspondente aos conector (Guarda) e ainda das portas. Estas definições são feitas através de uma ADL (Architecture Description Language). O código 4-1 mostra a descrição arquitetural da aplicação PCB utilizando-se a ADL Babel [Mal96], empregada no ambiente de suporte a arquiteturas R-RIO (Reflective-Reconfigurable
Interconnectable Objects) [Lob99].
Tipos para as portas, componentes e para o conector estão definidos entre as linhas 02 e 26. A partir da linha 27, há instruções que instanciam os tipos definidos (instantiate), interligam as instâncias (link) e iniciam (start) a aplicação.
Essa definição é abstrata e deve ser configurada, criando-se instâncias de componentes e conectores, as quais devem ser ligadas formando a estrutura topológica da arquitetura. O processo de instanciação de componentes e conectores e suas interligações é chamado Programação da Configuração. A partir do nosso exemplo,
vários produtores e consumidores podem ser instanciados e interligados a uma instância de Buffer através de uma instância de Guarda. A interligação é configurada através das portas definidas nos tipos referentes aos componentes e ao conector.
1 module PCB {
2 port Put (Any Item) (void); 3 port Get (void) (Any Item); 4 port Estado (void) (String); 5 6 module Tipo_Buffer { 7 inport Put; 8 inport Get; 9 inport Estado; 10 } buffer; 11 12 module Tipo_Produtor { 13 outport Put; 14 } produtor; 15 16 module Tipo_Consumidor { 17 outport Get; 18 } consumidor; 19 20 connector Guarda { 21 inport Put; 22 inport Get; 23 outport Put; 24 outport Get; 25 outport Estado; 26 } guarda; 27
28 instantiate buffer, produtor, consumidor, guarda; 29 link produtor to buffer by guarda;
30 link consumidor to buffer by guarda; 31
32 } pcb; 33
34 start pcb;
Código 4-1 Descrição da arquitetura da aplicação Produtor-Consumidor com buffer limitado (PCB).
O suporte à abstração e o auxílio na separação de requisitos funcionais de não-funcionais torna componentes e conectores mais independentes e, portanto, mais reutilizáveis.
Problema
A composição de uma aplicação parte do estabelecimento dos componentes envolvidos e das suas interações. A definição de um sistema em termos dos componentes e das interações entre estes componentes é chamada Arquitetura de um Sistema de Software [SDZ96], a qual é descrita através de uma ADL (Architecture
Description Language). O arquiteto, ao utilizar uma ADL, seleciona e especifica os
componentes do sistema e os conectores. Este processo é chamado Programação da Configuração, ou simplesmente, Configuração.
Para a aplicação do exemplo anterior, a arquitetura pode ser descrita como no código 4-1, com 2 (dois) componentes e 1 (um) conector e suas respectivas portas. Instâncias dos componentes e do conector podem ser criadas e configuradas formando a topologia da arquitetura.
Uma vez descrita a arquitetura, sua configuração deve ser implementada, ou seja, o mecanismo que concretiza a troca de mensagens (requisições e respostas) entre as instâncias de componentes e conectores deve ser estabelecido, conforme definido pela configuração arquitetural.
Forças
Uma solução para o problema da implementação de configurações arquiteturais deve considerar o seguinte:
- Propriedades da arquitetura como a reutilização de componentes e conectores, a separação de requisitos e a abstração, devem ser preservadas na implementação de sua configuração.
- Componentes e conectores devem ser independentes quanto à sua definição. Esta ortogonalidade permite que modificações em componentes não afetem conectores, e vice-versa.
- Componentes de um sistema não devem ter suas interfaces e comportamentos modificados com o objetivo de adicionar requisitos não-funcionais ao mesmo.
- Componentes e conectores devem preferencialmente ser coesos quanto aos seus requisitos.
- A especificação da interface dos componentes deve tratar preferencialmente dos requisitos funcionais dos mesmos e deve ser claramente definida.
- Conectores podem ser utilizados para estender a arquitetura quanto a novos serviços não-funcionais.
Solução
A figura 4-2 apresenta uma solução2 para a implementação da configuração da arquitetura de Produtor-Consumidor com buffer limitado. As classes
2 Orientação a objetos é aplicada na solução, embora o problema e as forças apresentadas não necessariamente exijam tal abordagem.
Produtor, Consumidor e Buffer Real representam os componentes e a classe Guarda representa o conector da arquitetura. A programação da configuração está representada pela classe Configuração (Descrição/Execução).
sucessor Interface Buffer Produtor Configuração (Descrição/Execução) Guarda Buffer forward() handle() get() put() handle() handle() get() put() buffer->handle() guarda->handle() this.prePut() this.forward() this.posPut() Buffer Real get() put() Handler Porta Consumidor Configurador
Figura 4-2 Aplicação Produtor-Consumidor com buffer limitado. A classe Configuração (Descrição/Execução) representa a configuração da aplicação.
Configurador é uma classe relacionada à definição da arquitetura; através dela são definidos os tipos para componentes, conectores e portas, a instanciação e interligação dos mesmos, além de permitir a execução da aplicação. Configurador atua como um mecanismo que interpreta as instruções de uma determinada ADL e invoca os serviços da classe Configuração para executá-las.
Configuração (Descrição/Execução), ou apenas Configuração, recebe as solicitações de Configurador para realizar a configuração de componentes e conectores, e iniciá-los. Ela mantém duas categorias de informações: de descrição e de execução da arquitetura. Informações de descrição referem-se à arquitetura descrita através de uma ADL (código 4-1), enquanto as informações de execução relacionam-se às instâncias – referências – dos componentes e conectores durante o processo de execução da aplicação.
A classe abstrata Handler é responsável pelo encadeamento entre conectores e componentes conforme a descrição arquitetural, disponibilizada por Configuração. Handler utiliza-se também das informações de execução da mesma classe, uma vez que necessita das referências aos objetos que representam componentes e conectores no espaço de execução da aplicação. No modelo, as classes Guarda e Buffer são encadeadas por Handler. Buffer representa a classe Buffer Real no encadeamento e possui uma referência a esta última.
As requisições invocadas por Produtor e Consumidor são interceptadas pela classe Interface Buffer, que possui a mesma interface de Buffer Real. Interface Buffer busca em Configuração a referência ao conector Guarda e invoca a operação
handle() do mesmo.
Conforme a porta de entrada configurada no conector Guarda, handle()
invoca a operação get() ou put(). Ambas operações tratam da sincronização (requisito não-funcional) e invocam forward() responsável por dar seguimento ao encadeamento controlado por Handler. Na seqüência, Buffer tem sua operação
handle() solicitada, a qual encaminha a requisição original para Buffer Real, finalizando o encadeamento.
Nesse modelo as portas de entrada configuradas na descrição da arquitetura são mapeadas para operações. Por exemplo, as duas portas de entrada definidas no conector Guarda (figura 4-1) são mapeadas para as operações get() e
put() da classe Guarda.
Algumas propriedades:
- A reutilização de componentes e conectores é possível uma vez que suas funcionalidades estão separadas dos processos de interceptação e encaminhamento das requisições, os quais são realizados pelas classes Interface Buffer e Handler respectivamente.
- Modificações funcionais ou de interface de componentes não afetam os conectores, e vice-versa. Isto ocorre pois classes distintas estão definidas para componentes e conectores e a classe Porta mantém os pontos de acesso dos mesmos, independentemente das interfaces. - Mapeamentos de portas de entrada para operações definidas nas
classes, e de portas de saída para invocações a operações, são mantidos pela classe Porta. Isto torna componentes e conectores livres para terem suas interfaces modificadas de forma transparente, sem afetar os demais elementos da configuração implementada.
- A classe Configuração mantém informações da arquitetura configurada, tanto sua descrição quanto sua execução, e disponibiliza operações suficientes para a recuperação das mesmas por parte das classes Interface Buffer e Handler.
- Um conector tem a responsabilidade de rotear as mensagens para outros conectores e componentes interligados.
- Mais conectores podem ser definidos para refletir novos serviços não-funcionais.