20 de julho de 2011

Usando um Web Service no Visual Studio - parte II

Escrevi um post em 2009 explicando alguns detalhes sobre o funcionamento de Web Services e mostrando como usá-los em um projeto do Visual Studio. Aquele post descrevia como usar o IDE para gerar automaticamente uma classe proxy, necessária para fazermos a chamada das funções remotas disponibilizadas pelo serviço. Tal classe é chamada de "proxy" por ser projetada segundo o design pattern que leva esse nome.

Como se vê no post, gerar o proxy é um procedimento muito simples, assim como utilizá-lo para executar a função remota. Para a grande maioria das situações, usar o Web Service desse modo é suficiente. No entanto, há uma desvantagem na forma como isso é feito. Uma vez que o processo todo é automatizado e o trâmite da comunicação com o servidor fica escondido nas bibliotecas da plataforma .Net, não é possível customizar parte alguma da comunicação.

Mas, porque seria necessário interceptar a comunicação e alterar alguma coisa ? Uma razão óbvia é depuração. Isso é, se acontecer um erro, é interessante saber quais foram os comandos enviados e os respectivos retornos fornecidos pelo servidor. Um sniffer não conseguiria fazer esse serviço caso o web service esteja num canal seguro (HTTPS) pois as informações são criptografadas.

Uma outra razão seria customizar a serialização das informações, oportunidade para enxugar o texto trafegado, removendo-se partes sobressalentes. Ou, ainda, para adequar a formatação dos parâmetros da função que são trafegados. Do ponto de vista do proxy, a serialização é o processo pelo qual o nome da função remota e os parâmetros que passamos para ela são transformados em texto e empacotados num XML antes de serem enviados para execução no servidor. Ao receber uma resposta do servidor, o proxy tem que aplicar o processo inverso no XML recebido, isto é, fazer a desserialização, convertendo o conteúdo do XML em tipos de dados nativos da linguagem e associando-o ao valor de retorno da função. Lembre-se que o protocolo usado para comunicação com web services é o SOAP, que é baseado em XML. Portanto, um parâmetro com tipo de dado complexo pode exigir uma formatação especial pra funcionar.

Já que o código gerado automaticamente pelo IDE do Visual Studio não permite interceptar a serialização, teremos que optar por um método alternativo. O SDK do .NET vem com uma ferramenta de linha de comando chamada WSDL.EXE. Basicamente, nela você informa o arquivo descritor do Web Service (WSDL) e ela gera a classe proxy apropriada. O quadro abaixo mostra um exemplo básico de como usar a ferramenta:
wsdl /language:CS /n:"WNameSpace" WMeuServico.wsdl

Nesse exemplo, o WSDL.EXE interpretará a descrição contida no arquivo WMeuServico.wsdl e gerará um uma classe proxy em C# inserindo-a no namespace WNameSpace. A diferença em relação ao proxy gerado diretamente pelo Visual Studio está na infraestrutura de classes envolvidas. No caso do WSDL.EXE, o proxy é uma herança de SoapHttpClientProtocol. A execução dos métodos dessa classe pode ser extendida usando-se atributos que implementem as funções contidas em SoapExtension. Ou seja, cria-se uma herança de SoapExtension que então é aplicada como um atributo da função existente no Web Service. Quando a função remota é chamada, a serialização passa a ser controlada pela nossa versão do SoapExtension.

O cerne dessa solução é o método ProcessMessage. Ele recebe uma instância com a mensagem SOAP atual, permitindo manipulá-la conforme a necessidade. O exemplo a seguir cria um log com as mensagens trafegadas no Web Service:
public class TWTraceExtension : SoapExtension
{
/* ... */
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
StreamWriter w = new StreamWriter(_LogFile);
w.WriteLine ("=> Serializado");
w.Flush();
message.Stream.Position = 0;
Copy(message.Stream, fs);
w.Close();
break;
case SoapMessageStage.BeforeDeserialize:
StreamWriter w = new StreamWriter(_LogFile);
w.WriteLine ("=> Desserializado");
w.Flush();
message.Stream.Position = 0;
Copy(message.Stream, fs);
w.Close();
break;
case SoapMessageStage.AfterDeserialize:
break;
}
}
/* ... */
}

