O padrão MVP tem a finalidade de separar a camada de apresentação das camadas de dados e regras de negócio. Neste artigo vamos apresentar como usar o MVP em uma aplicação Delphi.

O MPV é divido em três partes bem distintas e com responsabilidades específicas, são elas o Model, View e Presenter, conforme figura 1.

MVP
Figura 1. MVP

Em uma descrição simples podemos definir como:

  • View - em nosso caso é o formulário que exibirá os dados, não contém regra alguma do negócio a não ser disparar eventos que notificam mudança de estado dos dados que ele exibe e processamento próprio dele, como por exemplo código para fechar o formulário. Um objeto view implementa uma interface que expõe campos e eventos que o presenter necessita.
  • Model - São os objetos que serão manipulados. Um objeto Model implementa uma interface que expõe os campos que o presenter irá atualizar quando sofrerem alteração na view.
  • Presenter - É a ligação entre View e Model, possui papel de mediador entre eles. Ele é encarregado de atualizar o view quando o model é alterado e de sincronizar o model em relação ao view.

Demonstração

Nosso aplicativo de exemplo será simples, porém irá mostrar como iniciar o desenvolvimento de um aplicativo utilizando o padrão MVPe fazendo uso de interfaces.

Constará apenas de uma tela de Login que será exibida antes do formulário principal da aplicação, validando o usuário. Inicie uma nova aplicação Win32 no Delphi 2006 ou Delphi 7.

O Model

Crie uma nova unit e a adicione ao projeto, salvando-a como UsuarioIntf.pas e declare a seguinte interface da listagem 1.


  IUsuario = interface
  ['{EC207CEB-2C68-4EC3-92D6-0748E8B64882}']
    procedure SetSenha(val : string);
    function GetSenha : string;
    procedure SetNome(val : string);
    function GetNome : string;
    function Login: boolean;
    property Nome :  string read GetNome write SetNome;
    property Senha :  string read GetSenha write SetSenha;
end;
Listagem 1. Interface para usuário

Vamos agora criar a classe que implementa nossa interface. Adicione outra unit ao projeto salvando-a como Usuario.pas e definindo a classe como listagem 2.

 
uses UsuarioIntf;

type
  TUsuario = class(TInterfacedObject, IUsuario)
  private
    FNome:String;
    FSenha:String;
  protected
    procedure SetNome(val:string);
    function GetSenha: String;
    function GetNome: String;
    procedure SetSenha(val:string);
  public
    property Senha : String read GetSenha write SetSenha;
    property Nome : String read GetNome write SetNome;
    function Login:boolean;
end;
Listagem 2. Classe TUsuario

Você pode implementar os Gets e Sets das propriedades, neles não tem segredo algum, na listagem 3 temos o método Login, que aqui contém uma senha fixa apenas para simplificar o exemplo, mas o correto seria buscar essa senha em algum banco de dados.


function TUsuario.Login: boolean;
begin
  result := (self.FNome = 'ClubeDelphi') and (self.FSenha = 'MVP');
end;
Listagem 3. Método Login

Interfaces da View e Presenter

Como já mencionado, a View é a representação visual de nossos objetos, é nela que o usuário vai interagir. Observe agora a figura 1, veja que uma view possui referência para um presenter e nunca diretamente para um model.

Em uma nova unit adicione crie a interface IUsuarioView, conforme listagem 4 e salve-a como UsuarioMVPIntf.


uses UsuarioIntf,
     Controls;
type
IUsuarioPresenter = interface;

IUsuarioView = interface 
  ['{10EC2701-D328-45E9-AC26-9889C7F3ECA3}'] 
  procedure SetSenha(val : string); 
  function GetSenha : string; 
  procedure SetNome(val : string); 
  function GetNome : string; 
  procedure SetPresenter(const val : IUsuarioPresenter);
  function GetPresenter: IUsuarioPresenter; 
  property Nome: string read GetNome write SetNome; 
  property Senha: string read GetSenha write SetSenha; 
  property Presenter: IUsuarioPresenter read GetPresenter write SetPresenter; 
  function ShowView: TModalResult;
