Skip to content

10. Schichtbasierte Anwendungen

10.1. Einleitung

Image

Wir werden nun untersuchen, wie eine Anwendung PHP in Schichten strukturiert werden kann:

Image

In diesem Schema ergreift die linke Schicht die Initiative, um die rechte Schicht zu nutzen. Die Schichten haben folgende Aufgaben:

  • [1]: Die als [dao] (Data Access Objects) bezeichnete Schicht kümmert sich um den Austausch mit externen Datenspeichern [4] (Dateien, Datenbanken, Webdienste…). Diese Schicht wird manchmal als [DAL] (Data Access Layer) bezeichnet – ein Begriff, der die Rolle der Schicht besser beschreibt. Diese Schicht kann Daten aus [3] lesen oder in [2] schreiben. Sie wird von der Schicht [métier] und [6] aufgerufen und liefert ihr Ergebnisse im Format [7];
  • [5]: eine Schicht namens [métier], die „Geschäftsprozesse“ bündelt, denen alle benötigten Daten zur Verfügung gestellt werden. Dies ist in der Regel die stabilste Schicht eines Projekts, da sie nicht davon abhängt, wie die Daten erfasst werden. Diese stammen aus zwei Quellen:
    • [9]: die vom Skript PHP bereitgestellten Daten;
    • [6,7]: die von der Schicht [dao] angeforderten Daten.
  • [8]: Das Hauptskript fungiert als Koordinator. In einer Konsolenanwendung führt es folgende Schritte aus:
    • die Ebenen [métier] und [dao] erstellen;
    • den Algorithmus der Anwendung ausführen. Dieser Algorithmus entspricht dem eines Dirigenten: Er enthält keinerlei „Geschäftslogik“ oder Code für den Datenzugriff. Das Hauptskript beschränkt sich darauf, die Prozeduren der Schichten [métier] und [9] aufzurufen. Die Schicht [dao] und externe Daten werden dabei vollständig ignoriert. Es kann der Schicht [métier] Daten aus der Schicht [9] bereitstellen. In einer Konsolenanwendung können diese aus Konfigurationsdateien oder vom Benutzer des Skripts stammen. Er erhält Ergebnisse [10] von der Schicht [métier]. Möglicherweise muss es bestimmte Ergebnisse speichern: Dazu nutzt es erneut die Prozeduren der Schicht [métier], die wiederum die Schicht [dao] ansprechen, welche die Arbeit erledigt;
    • Unter den Ergebnissen kann das Hauptskript auch Ausnahmen erhalten. Es ist seine Aufgabe, die Ausnahmen zu behandeln, die aus allen Schichten zurückgemeldet werden;

Diese Aufteilung in Schichten soll die Weiterentwicklung der Anwendung erleichtern. Zu diesem Zweck implementiert jede Schicht eine Schnittstelle. Nehmen wir an, dass:

  • Die Schicht [dao] wird durch die Klasse [Dao] implementiert, die die Schnittstelle [IDao] implementiert;
  • Die Schicht [métier] wird durch die Klasse [Métier] implementiert, die die Schnittstelle [IMétier] implementiert;

Die Prozeduren der Schicht [métier] verwenden dann die Schnittstelle [IDao] anstelle der Klasse [Dao]. Dadurch kann die Schicht [Dao] weiterentwickelt werden, ohne die Schicht [Métier] zu verändern. Nehmen wir an, dass in Version 1 die Schicht [Dao1] Daten aus einer Datenbank verwendet. Nach einer Weiterentwicklung werden diese Daten nun in Version 2, [Dao2], von einem Webdienst bereitgestellt. Es ist sicherzustellen, dass die beiden Klassen [Dao1] und [Dao2] dieselbe Schnittstelle [IDao] implementieren und dieselben Ausnahmen auslösen. Wenn dies überprüft wurde, bleibt die Schicht [Métier], die mit der Schnittstelle [IDao] arbeitet, unverändert;

Die gleiche Argumentation gilt für die Schicht [Métier].

Betrachten wir eine Implementierung mit Klassen und Schnittstellen:

Image

Die Anwendung wird die folgende Schichtstruktur implementieren:

Image

10.2. Zwischen den Schichten ausgetauschte Objekte

Normalerweise tauschen die Schichten verschiedene Objekte aus. Hier tauschen sie die folgende Klasse aus: [Personne.php]:


<?php

// eine Person
class Personne {
  // ID
  private $id;

  // Konstruktor
  public function __construct(int $id) {
    $this->id = $id;
  }

