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