MVP – Passive View

O MVP é um padrão para a construção de aplicações que interagem com o usuários (User Interface) que tem dois objetivos principais:

  1. Separar as responsabilidades na camada de apresentação, ou seja, o que é da tela e o que é da lógica de negócio do aplicativo.
  2. Facilitar os testes,  permitindo que os mesmos sejam automatizados.

O pattern MVP pode ser separado em duas categorias: Passive View e Supervising Controller. A diferença entre os dois é o grau de testabilidade que cada um provê. Hoje iremos ver alguns detalhes da primeira categoria, com foco na questão de separação de responsabilidades.

Diagrama de classes MVP

A idéia aqui é separarmos o que o que é da apresentação e o que é da lógica de acesso aos objetos do domínio de negócios. O diagrama acima mostra como temos essa separação entre as classes que compõe a estrutura básica do MVP Passive View. Do lado da camada de apresentação, temos a classe View, que no final das contas será uma tela WPF ou uma página ASP.NET. Do lado da lógica de negócios, temos a classe Model, que representa os dados que serão exibidos pela aplicação. Na intersecção das duas camadas está a interface IView, que veremos em breve deve ser implementada por todas as views (telas ou páginas) e a classe Presenter. A classe Presenter funciona como um meio-de-campo entre a camada de apresentação e a lógica de negócios.

Antes de prosseguirmos com um exemplo, vamos notar como é o relacionamento entre no diagrama acima. A classe Presenter é referenciada diretamente por View, mas Presenter não conhece View. Na verdade, ela conhece apenas a interface IView que é implementada por View, e é dessa forma que a conversa entre as duas ocorre. Por isso que este pattern é conhecido como Passive View, pois a view é manipulada, indiretamente, pela classe Presenter. Veja que em nenhum momento View acessa diretamente Model, o que provê a separação entre as camadas.

Vamos comentar o exemplo criado para ilustrar. Ele é uma tela fictícia de consulta de preços de ações (Bolsa de Valores), onde o usuário irá informar o código da ação (conhecido por “ticker”), e a aplicação retornará o valor atual da cotação. É claro que, como um exemplo, vou gerar estes valores de forma aleatória, para simplificar. Abaixo está como eu organizei a solution. Temos quatro projetos aqui:

  • MVP.Domain – É o projeto que contém os objetos de negócio (Model). Numa solução distribuída do “mundo real”, estas classes ficariam hospedadas em um servidor de componentes e poderiam, por exemplo, serem expostas através de serviços WCF.
  • MVP.Presentation – Este projeto faz parte da camada de apresentação / serviços e contém a classe que fará o meio-de-campo entre a interface e a chamada aos componentes de negócio.
  • MVP.Task – No nosso exemplo, este projeto contém os componentes que compõem a camada de serviços de apresentação. Esta camada também faz um papel intermediário entre a camada de apresentação e a camada de componentes de negócios. Para o MVP, podemos considerá-la como uma abstração da classe Model (que está presente no exemplo em MVP.Domain).
  • MVP.WPFUI – Este projeto é a camada de apresentação em si, e é composto de uma tela construída em WPF.

Solution

Começaremos pela interface que representa IVew: IStockSearchView. Esta interface possui algumas propriedades que ajustam valores, ou seja, que recebem valores e os colocarão em “algum lugar”. Veja que neste momento não interessa que lugar seja esse.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MVP.Presentation
{
public interface IStockSearchView
{
string Ticker { set; }
double Price { set; }
string Message { set; }
}
}


Em seguida temos a classe Presenter: StockSearchPresenter. Lembre-se que ela é o meio-de-campo com a interface e a classe Model. Olhe que interessante: no construtor de Presenter recebe-se um objeto que implementa IStockSearchView, e esta instância é armazenada em uma propriedade privada. No método SearchStock, que efetua a pesquisa de uma determinada ação, é o modelo (Model) é acessado através da classe StockSearchTask (camada de serviços da apresentação), e o resultado obtido é atribuído na view. Quem quer que seja este objeto que implementa IStockSearchView, ele está recebendo os valores passivamente. O que será que será feito com estes dados?



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MVP.Task;

namespace MVP.Presentation
{
public class StockSearchPresenter
{
private IStockSearchView _view = null;
private StockSearchTask _task = new StockSearchTask();

public StockSearchPresenter(IStockSearchView view)
{
_view = view;
}

public void SearchStock(string ticker)
{
StockInfoVO info = _task.SearchStock(ticker);
_view.Price = info.IsValid ? info.Price : 0;
_view.Message = info.Message;
_view.Ticker = info.IsValid ? info.Ticker : ticker;
}
}
}


Antes de prosseguir, vamos dar uma olhada na camada de serviços de apresentação e o modelo de negócios.



StockSearchTask é uma classe que acessa os componentes de negócio. Neste nosso exemplo, é uma chamada simples, mas na “vida real” esta chamada poderia ter sido feita através de um Web Service. Note que aqui a classe Task está fazendo uma abstração de como nós estamos acessando a camada de negócios. Veja também que ela faz uso de uma classe do tipo Value Object (StockInfoVO), ou seja, um objeto criado simplesmente para servidor de conteiner de informações, ou seja, este objeto não possui operações (comportamento).



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MVP.Domain;