  // toString
  public function __toString(): string {
    return "[Personne($this->id)]";
  }

}

Anmerkungen

  • Zeile 6: Die Person hat nur ein Attribut, ihre Kennung [$id]. Wir gehen davon aus, dass diese Kennung eine eindeutige Person in einem Data Warehouse bezeichnet;
  • Zeilen 9–11: Der Konstruktor, mit dem ein Typ [Personne] mit seiner Kennung erstellt werden kann;
  • Zeilen 14–16: Die Methode [__toString], die die Kennung der Person ausgibt;

10.3. Schicht [dao]

Die von der Schicht [dao, 1] implementierte Schnittstelle [IDao] lautet wie folgt: [IDao.php]:


<?php


// Ebene [dao] ------------------------
interface IDao {

  // Abholung einer Person aus einem externen Lager
  // Die Identifikationsnummer der Person wird übermittelt
  public function get(int $id): Personne;

  // Speichern einer Person in einem externen Lager
  public function save(Personne $p): void;
}

Diese Schnittstelle wird von der folgenden Klasse [Dao1] implementiert: [Dao1.php]:


<?php

class Dao1 implements IDao {

  // Speichern einer Person in einem externen Lager
  public function save(Personne $p): void {
    print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
  }

  // Abruf einer Person aus einem externen Lager
  public function get(int $id): Personne {
    print "[Dao1] : Récupération de la personne d'identité ($id) en base de données [locale]\n";
    return new Personne($id);
  }

}

Anmerkungen

  • Die Klasse interagiert nicht mit einem Data Warehouse. Es werden lediglich Meldungen angezeigt, um den Ablauf des Codes zu verfolgen (Zeilen 7 und 12);
  • Zeile 13: Es wird tatsächlich eine Person mit der an die Methode übergebenen ID zurückgegeben (Zeile 11);

Wir implementieren außerdem die Schnittstelle [IDao] mit der folgenden Klasse [Dao2] [Dao2.php]:


<?php

class Dao2 implements IDao {

  // Speichern einer Person in einem externen Speicher
  public function save(Personne $p): void {
    print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
  }

  // Abruf einer Person aus einem externen Data Warehouse
  public function get(int $id): Personne {
    print "[Dao2] : Récupération de la personne d'identité ($id) en base de données [distante]\n";
    return new Personne($id);
  }

}

Die Klasse [Dao2] ähnelt der Klasse [Dao1], mit dem Unterschied, dass wir die angezeigten Meldungen geändert haben.

10.4. Ebene [métier]

Die Schicht [métier, 2] bietet die folgende Schnittstelle [IMétier]: [IMetier.php]:


<?php

// Ebene [métier] ------------------------
interface IMétier extends IDao {

  // Auswertung einer Person anhand ihrer ID
  public function doSomething(Personne $p): void;
}

Anmerkungen

  • Die Schnittstelle [IMétier] erweitert die Schnittstelle [IDao]. Dies ist keineswegs zwingend erforderlich. Wir tun dies hier, weil das Beispiel einfach ist;
  • Zeile 7: Die Methode [doSomething] ist spezifisch für die Schicht [Métier];

Die Schnittstelle [IMétier] wird von der folgenden Klasse [Métier] implementiert: [Métier.php]:


<?php

class Métier implements IMétier {
  // Schicht [dao]
  private $dao;

  // Getter/Setter
  public function setDao(IDao $dao): void {
    $this->dao = $dao;
  }

  public function getDao(): IDao {
    return $this->dao;
  }

  // man beutet eine Person aus
  public function doSomething(Personne $p): void {
    // Bearbeitung
    print "Métier : Traitement métier de la personne $p\n";
  }

  // Speicherung der Person
  public function save(Personne $p): void {
    print "[Métier] : Sauvegarde de la personne $p\n";
    // die Ebene [dao] wird mit der Speicherung beauftragt
    $this->dao->save($p);
  }

  public function get(int $id): Personne {
    print "[Métier] : récupération de la personne d'identifiant $id\n";
    // Die Person wird von der Ebene [dao] angefordert
    $personne = $this->dao->get($id);
    return $personne;
  }

}

