sábado, 6 de agosto de 2011

Strategy - Separando os comportamentos - Parte Final

No último artigo falei sobre o padrão de projeto Strategy e expus um diagrama de como utilizar o strategy para exportação de dados. Hoje vou mostrar como podemos implementar o padrão seguindo a modelagem do diagrama.

Primeiro, vamos a declaração da interface que define a API de exportação e o enumerator com os formatos de saída suportados.

type
  TExporterType = (etText, etCSV, etXML);

  IExporterStrategy = interface
  ['{AA69E6F1-389F-4DC3-AC14-7FA7A749B183}']
    function GetFileName: String;
    procedure SetFileName(const Value: String);

    property FileName: String read GetFileName write SetFileName;

    procedure ExportToFile(const Value: String);
  end;



Agora vamos a implementação da classe responsável pela exportação em arquivo texto.

type
  TTextExporterStrategy = class(TInterfacedObject, IExporterStrategy)
  private
    FFileName: String;
  public
    procedure ExportToFile(const Value: String);
    
    function GetFileName: String;
    procedure SetFileName(const Value: String);
  end;
  
implementation
  
  /* Getter e Setter omitidos*/
  
procedure TTextExporterStrategy.ExportToFile(const Value: String);
var
  vFile: TextFile;
begin
  if (FFileName = EmptyStr) then
    raise EFileIsNotDefined.Create;

  AssignFile(vFile, FFileName);
  try
    Rewrite(vFile);

    WriteLn(vFile, Value);
  finally
    CloseFile(vFile);
  end;
end;  

Também implementei a exportação em formato CSV. Assumi que os espaços em branco seriam substituídos pelo separador.

type
  TCSVExporterStrategy = class(TInterfacedObject, IExporterStrategy)
  private
    FFileName: String;
  public
    procedure ExportToFile(const Value: String);
    
    function GetFileName: String;
    procedure SetFileName(const Value: String);
  end;
  
implementation
  
  /* Getter e Setter omitidos*/  
  
implementation

{ TCSVExporterStrategy }

procedure TCSVExporterStrategy.ExportToFile(const Value: String);
var
  vFile: TextFile;
  vCSVLine: TStringList;
  i: Integer;
begin
  if (FFileName = EmptyStr) then
    raise EFileIsNotDefined.Create;

  AssignFile(vFile, FFileName);
  try
    Rewrite(vFile);

    vCSVLine := TStringList.Create;
    try
      ExtractStrings([' '], [' '], PChar(Value), vCSVLine);

      for i := 0 to vCSVLine.Count-1 do
      begin
        if i = vCSVLine.Count-1 then
         WriteLn(vFile, vCSVLine[i])
        else
         Write(vFile, vCSVLine[i] + ';');
      end;
    finally
      vCSVLine.Free;
    end;
  finally
    CloseFile(vFile);
  end;
end;  

Agora que já temos duas classes concretas que implementam a exportação, vamos criar a factory responsável por instanciar as classes adequadas.

TExporterFactoryStrategy = class
  private
  public
    class function NewExporter(const ExporterType: TExporterType): IExporterStrategy;
  end;
  
implementation

{ TExporterFactoryStrategy }
class function TExporterFactoryStrategy.NewExporter(const ExporterType: TExporterType): IExporterStrategy;
begin
  case ExporterType of
    etText: Result := TTextExporterStrategy.Create;
    etCSV : Result := TCSVExporterStrategy.Create;
  else
    raise EStrategyNotImplemented.Create(ExporterType);
  end;
end;  

{ EStrategyNotImplemented }
constructor EStrategyNotImplemented.Create(AExporterType: TExporterType);
begin
  inherited CreateFmt('Strategy não implementado para o tipo "%s"',[GetEnumName(TypeInfo(TExporterType), Ord(AExporterType))]);
end;

Caso alguém peça a factory para instanciar um objecto para exportar para XML, uma exceção do tipo EStrategyNotImplemented será lançada.

Finalmente, vamos a classe TExporterContext, que representa a classe de negócio que faria a exportação dos dados na vida real, onde teria suas queries, cálculos e etc. O mais legal é que essa classe tem um nível de acoplamento extremamente baixo com o método de exportação, ela depende apenas da interface.

type
  TExporterContext = class
  private
    FExporterStrategy: IExporterStrategy;
  public
    constructor Create(const AExporterStrategyStrategy: IExporterStrategy);

    procedure MakeExport(const AOutputFile: String);
  end;

implementation

{ TExporterContext }

constructor TExporterContext.Create(const AExporterStrategyStrategy: IExporterStrategy);
begin
  FExporterStrategy := AExporterStrategyStrategy;
end;

procedure TExporterContext.MakeExport(const AOutputFile: String);
begin
  FExporterStrategy.FileName := AOutputFile;

  FExporterStrategy.ExportToFile(ClassName);
end;  

E não poderiam faltar os Testes, como sempre, utilizando o DUnit. Se você ainda não é familiarizado com testes unitários, leia os posts: Usando DUnit para implementar testes unitários e Usando DUnit para implementar testes unitários - 2

type
  TTestTextExporter = class(TTestCase)
  private
  published
    procedure DeveLancarExcecaoQuandoFileNameNaoForDefinido;
    procedure DeveExportarParaArquivoNoFormatoTexto;
  end;

