ADAPTER DESIGN PATTERN: MAXIMIZANDO A REUTILIZAÇÃO DE COMPONENTES

Autores: Marcelo Fermann Guimarães, Ricardo S Ikematu

ABSTRACT

Designing and implementing a new Object-Oriented system is a challenging task, especially when the implementors are novice Object-Oriented programers. Using design patterns helped us accelerate the design phase of our project, and result in a more easily understandable and better factored design. This article intend to demonstrate how the Adapter Design Pattern help us to reuse others components that weren´t designed for a specific project.

PALAVRAS-CHAVE

Orientação a Objetos; Design Pattern, Adapter; Interface; Reuso; Componente.

INTRODUÇÃO

A origem de design patterns encontra-se em um trabalho feito por um arquiteto chamado Christopher Alexander durante os anos 70 com seu livro "A Pattern Language" [Alex77]. Ele disse que cada "pattern" descreve um problema que ocorre várias vezes em nosso ambiente e então descreve a solução reutilizável para o problema de modo que você não precise procurar a solução mais de uma vez. O conceito foi sendo adaptado e desenvolvido para a área de informática. No entanto este conceito tornou-se popular pela primeira vez com a publicação do livro "Design Patterns: Elements of Reusable Object-Oriented Software" [Gamma95].

Desenvolver software orientado a objetos é difícil. Desenvolver software orientado a objetos reutilizável é muito mais difícil, pois o projeto deve atender um problema específico e também ser geral o suficiente para atender requisitos e problemas futuros. A habilidade de alterar classes de forma a se tornar útil em mais de um caso específico, é uma qualidade normalmente encontrada em projetistas com muita experiência em projeto de software orientado a objetos e em manutenção de sistemas.

Desenvolvedores experientes encontram moldes de classes e objetos em muitos sistemas em que participaram. O desenvolvedor utiliza sua experiência em projetos de sucesso para não ter que redescobrir a solução do problema novamente.

Design patterns são o registro destas experiências. Estes moldes resolvem problemas de projetos específicos e tornam estes projetos mais flexíveis, elegantes e reutilizáveis. Eles ajudam o desenvolvedor a fazer o projeto corretamente e de forma mais rápida.

DESIGN PATTERN

Cada pattern é uma regra de três partes, que expressa uma relação entre um certo contexto, um certo sistemas de forças que ocorre repetidamente neste contexto e certa configuração de software que permite que estas forças se resolvam. Para descrever um pattern as notações gráficas, embora importantes são insuficientes. A utilização de uma estrutura facilita o aprendizado, a comparação e a sua utilização. Esta estrutura é composta das seguintes partes:

  • Nome do pattern e sua classificação. O nome identifica a essência do pattern. Um bom nome é importante porque se tornará parte do vocabulário do projeto.
  • Finalidade. Um texto que descreva o que ele faz e que problemas ele resolve.
  • Sinônimo ou aliás. Outros nomes conhecidos se tiver.
  • Motivação. Descrição de um cenário que ilustra o problema do projeto e como as estruturas do pattern resolvem o problema. Sua função é ajudar a entender o problema.
  • Aplicabilidade. Deve descrever as situações que o pattern pode ser aplicado, quais exemplos de projetos que o pattern poderia ajudar e como se poderia reconhecer estas situações.
  • Estrutura. É uma representação gráfica de classes no pattern. É a descrição dos elementos de projeto, relacionamentos, responsabilidades e colaborações envolvidas na solução.
  • Participantes. As classes e/ou objetos que participam do pattern e suas responsabilidades.
  • Colaborações. Como os participantes colaboram para cumprir suas responsabilidades.
  • Consequências. Descrição de como os patterns suportam seus objetivos e quais os resultados de se usar o pattern, com suas vantagens e desvantagens.
  • Implementação. Descrição das armadilhas, dicas ou técnicas de implementação do pattern.
  • Código exemplo. Código que mostra como implementar o pattern em uma linguagem.
  • Casos conhecidos ou exemplos. Exemplos do uso do pattern em sistemas reais.
  • Patterns relacionados. Descreve uma referência cruzada dos patterns com o descrito. Quais as diferenças entre eles e com quais deles pode ser usado.

Os patterns podem ser classificados por finalidade e escopo.

A finalidade reflete o que o pattern faz e pode ser dividido em:

  • patterns de criação que se referem ao processo de criação de objetos;
  • patterns estruturais tratam da composição das classes ou objetos;
  • patterns comportamentais caracterizam o modo que as classes ou objetos interagem e distribuem responsabilidades.