namespace MVP.Task
{
public class StockSearchTask
{
public StockInfoVO SearchStock(string ticker)
{
StockInfoVO info = new StockInfoVO();
StockManager manager = new StockManager();
Stock stock = manager.SearchForStock(ticker);
info.IsValid = stock != null;
if (info.IsValid)
{
info.Ticker = stock.Ticker;
info.Price = stock.Price;
}
else
info.Message = "Stock not found!";
return info;
}
}
}


Outro detalhe importante deste objeto do tipo VO é que ele também ajuda a separar a camada de apresentação da de negócios, fazendo com que os tipos que nesta última existam não sejam conhecidos pela interface gráfica.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MVP.Task
{
public struct StockInfoVO
{
public string Ticker;
public double Price;
public bool IsValid;
public string Message;
}
}


Agora vamos a fundo no projeto dos componentes de negócio. Temos a classe Stock, que representa uma ação. Ela armazena o ticker da ação e possui um método que retorna seu preço (que aqui, como falamos, retorna um número aleatório).



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MVP.Domain
{
public class Stock
{
public string Ticker { get; set; }
public double Price
{
get
{
// Gera um preço aleatório
Random random = new Random(Environment.TickCount);
return random.NextDouble() * 100;
}
}
}
}


Também temos a classe StockManager, que é responsável pelo gerenciamento das classes Stock. Aqui ela funciona como um agrupador de várias instâncias de Stock para nosso exemplo simples (temos apenas algumas empresas relacionadas aqui…, Petrobrás, Vale, Lojas Renner, etc.). O que ela faz, efetivamente, é manter uma lista dessas empresas em memória, e quando chamado o método SearchForStock(string), ele retorna uma dessas instâncias, caso ela exista (senão retorna null).



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MVP.Domain
{
public class StockManager
{
private static List _stocks = new List()
{
new Stock(){ Ticker = "PETR3"},
new Stock(){ Ticker = "VALE5"},
new Stock(){ Ticker = "LREN3"},
new Stock(){ Ticker = "GGBR4"},
new Stock(){ Ticker = "ITUB3"},
};

public Stock SearchForStock(string ticker)
{
Stock stock = (from s in _stocks
where s.Ticker == ticker.ToUpper()
select s).FirstOrDefault();
return stock;
}
}
}


Por fim, temos a própria tela em WPF. Veja que ela implementa IStockSearchView. As implementações das operações definidas nessa interface fazem a atribuição dos valores aos controles visuais que estão na ela (labels, por exemplo). No evento de clique do botão, temos a chamada a classe Presenter: No seu construtor, passamos a própria instância da tela (this). Executando o seu método SearchStock, passamos as informações necessárias a StockSearchPresenter para que ela possa acessar o modelo de negócios e trazer as informações que queremos.



Tela em WPF



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using MVP.Presentation;

namespace MVP.WPFUI
{
///
/// Interaction logic for MainWindow.xaml
///

public partial class MainWindow : Window, IStockSearchView
{
public MainWindow()
{
InitializeComponent();
}

public string Ticker
{
set { txtTicker.Text = value; }
}

public double Price
{
set { lblPrice.Content = string.Format("{0:c}", value); }
}

public string Message
{
set { lblMsg.Content = value; }
}

private void btnSearch_Click(object sender, RoutedEventArgs e)
{
StockSearchPresenter presenter = new StockSearchPresenter(this);
presenter.SearchStock(txtTicker.Text);
}
}
}


Vimos aqui como fazer a separação entre a apresentação e o serviços de negócio, mas qual seria o esforço se precisássemos colocar essa funcionalidade em outra plataforma, por exemplo uma plataforma web? A criação da página seria bem simples, como podemos ver no código abaixo. Basta apenas fazer com que a própria página ASP.NET implemente IStockSearchView, acertar o que deve ser mudado quando suas propriedades forem chamadas e colocar o que deve ser feito quando o botão de pesquisa é clicado.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using MVP.Presentation;

namespace MVP.ASPNETUI
{
public partial class _Default : System.Web.UI.Page, IStockSearchView
{
protected void Page_Load(object sender, EventArgs e)
{

}

public string Ticker
{
set { txtTicker.Text = value; }
}

public double Price
{
set { lblPrice.Text = string.Format("{0:c}", value); }
}

public string Message
{
set { lblMsg.Text = value; }
}

protected void btnSearch_Click(object sender, EventArgs e)
{
StockSearchPresenter presenter = new StockSearchPresenter(this);
presenter.SearchStock(txtTicker.Text);
}
}
}


Página ASP.NET



Com isso, percebemos como é feita a separação de responsabilidades entre a camada de apresentação e a camada de negócios. Com isso, temos menos código rodando na tela / página, o que facilita a manutenção e migração de plataforma dos sistemas que construímos.



Outra vantagem importante mas que não cobri neste post é a facilidade com que podemos criar testes automatizados nos sistemas, utilizando técnicas de mock.



[]’s

Comentários

Postagens mais visitadas deste blog

Trocando configurações padrão do Live TIM

Uma proposta de Clean Architecure com Modelo de Atores

Testes automatizados em sistemas autenticados com certificados digitais, usando Selenium e PhantomJS