implementation

{ TTestTextExporter }

procedure TTestTextExporter.DeveExportarParaArquivoNoFormatoTexto;
var
  vTextExporter: IExporterStrategy;
  vValue,
  vFileName,
  vLine: String;
  vTextFile: TextFile;
begin
  vFileName := ExtractFilePath(ParamStr(0)) + 'textexporter.txt';

  vValue := 'Valor exportado';

  vTextExporter := TTextExporterStrategy.Create;
  vTextExporter.FileName := vFileName;
  vTextExporter.ExportToFile(vValue);

  CheckTrue(FileExists(vFileName), 'Arquivo não foi gerado.');
  
  AssignFile(vTextFile, vFileName);
  try
    Reset(vTextFile);

    Readln(vTextFile, vLine);

    CheckEqualsString(vValue, vLine);
  finally
    CloseFile(vTextFile);
  end;

  DeleteFile(vFileName);
end;

procedure TTestTextExporter.DeveLancarExcecaoQuandoFileNameNaoForDefinido;
begin
  ExpectedException := EFileIsNotDefined;
  TTextExporterStrategy.Create.ExportToFile('');
end;

initialization
  RegisterTest('Text Exporter',  TTestTextExporter.Suite);

Agora vou fazer um teste unitário e de integração na classe de contexto (TExporterContext). O teste de integração é o primeiro - método ExportaComoTexto - onde a classe de contexto recebe uma instância para o formato etText através da factory. TExporterFactoryStrategy.

TTextExporterContext = class(TTestCase)
  private
  published
    procedure ExportaComoTexto;
    procedure DeveInvocarOStrategy;
  end;

implementation

{ TTextExporterContext }

procedure TTextExporterContext.ExportaComoTexto;
var
  vExporter: TExporterContext;
  vFileName,
  vLine: String;
  vTextFile: TextFile;
begin
  vFileName := ExtractFilePath(ParamStr(0)) + 'textexporter.txt';

  vExporter := TExporterContext.Create(TExporterFactoryStrategy.NewExporter(etText));
  vExporter.MakeExport(vFileName);

  CheckTrue(FileExists(vFileName), 'Arquivo não foi gerado.');
  
  AssignFile(vTextFile, vFileName);
  try
    Reset(vTextFile);

    Readln(vTextFile, vLine);

    CheckEqualsString(vExporter.ClassName, vLine);
  finally
    CloseFile(vTextFile);
  end;

  DeleteFile(vFileName);
end;  

Esse teste é necessário para ver o funcionamento como um todo, mais tem um ponto a mais de falha, que é a classe de exportação, pois, a mesma pode gerar erros - de I/O, por exemplo - que não indicam necessariamente que a classe de contexto está com problemas,portanto, para testar a classe de forma realmente unitária, precisamos ter controle sobre a estratégia de exportação,e para isso, utilizamos Mock Objects. Para delphi, temos as opções: Pascal Mock, Simple Mock e Ultra Basic MockObjects. Se alguém conhece mais algum, deixe um comentário. Eu optei por utilizar o Pascal Mock.

Não vou entrar em detalhes sobre Mocks nesse artigo, isso fica para uma próxima, mais segue abaixo como ficou a classe Mock e o teste.

type
  TMockExporterStrategy = class(TMock, IExporterStrategy)
  public
    procedure ExportToFile(const Value: String);
    function GetFileName: String;
    procedure SetFileName(const Value: String);
  end; 
  
implementation
{ TMockExporterStrategy }

procedure TMockExporterStrategy.ExportToFile(const Value: String);
begin 
  //Registra que o método foi chamado e com qual valor para o parâmetro
  AddCall('ExportToFile').WithParams([Value]);
end;

function TMockExporterStrategy.GetFileName: String;
begin
  //Não é utilizado no teste
  Result := EmptyStr;
end;

procedure TMockExporterStrategy.SetFileName(const Value: String);
begin
  //Registra que o método foi chamado e com qual valor para o parâmetro
  AddCall('SetFileName').WithParams([Value]);
end;
  
procedure TTextExporterContext.DeveInvocarOStrategy;
const
  FileName = 'C:\Exporter\filename.txt';
var
  vMock: TMockExporterStrategy;
  vContext: TExporterContext;
begin
  //Instância o Mock
  vMock := TMockExporterStrategy.Create;
  try
    //Indica que deve ser invocado o método SetFileName uma vez com o valor FileName definido na constante.
    vMock.Expects('SetFileName', 1).WithParams([FileName]);
 //Indica que deve ser invocado o método ExportToFile uma vez.
    vMock.Expects('ExportToFile', 1);

 //Instancia a classe de contexto passando o Mock como argumento.
    vContext := TExporterContext.Create(vMock);
    try
      vContext.MakeExport(FileName);
    finally
      vContext.Free;
    end;

 //O mock verifica se os métodos configurados como esperados foram realmente invocados e com a quantidade esperada.
    vMock.Verify('Não invocou o método "SetFileName"');
  finally
    vMock.Free;
  end;
end;

Com isso encerramos a séria sobre Strategy. Como de costume, o código fonte desse artigo e dos anteriores está disponível na minha conta do bitbucket.

Nenhum comentário:

Postar um comentário