end;  
Listagem 4. Interface da View

É preciso também a interface do presenter, que também contém uma referência à View. Veja novamente a figura 1. Nela vemos que é o presenter que atutaliza o Model, dessa forma temos aqui uma referência ao Model. Veja como ficou a interface de nosso presenter na listagem 5.

 
IUsuarioPresenter = interface
  ['{56BBCC2F-E0AA-40CA-B0C6-95A499E80410}']
  procedure SetModel(const Value: IUsuario);
  procedure SetView(const Value: IUsuarioView);
  function GetModel: IUsuario;
  function GetView: IUsuarioView;
  function Login: boolean;
  procedure InitiView;
  property Model: IUsuario read GetModel write SetModel;
  property View: IUsuarioView read GetView write SetView;
end;
Listagem 5. Interface do Presenter

Veja a referência que o presenter faz ao model. Ele aponta para a interface e não para a classe concreta. Em ponto algum do MVP lidamos com o model concreto, as interfaces aqui são utilizados como uma espécie de contrato que todas as partes envolvidas devem utilizar, dessa forma conseguimos separar, encapsular, a classe TUsuario de tal forma que tanto a View como o Presenter não sabem quem a está implementando, porque enxergam apenas o que o “contrato” permite. Esse conceito de ter referências para interfaces é uma das bases de inversão de controle, que fica para um artigo futuro.

Implementando o Presenter

O presenter, como já dito, é o mediador entre a view e o model. Na listagem 6 vemos sua implementação, atente para o método Login.

 
uses UsuarioIntf,
     UsuarioMVPIntF;

type
  TLoginPresenter = class(TInterfacedObject,IUsuarioPresenter)
  private
    FModel: IUsuario;
    FView: IUsuarioView;
  protected
    procedure SetModel(const Value: IUsuario);
    procedure SetView(const Value: IUsuarioView);
    function GetModel: IUsuario;
    function GetView: IUsuarioView;

  public
    property Model: IUsuario read FModel write SetModel;
    property View: IUsuarioView read FView write SetView;
    function Login: boolean;
    procedure InitiView;
end;

(...)

function TLoginPresenter.Login: boolean;
begin
  model.Nome := View.Nome;
  model.Senha := View.Senha;
  result := Model.Login;
end;
Listagem 6. classe TLoginPresenter

O presenter, no método Login, obtém da view os valores que precisam ser atualizados no model. O presenter não sabe detalhes de como são gerados os valores, ele apenas os obtém através das propriedades e passa para o model, conforme figura 1.

Implementando a View

Adicione um novo Formulário e o salve sua unit como LoginU.pas. Ajuste sua aparência como como na figura 2.

Tela de Login
Figura 2. Tela de Login

Esse formulário irá implementar nossa interface IUsuarioView. Na listagem 7 temos o código.

 
type
  TLoginF = class(TForm, IUsuarioView)
    Label1: TLabel;
    EditUsuario: TEdit;
    Label2: TLabel;
    EditSenha: TEdit;
    btLogin: TButton;
    btCancelar: TButton;
    procedure btCancelarClick(Sender: TObject);
    procedure btLoginClick(Sender: TObject);
  private
    Fpresenter: Pointer;
    FNome: string;
    FSenha: string;
    procedure Setpresenter(const Val: IUsuarioPresenter);
    procedure SetSenha(val : string);
    function GetSenha : string;
    procedure SetNome(val : string);
    function GetNome : string;
    function Getpresenter: IUsuarioPresenter;
  public
    { Public declarations }
    property presenter: IUsuarioPresenter read Getpresenter write Setpresenter;
    property Nome :  string read GetNome write SetNome;
    property Senha : string read GetSenha write SetSenha;
    function ShowView: TModalResult;
  end;

