DataModule é apenas um recipiente, e só isso


Não utilize um DataModule como uma Classe de Negócio, implementando métodos públicos e utilizando-o como um Singleton. Isso é errado. Você deixará seu sistema acoplado a esta única implementação e não será possível testá-lo da maneira correta.

Um DataModule não deve conter Regras de Negócio.

Container

Utilize DataModules apenas com recipientes de componentes e nada mais.

Um DataModule deverá conter, na maioria das vezes, componentes de relatório e queries, todos relacionados a um contexto bem específico e delimitado.

Quanto menos componentes adicionar, melhor, mais simples e com menos gasto de memória do computador.

Componentes de relatório foram concebidos para utilizar dados tabulados. Aqui a Orientação a Objetos não ajuda muito. A maneira mais eficiente para gerar um relatório é trabalhar com queries, retornando conjuntos de dados relacionados entre si.

Vou mostrar um exemplo real – sistema em produção – que demostra como eu utilizo DataModules.

Implementação de um Relatório

Para a implementação de um relatório, a primeira coisa a fazer é definirmos uma abstração para a definição de um Relatório, ou seja, uma Interface.

Para a Interface apenas 3 métodos são necessários:

  1. Show: Para pré-visualizar o relatório na tela.
  2. Print: Para mostrar somente o PrintDialog para o usuário para impressão direta.
  3. PDF: Para gerar um PDF no disco.
type
  IReport = interface
  ['{29414A64-69BA-40A1-970B-2CD61C633508}']
    function Show: IReport;
    function Print: IReport;
    function PDF(const FileName: string): IReport;
  end;

O sistema que utiliza esse código foi codificado em Delphi 7, utilizando ADO e ReportBuilder®(RB), no entanto essa é uma Interface genérica o bastante para ser utilizado por qualquer versão do Delphi/Lazarus, com a possibilidade de implementar e utilizar qualquer gerador de relatório.

O próximo passo é codificar uma implementação de IReport para o RB.

Abaixo contém apenas a Interface da classe. Não é necessária a implementação para o entendimento dos conceitos que quero mostrar.

type
  TRBuilderReport = class(TInterfacedObject, IReport)
  private
    FReport: TppReport;
  public
    constructor Create(Report: TppReport);
    class function New(Report: TppReport): IReport;
    function Show: IReport;
    function Print: IReport;
    function PDF(const FileName: string): IReport;
  end;

O motivo de termos um Método New já foi explicado anteriormente.

Bem, nesse momento eu tenho uma Interface para qualquer tipo de relatório e uma implementação genérica para utilizar o design do RB.

Repare que na implementação do RB eu tenho que injetar a dependência Report: TppReport. A instância desse componente estará num DataModule.

Aqui está o design da classe TDividaAtivaMedicoModule, que é o DataModule que vou utilizar nesse exemplo.

DataModule

Todo o design dos relatórios, assim como o SQL é propriedade privada e não será mostrado aqui.

Garanto que neste DataModule não há métodos públicos ou privados. Apenas eventos dos componentes do relatório são implementados, pois essa é a maneira de customizar a impressão do relatório.

Para o contexto do problema, e de acordo com as regras de negócio, são necessários 3 relatórios como pode-se ver na imagem, mais irei demostrar a implementação de apenas um deles.

Um DataModule é um Singleton

Quando você instancia um DataModule ele se torna um Singleton global para todo projeto. Não importa se você está utilizando a variável global que a IDE adiciona automaticamente ou qualquer outra variável. Isso quer dizer que se eu tenho 2 Forms que tem componentes “apontando” para componentes num DataModule, ambos os Forms estarão utilizando a mesma instância do DataModule, compartilhando os mesmos componentes.

Eu não quero isso.

A maneira para fazer uma instância do DataModule não ser global é chamar RemoveDataModule no seu construtor:

procedure TDataModule1.DataModuleCreate(Sender: TComponent);
begin
  inherited;
  RemoveDataModule(Self);
end;

Por que fazer isso?

Bem, utilizar variáveis globais nunca é recomendado. A abordagem proposta aqui é mais thread-safe. Poderia apresentar o relatório ao usuário e gerar um PDF ao mesmo tempo, utilizando thread. Também poderia gerar relatórios diferentes ao mesmo tempo passando parâmetros diferentes às queries.

A Classe de Negócio

Instanciar um DataModule, atribuir parâmetros às queries, chamar o relatório… o passo-a-passo é mais ou menos assim, certo? No entanto eu espero que você não tenha pensado em fazer isso diretamente no código de um Form, por exemplo.

