Samstag, 3. Oktober 2009

Software-Patterns: MVP (Model-View-Presenter)

Nein ich habe mich nicht vertippt.
Wir behandeln heute ein Pattern das sich Model-View-Presenter schimpft. Viele von euch setzten sicher MVC-Frameworks ein, dagegen ist ja auch an und für sich nichts einzuweden, aber wir möchten euch zeigen das es auch noch andere Architekturen gibt die sich hervorragend dafür eignen Webseiten an den Mann zu bringen. Und das auch noch gekapselt!


Aber genug der Worte, hier ein einfaches Beispiel für ein MVP-Pattern:



namespace at\delegate\HazAClass\Views
{

 use at\delegate\HazAClass\Presenters\PersonPresenter as Presenter;

 interface iPersonView
 {
  public function setFirstName($firstName);
  public function getFirstName();
  
  public function setLastName($lastName);
  public function getLastName();
  
  public function render();
 }


 class PersonView implements iPersonView
 {
  private $presenter;
  private $firstName;
  private $lastName;

  public function __construct()
  {
   $this->presenter = new Presenter($this);
  }
  
  public function refreshView()
  {
   $this->presenter->loadData();
  }
  
  public function setFirstName($firstName)
  {
   $this->firstName = $firstName;
  }
  
  public function getFirstName()
  {
   return $this->fistName;
  }
  
  public function setLastName($lastName)
  {
   $this->lastName = $lastName;
  }
  
  public function getLastName()
  {
   return $this->lastName;
  }
  
  public function render()
  {
   echo 'Person: '.$this->getFirstName().' '.$this->getLastName();
  }
 }

 class ExtendetPersonView implements iPersonView
 {
  private $presenter;
  private $firstName;
  private $lastName;

  public function __construct()
  {
   $this->presenter = new Presenter($this);
  }
  
  public function refreshView()
  {
   $this->presenter->loadData();
  }
  
  public function setFirstName($firstName)
  {
   $this->firstName = $firstName;
  }
  
  public function getFirstName()
  {
   return $this->fistName;
  }
  
  public function setLastName($lastName)
  {
   $this->lastName = $lastName;
  }
  
  public function getLastName()
  {
   return $this->lastName;
  }
  
  public function render()
  {
   echo 'Mein Vorname ist '.$this->getFirstName().'. Der Nachname lautet '.$this->getLastName().'.';
  }
 }

}

namespace at\delegate\HazAClass\Presenters
{
 use at\delegate\HazAClass\Views\iPersonView;
 use at\delegate\HazAClass\Models\PersonRepository;
 
 class PersonPresenter
 {
  /**
   * @var iPersonView
   */
  private $view;
 
  public function __construct(iPersonView $view)
  {
   $this->view = $view;
  }
  
  public function loadData()
  {
   $model = PersonRepository::getCurrentUser();
   
   $this->view->setFirstName($model->getFirstName());
   $this->view->setLastName($model->getLastName());
  }
 }
}

namespace at\delegate\HazAClass\Models
{
 class PersonRepository
 {
  public static function getCurrentUser()
  {
   //Pseudo DB-Request
   return Database::select('Person');
  }
 }
 
 class Person
 {
  private $id;
  private $firstName;
  private $lastName;
  
  public function setId($id)
  {
   $this->id = $id;
  }
  
  public function getId($id)
  {
   return $this->id;
  }
  
  public function setFirstName($firstName)
  {
   $this->firstName = $firstName;
  }
  
  public function getFirstName()
  {
   return $this->firstName;
  }
  
  public function setLastName($lastName)
  {
   $this->lastName = $lastName;
  }
 }
}


namespace at\delegate\HazAClass\Pages
{
 use at\delegate\HazAClass\Views\PersonView;
 use at\delegate\HazAClass\Views\DetailPersonView;
 
 $personView = new PersonView();
 $personView->refreshView();
 $personView->render();
 //outputs:
 //Person: Manuel Grundner
 
 
 $extendetPersonView = new PersonView();
 $extendetPersonView->refreshView();
 $extendetPersonView->render();
 //outputs:
 //Mein Vorname ist Manuel. Der Nachname lautet Grundner.
 
}namespace at\delegate\HazAClass\Views
{

 use at\delegate\HazAClass\Presenters\PersonPresenter as Presenter;

 interface iPersonView
 {
  public function setFirstName($firstName);
  public function getFirstName();
  
  public function setLastName($lastName);
  public function getLastName();
  
  public function render();
 }


 class PersonView implements iPersonView
 {
  private $presenter;
  private $firstName;
  private $lastName;

  public function __construct()
  {
   $this->presenter = new Presenter($this);
  }
  
  public function refreshView()
  {
   $this->presenter->loadData();
  }
  
  public function setFirstName($firstName)
  {
   $this->firstName = $firstName;
  }
  
  public function getFirstName()
  {
   return $this->fistName;
  }
  
  public function setLastName($lastName)
  {
   $this->lastName = $lastName;
  }
  
  public function getLastName()
  {
   return $this->lastName;
  }
  
  public function render()
  {
   echo 'Person: '.$this->getFirstName().' '.$this->getLastName();
  }
 }

 class ExtendetPersonView implements iPersonView
 {
  private $presenter;
  private $firstName;
  private $lastName;

  public function __construct()
  {
   $this->presenter = new Presenter($this);
  }
  
  public function refreshView()
  {
   $this->presenter->loadData();
  }
  
  public function setFirstName($firstName)
  {
   $this->firstName = $firstName;
  }
  
  public function getFirstName()
  {
   return $this->fistName;
  }
  
  public function setLastName($lastName)
  {
   $this->lastName = $lastName;
  }
  
  public function getLastName()
  {
   return $this->lastName;
  }
  
  public function render()
  {
   echo 'Mein Vorname ist '.$this->getFirstName().'. Der Nachname lautet '.$this->getLastName().'.';
  }
 }

}