O escopo determina se o pattern se aplica a classe ou ao objeto. Patterns de classe tratam de relações entre classes através de herança, são estáticos, ou seja, estabelecidos em tempo de compilação. Patterns de objetos tratam de relacionamentos entre objetos que podem ser alterados durante o processamento e são mais dinâmicos.

O conjunto do nome, seus parâmetros e o valor de retorno da operação é conhecido como a assinatura da operação. O conjunto de todas as assinaturas definidas pelas operações do objeto é chamado de interface do objeto. Design patterns ajudam definir interfaces pela identificação de elementos chaves e os tipos de dados que são enviados através da interface.

Eles também especificam relacionamentos entre interfaces.

Em orientação a objetos há certa confusão entre framework e design patterns. Framework é um software executável. Framework" são realizações físicas de um ou mais soluções de patterns. Patterns são as instruções de como implementar estas soluções.

ADAPTER PATTERN

O principal objetivo do Adapter Design Pattern é facilitar a conversão da interface de um classe para outra interface mais interessante para o cliente, fazendo com que várias classes possam trabalhar em conjunto independentemente das interfaces originais. As vezes é preciso modificar uma classe que não pode ser alterada adequadamente devido à falta do código fonte (alguma biblioteca de classes comercial), ou por alguma outra razão. O Adapter Pattern é uma das formas de modificar classes nestas circunstâncias. O Adapter Pattern é classificado como de finalidade estrutural e abrange tanto escopo de classe quanto de objeto. É também conhecido por Wrapper.

Motivação

Algumas vezes uma biblioteca de classes projetada para reuso tem problemas somente porque sua interface não corresponde à interface de um domínio específico que uma aplicação exige.

Considere por exemplo um editor de desenho que deixa usuários desenhar e posicionar elementos gráficos (linhas, polígonos, texto, etc.) em diagramas. Cada objeto gráfico tem a sua forma e pode desenhar a si próprio. A interface para objetos gráficos é definida por uma classe abstrata chamada Forma. O editor define uma subclasse de Forma para cada tipo de objeto gráfico: classe FormaLinha, classe FormaPoligono, FormaTexto, etc. A subclasse FormaTexto que pode mostrar e editar texto é muito difícil de implementar, pois a edição de texto envolve atualizações de tela complicadas e gerenciamento de áreas de memória. Uma biblioteca comercial poderia fornecer uma classe VisaoTexto que manipula texto pronta para usar. O ideal seria reutilizar o VisaoTexto para implementar FormaTexto, mas a biblioteca não foi projetada para ser usada com a interface de Forma impedindo que estas classes se comuniquem. Poderíamos mudar a classe VisaoTexto conforme a interface de Forma, mas para isso precisaríamos do código fonte da biblioteca. Mesmo que tivéssemos, não faria sentido mudar VisaoTexto. A biblioteca não deveria adotar uma interface es-

pecífica para que apenas uma aplicação funcione. Ao invés disto poderíamos definir FormaTexto para adaptar a interface da VisaoTexto para a interface da Forma. Isto poderia ser feito de duas maneiras:

  • Herdando a interface de Forma e a implementação de VisaoTexto;
  • Compondo uma instância de VisaoTexto dentro de FormaTexto e implementando FormaTexto através da interface de VisaoTexto.

Este diagrama mostra como a operação DelimitadorForma, declarado na classe Forma, é convertido para a operação ObtemExtensao definido em VisaoTexto. Como a classe FormaTexto adapta VisaoTexto para interface de Forma, o editor de desenho agora pode reutilizar a classe VisaoTexto. Geralmente o Adapter é responsável pela funcionalidade que a classe adaptada não fornece. O usuário deveria ser capaz de arrastar cada objeto Forma para uma nova localização interativamente, mas o VisaoTexto não foi projetado para isto. FormaTexto pode acrescentar esta funcionalidade implementando a operação CriarManipulador de Forma, que retorna uma instância da operação Manipulador apropriada. Manipulador é uma operação abstrata para objetos que conhece como animar uma Forma em resposta a entrada do usuário.

Aplicabilidade