implementation

{$R *.dfm}

procedure TLoginF.btCancelarClick(Sender: TObject);
begin
  ModalResult := mrCancel;
end;

procedure TLoginF.btLoginClick(Sender: TObject);
begin
  if Presenter.Login then
    ModalResult := mrOk
  else
    ShowMessage('Usuário inválido');
end;

function TLoginF.GetNome: string;
begin
  result := EditUsuario.Text;
end;

function TLoginF.Getpresenter: IUsuarioPresenter;
begin
  result := IUsuarioPresenter(FPresenter);
end;

function TLoginF.GetSenha: string;
begin
  result := EditSenha.Text;
end;

procedure TLoginF.SetNome(val: string);
begin
  Self.FNome := val;
  EditUsuario.Text := val;
end;

procedure TLoginF.Setpresenter(const Val: IUsuarioPresenter);
begin
  FPresenter := Pointer(Val);
end;

procedure TLoginF.SetSenha(val: string);
begin
  Self.Senha := val;
  EditSenha.Text := val;
end;

function TLoginF.ShowView: TModalResult;
begin
  result:=   self.ShowModal;
end;
Listagem 7. View implementada por TLoginF

Na seção public temos a declaração das propriedades e métodos que a interface IUsuarioView nos obriga a realizar. Observe também que nos Gets e Sets das propriedades Nome e Senha não temos referencia ao model nem ao presenter, apenas passamos os valores paras propriedades.

Agora você pode se perguntar, e como faço pra validar o usuário? Veja o método btLoginClick. Nele chamamos o método Login do presenter e não do model, ou seja, nosso formulário de login não sabe como funciona o Login, sua implementação está encapsulada.

Executando

No evento onShow do formulário principal vamos configurar o ambiente do MVP, ou seja, definir o presenter, o model e a view e executá-la., confirma a listagem 8.

 
procedure TPrincF.FormShow(Sender: TObject);
var
  lview : IUsuarioView;
  lusuario: IUsuario;
  lpresenter: IUsuarioPresenter;
begin
  lview := TLoginF.Create(nil);
  lpresenter := TLoginPresenter.Create;
  lusuario := TUsuario.create;

  lpresenter.Model := lusuario;
  lpresenter.View := lview;
  if lview.ShowView = mrCancel then
  begin
    Close;
  end;
end;
Listagem 8. OnShow

Execute a aplicação e você que tudo funciona perfeitamente. Na figura 3 temos o diagrama de seqüência da aplicação.

Diagrama de seqüência
Figura 3. Diagrama de seqüência

Conclusão

Agora você deve estar pensando... “é muito código para um login!”. Sim, concordo com você se estiver vendo apenas pelo login. Digo que não dá para demonstrar todas as vantagens de se separar a camada de apresentação das regras de negócio de forma eficiente apenas com uma tela de login. Porém posso apontar aqui um fato muito importante, na view não temos referência ao model, a regra de negócio não está na view, assim se quisermos mudar a tela de login, por exemplo substituindo os botões comuns por ou outro tipo de botão ou qualquer coisa diferente, não alteraria a funcionalidade da janela. Ao programar no modelo “convencional”, acabamos quase sempre por colocar as regras dentro das janelas, e se precisarmos mudar a janela, podemos quebrar a regra, gerando um erro no programa, ou então fazemos nossas regras depender muito das visualizações (formulários e eventos de formulários) que dificultam qualquer alteração. A boa prática que mostrar é que precisar separar as regras das interfaces de usuário, e colocar menos código possível nessas interfaces, para torná-las independentes.

Talvez mesmo assim, você desanime da quantidade de código que foi preciso, mas não se preocupe, existe hoje o framework Infra que possui a implementação de um MVP completo para os componentes VCL do Delphi, reduzindo muito o código necessário.

Abraços e até a próxima.