Precisamos ter uma – ou muitas outras – Classe de Negócio para encapsular e implementar o relatório.

Para o único relatório que iremos implementar – AvisosReport – temos uma classe chamada TDividaAtivaMedicoAvisosReport.

type
  TDividaAtivaMedicoAvisosReport = class(TInterfacedObject, IReport)
  private
    FModule: TDividaAtivaMedicoModule;
    FReport: IReport;
  public
    constructor Create(NotificacaoId: Integer);
    class function New(NotificacaoId: Integer): IReport;
    destructor Destroy; override;
    function Show: IReport;
    function Print: IReport;
    function PDF(const FileName: string): IReport;
  end;

implementation  
  
{ TDividaAtivaMedicoAvisosReport }

constructor TDividaAtivaMedicoAvisosReport.Create(NotificacaoId: Integer);
begin
  inherited Create;
  FModule := TDividaAtivaMedicoModule.Create(nil);
  FReport := TRBuilderReport.Create(FModule.AvisosReport);
  with FModule.NotificacoesQuery.Parameters do
  begin
    ParamByName('notificacao_id').Value := NotificacaoId;
    ParamByName('fase').Value := CDividaAtivaFases.AvisoId;
  end;
end;

class function TDividaAtivaMedicoAvisosReport.New(NotificacaoId: Integer): IReport;
begin
  Result := Create(NotificacaoId);
end;

destructor TDividaAtivaMedicoAvisosReport.Destroy;
begin
  FModule.Free;
  inherited;
end;

function TDividaAtivaMedicoAvisosReport.Show: IReport;
begin
  Result := FReport.Show;
end;

function TDividaAtivaMedicoAvisosReport.Print: IReport;
begin
  Result := FReport.Print;
end;

function TDividaAtivaMedicoAvisosReport.PDF(const FileName: string): IReport;
begin
  Result := FReport.PDF(FileName);
end;

Essa implementação é real e está em produção. O código poderia ser melhorado, é claro, mas isso é irrelevante agora.

Essa classe, a TDividaAtivaMedicoAvisosReport, funciona como um decorador para IReport, e tem uma implementação implícita de TRBuilderReport. Como eu disse, pode melhorar. Enfim.

O mais importante são essas 2 linhas:

  FModule := TDividaAtivaMedicoModule.Create(nil);
  FReport := TRBuilderReport.Create(FModule.AvisosReport);

Um DataModule é instanciado em FModule. Esse DataModule é retirado do “estado global” utilizando RemoveDataModule em seu construtor.

Então temos uma instância de TRBuilderReport em FReport que é inicializado com um relatório específico: FModule.AvisosReport.

A implementação da classe, a partir daí, apenas repassa as solicitações para FReport utilizando os métodos da Interface IReport.

Dessa maneira o restante do código não precisa se preocupar em inicializar variáveis, campos calculados, cores, abrir queries, etc pois tudo que o sistema precisa é utilizar uma instância de TDividaAtivaMedicoAvisosReport.

Essa classe poderia ter sua implementação alterada, no futuro, sem termos que reescrever código que a utilize, pois o relatório, dados e parâmetros estão encapsulados dentro dela.

Conclusão

Quando o sistema precisa de emitir um relatório, a Interface IReport é utilizada e Classes de Negócio são implementadas para encapsular cada relatório. O sistema não sabe qual gerador de relatórios está sendo utilizado, apenas as Classes de Negócio sabem. É thread-safe, simples, extensível e de fácil manutenção.

Não é um método perfeito, mas demostra o uso eficiente de um componente RAD sendo utilizado num contexto mais Orientado a Objetos.

Para mim esse é o uso correto de um DataModule, ou seja, ser apenas um recipiente de componentes e nada mais.

Até logo.

Posts Relacionados

  • Memória Segura Utilizando Instâncias de Interfaces

  • Classes Mutáveis vs Objetos Imutáveis

  • Implementando Interfaces Utilizando Diferente Assinaturas de Métodos

  • Usando Paths ao invés de Diretivas de Compilação

  • Trabalhando com Exceções em Requisições HTTP

  • Tipo object Continua Vivo

  • Array de Objetos

  • Variáveis Locais Deveriam ter Nomes Curtos

  • Como Dividir e Organizar o Código em Formulários com Muitos Widgets

  • Pascal Deveria ser Modernizado?