Use o Adapter Pattern quando:

  • você desejar usar uma classe existente e sua interface não corresponde ao que você precisa;
  • você desejar criar uma classe reutilizável que coopera com classes imprevistas ou não relacionáveis, isto é, classes que não tem necessariamente interfaces compatíveis;
  • (somente para Adapter de objeto) quando for preciso usar várias subclasses existentes, mas é impraticável adaptar as interfaces de cada uma. Um Adapter de objeto pode adaptar a interface da sua classe pai.

Estrutura

Um Adapter de classe usa múltiplas heranças para adaptar uma interface para outra:

Um Adapter de objeto depende de composição de objeto:

Participantes

Alvo (Forma). Define o domínio específico da interface que o cliente usa.

Cliente (Editor de desenho). Colabora com objetos conforme a interface Alvo.

Adaptado (VisaoTexto). Define uma interface existente que necessita adaptação.

Adaptador (FormaTexto). Adapta a interface do Adaptado para a interface do Alvo.

Colaborações

Clientes chamam operações na instância do Adaptador. O Adaptador chama as operações do Adaptado que executa o pedido.

Conseqüências

Um Adapter de classe:

  • Adapta o Adaptado para o Alvo através de uma classe Adapter concreta. Como consequência, uma classe Adapter não funcionará para adaptar uma classe e suas subclasses.
  • Deixa o Adaptador sobrepor algum comportamento do Adaptado, desde que o Adaptador seja uma subclasse do Adaptado.
  • Introduz um único objeto e nenhum ponteiro adicional é necessário para chegar ao adaptado.

Um Adapter de objeto:

  • Deixa um Adaptador trabalhar com vários Adaptados, isto é, o próprio Adaptado e as suas subclasses. O Adaptador acrescenta funcionalidade para todos os Adaptados de uma vez.
  • Dificulta a sobreposição do comportamento do Adaptado. Ele exigirá a especialização do Adaptado e faz o Adaptador se referir para as subclasses em vez do próprio Adaptado.

Ao usar Adapter Pattern deve-se levar em consideração os seguintes pontos:

  • Adapters variam no volume de trabalho que eles geram para adequar o Adaptado para a interface Alvo. Pode ser desde uma simples conversão de interface, como suportar um conjunto completamente diferente de operações. O volume de trabalho do Adapter depende do quão semelhante é a interface do Adaptado em relação ao Alvo.
  • Pluggable Adapters. Uma classe é mais reutilizável quando você minimiza as suposições que as outras classes devem fazer para usá-lo. Construindo uma adaptação de interface dentro da classe, você elimina a hipótese de que outra classe veja a mesma interface. Em outras palavras, a adaptação da interface deixa incorporar a classe dentro de sistemas existentes que poderiam esperar interfaces diferentes para a classe. Object-Works\Smalltalk usa o termo Pluggable Adapter para descrever classes com interface adaptada embutida.
  • Usando Adapters bi-direcionais para fornecer transparência. Um problema com Adapters é que eles não são transparentes para todos os clientes. Um objeto Adaptador não está em conformidade em relação a interface a ser adaptada, então o objeto Adaptado não pode usar o objeto Alvo. Adaptadores bi-direcionais podem fornecer esta transparência. Eles serão úteis quando dois clientes diferentes necessitarem ver um mesmo objeto diferentemente.

Considere o Adaptador bi-direcional que integra o Unidraw, um framework de editor gráfico, e QOCA, uma biblioteca de tratamento de restrições. O Unidraw tem interface EstadoVariavel e QOCA tem RestricaoVariavel. Para trabalharem juntos um deve ser adaptado para o outro.

A solução envolve uma classe Adapter bi-direcional Restrição Estado Variavel, subclasse de EstadoVariavel e RestriçãoVariavel, que adapta as duas interfaces de uma para outra. Herança múltipla é uma solução viável neste caso porque as interfaces das classes adaptadas são substancialmente diferentes. Uma classe Adapter bi-direcional está em conformidade com ambas as classes adaptadas e podem trabalhar em ambos sitemas.

Implementação