Anmerkungen

  • Zeile 5: Die Schicht [métier] muss eine Referenz auf die Schicht [dao] haben, um deren Methoden nutzen zu können;
  • Zeilen 8–10: Mit der Methode [setDao] kann der Ebene [métier] die Referenz der Ebene [dao] zugewiesen werden. Es ist zu beachten, dass der Typ des Parameters [IDao] ist. Somit kann die Schicht [métier] mit jeder Klasse arbeiten, die die Schnittstelle [IDao] implementiert. Wenn man von einer Schicht [Dao1] zu einer Schicht [Dao2] wechselt und beide die Schnittstelle [IDao] implementieren, muss die Schicht [métier] nicht neu geschrieben werden;
  • Zeilen 17–20: Implementierung von [IMetier::doSomething];
  • Zeilen 23–27: Implementierung von [IMetier::save]. Diese Methode muss eine Person in einem Data Warehouse speichern. Die Schicht [métier] kann dies nicht. Sie wendet sich an die Schicht [dao], um diese Speicherung durchzuführen;
  • Zeilen 29–34: Implementierung von [IMetier::get]. Diese Methode muss aus einem Data Warehouse die Person abrufen, deren ID ihr als Parameter übergeben wird. Die Schicht [métier] kann dies nicht ausführen. Sie wendet sich an die Schicht [dao], um die Aufgabe zu erledigen (Zeile 32);

Fazit

Sobald die Schicht [métier] auf die im Data Warehouse gespeicherten Daten zugreifen muss, muss sie die Schicht [dao] durchlaufen, die für den Zugriff auf diese Daten erstellt wurde.

10.5. Hauptskript

Wir werden zwei Skripte schreiben, die als „Dirigenten“ für diese Anwendung fungieren. Das erste, [main1.php], wird die Schicht [Dao1] nutzen, während das zweite, [main2.php], die Schicht [Dao2] nutzen wird. Wir möchten zeigen, dass dies keinerlei Auswirkungen auf den Code der Schicht [Métier] hat.

Das Skript [main1.php] lautet wie folgt:


<?php

// Strikte Einhaltung der Funktionsparameter
declare (strict_types=1);

// Einbindung von Klassen und Schnittstellen
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao1.php";
require_once __DIR__."/Métier.php";

// Test ----------------
// Erstellung der Schichten
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// Verwendung der Ebene [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);

Anmerkungen

  • Lassen Sie uns einige Punkte in Erinnerung rufen: Das Skript [main1.php] ist der „Dirigent“ der Anwendung. Es erstellt die Schichtstruktur der Anwendung (Zeilen 15–17) und beginnt anschließend mit der Kommunikation mit der Schicht [métier] (Zeilen 19–21). Die Schichtstruktur sieht wie folgt aus:

Image

Gemäß diesem Schema darf sich das Skript [main1.php] nur an die Schicht [métier] wenden. Es darf sich nicht an die Schicht [dao] wenden, auch wenn dies theoretisch möglich wäre.

Die Ergebnisse der Ausführung lauten wie folgt:


[Métier] : récupération de la personne d'identifiant 4
[Dao1] : Récupération de la personne d'identité (4) en base de données [locale]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao1] : Sauvegarde de la personne [Personne(4)] en base de données [locale]

Kommentare

  • Zeile 10 des Codes hat dazu geführt, dass die Zeilen 1 und 2 der Ergebnisse geschrieben wurden;
  • Zeile 20 des Codes hat dazu geführt, dass Zeile 3 der Ergebnisse geschrieben wurde;
  • Zeile 21 des Codes hat dazu geführt, dass die Zeilen 4 und 5 der Ergebnisse geschrieben wurden;

Das Skript [main2.php] lautet wie folgt:


<?php

// Strikte Einhaltung der Funktionsparameter
declare (strict_types=1);

// Einbindung von Klassen und Schnittstellen
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao2.php";
require_once __DIR__."/Métier.php";

// Test ----------------
// Erstellung der Schichten
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// Verwendung der Schicht [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);

Kommentare

  • Zeilen 36–38: Die Schichtstruktur verwendet nun die Schicht [Dao2];

Die Ergebnisse der Ausführung lauten wie folgt:


[Métier] : récupération de la personne d'identifiant 4
[Dao2] : Récupération de la personne d'identité (4) en base de données [distante]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao2] : Sauvegarde de la personne [Personne(4)] en base de données [distante]

Die Schicht [Dao1] simuliert einen Zugriff auf eine lokale Datenbank, während die Schicht [Dao2] einen Zugriff auf eine entfernte Datenbank simuliert. Solange diese beiden Schichten die Schnittstelle [IDao] einhalten, ist ersichtlich, dass der Code der Schicht [Métier] nicht geändert werden musste.

Wir werden das gerade Gelernte auf die Übung zur Steuerberechnung anwenden, die uns als Leitfaden dient.