Agora que temos a extensão do processo de serialização e desserialização, precisamos criar um atributo que seja aplicável ao método do Web Service. Fazemos isso criando uma extensão da classe de atributo SoapExtensionAttribute, cujo objetivo principal é determinar qual classe deve ser usada para serializar. Veja um exemplo:
[AttributeUsage(AttributeTargets.Method)]
public class TWTraceExtensionAttribute : SoapExtensionAttribute
{
/* ... */
public override Type ExtensionType
{
get { return typeof(TWTraceExtension); }
}
/* ... */
}

Veja que a propriedade ExtensionType é sobrescrita para retornar informações sobre nossa classe de serialização. Um outro detalhe é que a declaração de nossa classe de atributo é ela própria configurada por um atributo indicativo do tipo de uso esperado para ela : ser atributo de métodos de outras classes.

Por fim, devemos usar o atributo criado acima para configurar o método do Web Service que vamos utilizar. Isso implica modificar o fonte gerado automaticamente pelo WSDL; então, tal configuração terá que ser refeita se precisarmos gerar o fonte de novo do Web Service. Veja um exemplo de configuração:
/* ... */
public partial class Service1 : System.Web.Services.Protocols.SoapHttpClientProtocol
{
/* ... */
[TWTraceExtensionAttribute(Filename = "c:\\log.txt")]
public string consultaNFe ([System.Xml.Serialization.XmlAnyElementAttribute()]
System.Xml.XmlElement Any)
{
/* ... */
}

A mesma solução pode ser adotada para a classe servidora do Web Service, situação em que estaremos interceptando as mensagens que um Web Service desenvolvido por nós está trocando.

Um exemplo completo de customização do SoapExtension pode ser encontrado na documentação dessa classe no MSDN. No exemplo, as mensagens trafegadas são armazenadas num arquivo de log.

7 de julho de 2011

Design Patterns com Delphi: State - Parte II

No último post, apresentei o conceito do Design Pattern comportamental State. Como exemplo de situação onde o padrão é aplicável, criei um controle simples de conta corrente no qual a conta tem comportamentos diferentes, reagindo ao nível do saldo que ela atualmente contém.

Para ficar mais fácil visualizar, reproduzo abaixo o diagrama de classes que representa uma solução para a situação do exemplo.
Diagrama UML para o padrão State

O modo como uma conta tem que se comportar é ditado por uma classe abstrata simples, chamada de TWEstadoConta no diagrama. Essa classe é apenas uma interface que introduz as funções e propriedades esperadas para a operação básica da conta em si. Os estados reais podem, então, ser implementados como heranças dessa interface, providenciando a diferenciação necessária.
TWEstadoConta = class
protected
_Saldo : Double;
_PorcRendim : Double;
_LimInf, _LimSup : Double;
_Conta : TWConta; { ... }

public
{ ... }
procedure Depositar (AValor : Double);virtual;abstract;
procedure Sacar (AValor : Double);virtual;abstract;
procedure AplicarRendimento;virtual;abstract;
end;

TWContaComum = class(TWEstadoConta)
protected
_TaxaServico : Double;
Constructor Create (AConta : TWConta);override;
public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;


TWContaDiferenciada = class(TWContaComum)
protected
_TaxaJuros : Double;
Constructor Create (AConta : TWConta);override;

public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;

TWContaOuro = class(TWEstadoConta)
protected
Constructor Create (AConta : TWConta);override;

public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;

As operações para depósito, saque e a que aplica rendimentos ao saldo da conta são abstratas na interface mas implementadas pela classe de cada estado possível. Veja, por exemplo, as diferenças da operação de saque:
procedure TWContaComum.Sacar (AValor : Double);
var lValorReal : Double;
begin
{ Conta comum, aplica a taxa de serviço }
lValorReal := AValor + _TaxaServico;
_Saldo := _Saldo - lValorReal;
end;

procedure TWContaDiferenciada.Sacar (AValor : Double);
var lJuros, lValorReal : Double;
begin
{ Na Conta diferenciada, aplica taxa de serviço, critica se extrapolar o limite de crédito e aplica uma taxa de juros se a conta ficar negativa }
lValorReal := AValor + _TaxaServico;
if (_Saldo - lValorReal) < _LimInf then
raise Exception.Create('Saque não pode ser efetivado. Conta sem saldo.');

lJuros := 0.0;
if (_Saldo - lValorReal) < 0.0 then
lJuros := (lValorReal - _Saldo) * _TaxaJuros / 100.0;

_Saldo := _Saldo - lValorReal - lJuros;
end;

procedure TWContaOuro.Sacar (AValor : Double);
begin
{ Conta Ouro, não se preocupa com nada - apenas saca. }
_Saldo := _Saldo - AValor;
end;

Os construtores das classes que representam estados têm que alimentar os valores das propriedades internas, caracterizando os respectivos estados. É isso que permitirá à classe da conta decidir qual estado deve estar ativo num determinado momento:
Constructor TWContaComum.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 1.0; { Rendimento padrão : 1%}
_TaxaServico := 5.00; { Saques são cobrados }
{ Valores de saldo que delimitam um estado }
_LimInf := 100.00;
_LimSup := 999.99;
end;

Constructor TWContaDiferenciada.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 0.0; { Sem rendimento }
_TaxaJuros := 1.00; { Taxa de juros sobre saques qdo a conta fica sem saldo }
_TaxaServico := 10.00; { Saques são cobrados }

{ Valores de saldo que delimitam um estado }
_LimInf := -100.00;
_LimSup := 99.99;
end;

Constructor TWContaOuro.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 2.00; { Rendimento Ouro : 2% }
{ Valores de saldo que delimitam um estado }
_LimInf := 1000.00;
_LimSup := 99999.99;
end;

A classe da Conta, então, deve guardar uma referência do estado atual para poder realizar as operações solicitadas. Portanto, ela age como uma ponte, transferindo para a classe de estado a responsabilidade de executar efetivamente a operação.
TWConta = class
private
_Estado : TWEstadoConta;
{ ... }
public
{ ... }
procedure Depositar (AValor : Double);
procedure Sacar (AValor : Double);
procedure AplicarRendimento;
{ ... }
function ObtemSaldo : double;
function ObtemEstado : TWEstadoConta;
end;
Para que a implementação desse padrão funcione, é preciso trocar dinamicamente a instância de classe que representa o estado atual. E quando é necessário efetuar essa troca da instância de estado ? O bom senso diz que isso deve acontecer após qualquer operação que possa afetar o estado monitorado. No nosso caso, isso significa atuar em todas as operações que modifiquem o saldo da conta.
procedure TWConta.VerificaStatus;
var lNovoEstado : TWEstadoConta;
begin
{ ... }
{ Testa as variações de estado }
lNovoEstado := Nil;
if (_Estado._Saldo < _Estado._LimInf) then
begin
{ Rebaixa de COMUM para DIFERENCIADA }
if (_Estado.InheritsFrom (TWContaComum)) then
lNovoEstado := TWContaDiferenciada.Create (Self);

{ Rebaixa de OURO para COMUM }
if (_Estado.InheritsFrom (TWContaOuro)) then
lNovoEstado := TWContaComum.Create (Self);
end;

if (_Estado._Saldo > _Estado._LimSup) then
begin
{ Eleva de COMUM para OURO }
if (_Estado.InheritsFrom (TWContaComum)) then
lNovoEstado := TWContaOuro.Create (Self);

{ Rebaixa de OURO para COMUM }
if (_Estado.InheritsFrom (TWContaOuro)) then
lNovoEstado := TWContaComum.Create (Self);
end;

{ Efetiva a troca de estado }
if (lNovoEstado <> Nil) then
SetEstado (lNovoEstado);
end;

procedure TWConta.Depositar (AValor : Double);
begin
_Estado.Depositar(Avalor);
VerificaStatus ();
end;

procedure TWConta.Sacar (AValor : Double); begin
_Estado.Sacar (AValor);
VerificaStatus ();
end;

procedure TWConta.AplicarRendimento;
begin
_Estado.AplicarRendimento ();
VerificaStatus ();
end;

Pelo quadro acima, vemos que quem faz a troca é a própria conta pois as classes de estado não conhecem o fluxo de mudança, isto é, nenhuma delas sabe qual é o estado seguinte para o qual a conta pode ser lançada. Isso facilita a inclusão de novos estados já que apenas um ponto precisa ser modificado para levá-lo em consideração.

A recuperação de uma instância da classe de estado pode ser modelada para usar o Design Pattern criacional Factory ou ainda o Singleton.

Usar uma Factory como fizemos em VerificaStatus pode acarretar problemas de desempenho se as trocas de estado ocorrerem com muita frequência. O mesmo é válido se for grande o volume de dados que caracterizam os estados. Neste caso, optar por um Singleton é recomendável, sem esquecer de que ele deve se restringir ao escopo da classe. Isto é, cada instancia da classe deve ter seu Singleton particular; senão, o estado de uma instância poderá se confundir com o de outras que porventura existam ao mesmo tempo.

O download do projeto Delphi com esse exemplo pode ser feito a partir desse link.