A implementação do Adapter normalmente é simples, mas deve-se considerar o seguinte:

  1. Na implementação de uma classe Adapter em C++, o Adaptador herda publicamente da classe Alvo e faz herança privada da classe Adaptada. O Adaptador poderia ser um subtipo de Alvo, mas não do Adaptado. Em Delphi, que não permite herança múltipla, o Adaptador herda publicamente de Alvo e referencia a uma instância específica da classe Adaptada.
  2. Adaptadores plugáveis: O primeiro passo para a implementação é encontrar uma interface mínima para o Adaptado. Essa interface é um subconjunto de operações que nos permite fazer a adaptação. Existem três formas de implementar adaptadores plugáveis.
  1. usando operações abstratas: a classe Adaptadora herda a interface da classe Alvo e a implementação da classe a ser adaptada e sobrecarrega as operações abstratas. Assim a nova classe especializa a interface herdada e pode trabalhar com ela da forma correta;
  2. Usando objetos delegados: desta forma, a nova classe delega a outro objeto o tratamento adequado da operação solicitada, quer estática ou dinâmicamente.
  3. usando Adapters parametrizados: o modo de suportar Adapters plugáveis em Smalltalk, é através da parametrização do adaptador em um ou mais blocos. A construção de blocos permite fazer a adaptação sem subclassificação. Um bloco pode adaptar uma solicitação e o adaptador pode armazenar um bloco para cada solicitação.

Código exemplo

Usamos como exemplo, uma implementação que ilustra o tratamento que pode ser dado à questão do problema do ano 2000 [Heywo96]. Tem-se um classe Customer ( que trata o ano com 4 dígitos ) e uma classe Old Customer ( que trata o ano com 2 dígitos ). A nova classe, Customer, tem o atributo FDOB que trata o ano com 4 dígitos, as operações protegidas são visíveis somente às classes descendentes e as operações property permitem métodos de leitura e escrita, podendo sobrecarregar o operação sem que a classe pai saiba sobre sua nova implementação:

unit Adapter;

interface

uses SysUtils, Classes;

type

{ The new class }

TNewCustomer = class

private

FCustomerID: Longint;

FFirstName: string;

FLastName: string;

FDOB: TDateTime;

protected

function GetCustomerID: Longint; virtual;

function GetFirstName: string; virtual;

function GetLastName: string; virtual;

function GetDOB: TDateTime; virtual;

public

constructor Create(CustID: Longint); virtual;

property CustomerID: Longint read GetCustomerID;

property FirstName: string read GetFirstName;

property LastName: string read GetLastName;

property DOB: TDateTime read GetDOB;

end;

{ An interface method }

{ Lets us hide details of TOldCustomer from the client }

function GetCustomer(CustomerID: Longint): TNewCustomer;

implementation

const

Last_OldCustomer_At_Year_2000 = 15722;

Last_OldCustomer_In_Database = 30000;

{ The new class }

constructor TNewCustomer.Create(CustID: Longint);

begin

FCustomerID := CustID;

FFirstName := 'A';

FLastName := 'New_Customer';

FDOB := Now;

end;

function TNewCustomer.GetCustomerID: Longint;

begin

Result := FCustomerID;

end;

Function TNewCustomer.GetFirstName: string;

begin

Result := FFirstName;

end;

function TNewCustomer.GetLastName: string;

begin

Result := FLastName;

end;

function TNewCustomer.GetDOB: TDateTime;

begin

Result := FDOB;

end;

A classe antiga ( OldCustomer ) trata o ano com 2 dígitos:

type

{ The old class }

TOldDOB = record

Day: 0..31;

Month: 1..12;

Year: 0..99;

end;

TOldCustomer = class

FCustomerID: Integer;

FName: string;

FDOB: TOldDOB;

public

constructor Create(CustID: Integer);

property CustomerID: Integer read FCustomerID;

property Name: string read FName;

property DOB: TOldDOB read FDOB;

end;

constructor TOldCustomer.Create(CustID: Integer);

begin

FCustomerID := CustomerID;

FName := 'An Old_Customer';

with FDOB do begin

Day := 1;

Month := 1;

Year := 1;

end;

end;

A classe Adapter herda de Customer e delega operações à classe antiga (OldCustomer ) sobrecarregando a operação que quer fazer um tratamento diferenciado.

type

{ The Adapter class }

TAdaptedCustomer = class(TNewCustomer)

private

FOldCustomer: TOldCustomer;

protected

function GetCustomerID: Longint; override;

function GetFirstName: string; override;

function GetLastName: string; override;

function GetDOB: TDateTime; override;

public

constructor Create(CustID: Longint); override;

destructor Destroy; override;

end;

{ The Adapter class }

constructor TAdaptedCustomer.Create(CustID: Longint);

begin

inherited Create(CustID);

FOldCustomer := TOldCustomer.Create(CustID);

end;

destructor TAdaptedCustomer.Destroy;

begin

FOldCustomer.Free;

inherited Destroy;

end;