namespace at\delegate\HazAClass\Presenters
{
 use at\delegate\HazAClass\Views\iPersonView;
 use at\delegate\HazAClass\Models\PersonRepository;
 
 class PersonPresenter
 {
  /**
   * @var iPersonView
   */
  private $view;
 
  public function __construct(iPersonView $view)
  {
   $this->view = $view;
  }
  
  public function loadData()
  {
   $model = PersonRepository::getCurrentUser();
   
   $this->view->setFirstName($model->getFirstName());
   $this->view->setLastName($model->getLastName());
  }
 }
}

namespace at\delegate\HazAClass\Models
{
 class PersonRepository
 {
  public static function getCurrentUser()
  {
   //Pseudo DB-Request
   return Database::select('Person');
  }
 }
 
 class Person
 {
  private $id;
  private $firstName;
  private $lastName;
  
  public function setId($id)
  {
   $this->id = $id;
  }
  
  public function getId($id)
  {
   return $this->id;
  }
  
  public function setFirstName($firstName)
  {
   $this->firstName = $firstName;
  }
  
  public function getFirstName()
  {
   return $this->firstName;
  }
  
  public function setLastName($lastName)
  {
   $this->lastName = $lastName;
  }
 }
}


namespace at\delegate\HazAClass\Pages
{
 use at\delegate\HazAClass\Views\PersonView;
 use at\delegate\HazAClass\Views\DetailPersonView;
 
 $personView = new PersonView();
 $personView->refreshView();
 $personView->render();
 //outputs:
 //Person: Manuel Grundner
 
 
 $extendetPersonView = new PersonView();
 $extendetPersonView->refreshView();
 $extendetPersonView->render();
 //outputs:
 //Mein Vorname ist Manuel. Der Nachname lautet Grundner.
 
}

Hui das war grad ne Menge Code...
Naja schaut viel aus, ist aber eigentlich ganz einfach.

Wir haben hier 2 Views. PersonView und DetailPersonView.
Die beiden benutzen die selben Daten, zeigen Sie aber unterschiedlich an (das kann man noch komplett weiterspinnen, mit Templates oder ähnlichem)

Der Presenter weiß eigentlich nichts über die Views an sich, nur das sie vom Type iPersonView sind. Er weiß nur wie er die Daten "reinschieben" kann, aber nicht wie diese gerendert werden oder ähnliches.
Er hält die Logik über die Aktualisierung des Views.

Der View weiß zu keiner Zeit wann er die Daten nachgeschoben bekommt, er deligiert lediglich eine Anfrage an seinen Presenter (den er kennt, da er ihn ja erzeugt hat). Das kann mittels $_GET oder $_POST oder irgendeiner anderen Logik gemacht werden. Hier in diesem Beispiel wird refreshView() von der übergeordneten Buisinesslogic aufgerufen.

Das Model weiß überhaupt nur über seine Daten bescheid. Das Repository hält Informationen über seine Modelle, weiß jedoch nichts über die Presenter oder gar die Views.

Aber warum der ganze Aufwand? Warum holt sich der View nicht selbst seine Daten beim Modell? Warum der Presenter? Das ergibt doch keinen Sinn!
Doch! Es macht Sinn. Die Views sind sehr stark gekapselt, man kann sie sehr einfach austauschen. Um eine neue Ansicht zu erstellen brauchen wir lediglich das Interface iPersonView zu Implementieren.
So ist es auch einfach große Views aus vielen kleinen Teilviews zu erzeugen, ein View kann ja mehrere Presenter haben. Wir müssen lediglich die richtigen Interfaces implementieren.

Ein weiterer Vorteil ist die einfache Testbarkeit. Durch das Interface kann ich den Test sehr modular gestalten und da ich strikt zwischen Presentation und Logic trennen kann, sind die Views (die an und für sich normalerweise untestbar sind) einfach zu testen, ich weiß ja was ich von der Schnittstelle erwarte.

Keine Kommentare:

Kommentar veröffentlichen