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