function TAdaptedCustomer.GetCustomerID: Longint;

begin

Result := FOldCustomer.CustomerID;

end;

function TAdaptedCustomer.GetFirstName: string;

var

SpacePos: integer;

begin

SpacePos := Pos(' ', FOldCustomer.Name);

if SpacePos = 0 then

Result := ''

else

Result := Copy(FOldCustomer.Name,1,SpacePos-1);

end;

function TAdaptedCustomer.GetLastName: string;

var

SpacePos: integer;

begin

SpacePos := Pos(' ', FOldCustomer.Name);

if SpacePos = 0 then

Result := FOldCustomer.Name

else

Result := Copy(FOldCustomer.Name,SpacePos+1,255);

end;

function TAdaptedCustomer.GetDOB: TDateTime;

var

FullYear: Word;

begin

if CustomerID > Last_OldCustomer_At_Year_2000 then

FullYear := 2000 + FOldCustomer.DOB.Year

else

FullYear := 1900 + FOldCustomer.DOB.Year;

Result := EncodeDate(FullYear, FOldCustomer.DOB.Month, FOldCustomer.DOB.Day);

end;

function GetCustomer(CustomerID: Longint): TNewCustomer;

begin

if CustomerID > Last_OldCustomer_In_Database then

Result := TNewCustomer.Create(CustomerID)

else

Result := TAdaptedCustomer.Create(CustomerID) as TNewCustomer;

end;

end.

EXEMPLOS

O exemplo de motivação vem do ET++Draw, uma aplicação de desenho baseado em ET++. ET++Draw reutiliza as classes ET++ para edição de texto usando a classe Adapter FormaTexto.

Interviews 2.6 define uma classe abstrata Interactor para elementos de interface de usuário, tais como barras de rolamento, botões e menus. Ele define uma classe abstrata Graphic para objetos gráficos tais como, linhas, círculos, polígonos, etc. Ambas as classes Interactor e Graphic tem aparências gráficas, mas tem interfaces e implementações diferentes. InterView 2.6 define um objeto Adapter GraphicBlock que é uma subclasse de Interactor que contém a instância de Graphic. O GraphicBlock adapta a interface da classe Graphic para o Interactor.

A herança por conveniência é uma forma de classe Adapter. Meyer descreve como uma classe de pilha fixa adapta a implementação de uma classe array para a interface da classe pilha.

O próprio Delphi gera uma nova classe que traduz as interfaces quando se importa objetos VBX ou OCX, fazendo com que sejam compatíveis com a interface Pascal.

PATTERNS RELACIONADOS

O Bridge pattern tem uma estrutura similar a um Adapter de objeto, porém o Bridge tem uma finalidade diferente. Ele serve para separar uma interface de suas implementações de modo que eles podem variar facilmente e independentemente. Um Adapter serve para trocar a interface de um objeto existente.

O Decorator pattern complementa outro objeto sem trocar sua interface. Um Decorator é mais transparente para a aplicação do que um Adapter. Como consequência, o Decorator suporta composição recursiva que é impossível com um Adapter puro.

O Proxy pattern define um representante para outro objeto e não altera sua interface.

CONCLUSÃO

A utilização de patterns se beneficia do conhecimento e experiência de outras pessoas que gastaram um esforço de entender um problema e encontrar uma solução para ele. Os patterns podem ser mais reutilizáveis do que código, pois eles podem ser adaptados a um caso especial que um componente existente não resolve. Patterns também criam uma cultura de nomes e conceitos para tratar problemas de desenvolvimento facilitando a comunicação entre desenvolvedores. Design patterns não deveriam ser aplicados indiscriminadamente. Geralmente eles obtém flexibilidade introduzindo níveis adicionais de abstração que podem complicar o projeto e/ou degradar a performance do projeto. Um design pattern deveria ser aplicado com muito critério. O Adapter pattern, tal como o Bridge e o Decorator, além de permitir uma maior flexibilidade para reutilização, ajudam a minimizar a alteração de códigos de componentes existentes.

REFERÊNCIAS BIBLIOGRÁFICAS:

[Alex77] ALEXANDER, Christopher. A pattern Language: towns, buildings, construction. Oxford: University Press, 1977.

[Gamma95] GAMMA, Erich. Design patterns: elements of reusable object-oriented software. Addison-Wesley, 1995.

[Heywo96] HEYWORTH, James. Introduction to design patterns in Delphi. Camberra PC Users Group Delphi, 1996.