German (Deutsch) translation by Valentina (you can also view the original English article)
Beim neuen Programmierhype geht es um funktionale Programmierparadigmen. Funktionale Sprachen werden immer mehr in größeren und besseren Anwendungen verwendet. Scala, Haskel usw. gedeihen und andere, konservativere Sprachen wie Java begannen, einige der funktionalen Programmierparadigmen zu übernehmen (sehen Sie Abschlüsse in Java7 und Lazy Eval für Listen in Java8). Was jedoch nur wenige wissen, ist, dass PHP sehr vielseitig ist, wenn es um funktionale Programmierung geht. Alle wesentlichen funktionalen Programmierungskonzepte können in PHP ausgedrückt werden. Wenn Sie also mit der funktionalen Programmierung noch nicht vertraut sind, sollten Sie sich darauf vorbereiten, dass Ihre Gedanken durchgebrannt werden, und wenn Sie bereits mit der funktionalen Programmierung vertraut sind, bereiten Sie sich darauf vor, mit diesem Tutorial viel Spaß zu haben.
Programmierung-Paradigmen
Ohne Programmieung-Paradigmen könnten wir alles tun, was wir wollen, wie wir wollen. Das würde zwar zu extremer Flexibilität führen, aber auch zu unmöglichen Architekturen und sehr aufgeblähtem Code. Daher wurden Programmierung-Paradigmen erfunden, um uns Programmierern dabei zu helfen, auf spezifische Weise über ein bestimmtes Programm nachzudenken und auf diese Weise unsere Fähigkeit einzuschränken, unsere Lösung auszudrücken.
Jedes Programmierung-Paradigma nimmt uns die Freiheit:
- Modulare Programmierung nimmt unbegrenzte Programmgröße weg.
- Strukturierte und prozedurale Programmierung nehmen dem "Go-to" den Weg und beschränken den Programmierer auf Reihenfolge, Auswahl und Iteration.
- Objektorientierte Programmierung entfernt Zeiger auf Funktionen.
- Funktionale Programmierung nimmt Zuordnung und veränderlichen Zustand auf.
Funktionale Programmierung-Prinzipien
Bei der funktionalen Programmierung haben Sie keine Daten, die durch Variablen dargestellt werden.
In der funktionalen Programmierung ist alles eine Funktion. Und ich meine alles. Beispielsweise kann ein Satz wie in der Mathematik als mehrere Funktionen dargestellt werden. Ein Array oder eine Liste ist auch eine Funktion oder eine Gruppe von Funktionen.
In der objektorientierten Programmierung ist alles ein Objekt. Ein Objekt ist eine Sammlung von Daten und Methoden, die mit diesen Daten Aktionen ausführen. Objekte haben einen Zustand, einen flüchtigen, veränderbaren Zustand.
Bei der funktionalen Programmierung haben Sie keine Daten, die durch Variablen dargestellt werden. Es gibt keine Datencontainer. Einige Werte können definiert und zugewiesen werden. Einige Werte können definiert und zugewiesen werden. In den meisten Fällen handelt es sich jedoch um Funktionen, die "Variablen" zugewiesen sind. Ich habe "Variablen" zwischen Anführungszeichen gesetzt, weil sie in der funktionalen Programmierung unveränderlich sind. Obwohl die meisten funktionalen Programmiersprachen keine Unveränderlichkeit erzwingen, erzwingen die meisten objektorientierten Sprachen keine Objekte. Wenn Sie den Wert nach einer Zuweisung ändern, führen Sie keine rein funktionale Programmierung mehr durch.
Da den Variablen keine Werte zugewiesen sind, haben Sie bei der funktionalen Programmierung keinen Status.
Aufgrund der fehlenden Zustände und Zuweisungen haben funktionale Programmierung-Funktionen haben keine Nebenwirkung. Und aus den drei vorgenannten Gründen sind Funktionen immer vorhersehbar. Das bedeutet im Wesentlichen, dass Sie immer, wenn Sie eine Funktion mit denselben Parametern immer wieder aufrufen, immer dasselbe Ergebnis haben. Das ist ein großer Vorteil gegenüber der objektorientierten Programmierung und reduziert die Komplexität von Multithread- und Massive-Multithread-Anwendungen erheblich.
Wenn wir jedoch alles in Funktionen ausdrücken wollen, müssen wir sie Parametern zuweisen oder von anderen Funktionen zurückgeben können. Daher erfordert die funktionale Programmierung die Unterstützung von Funktionen höherer Ordnung. Das bedeutet im Grunde, dass eine Funktion einer "Variablen" zugewiesen, als Parameter an eine andere Funktion gesendet und als Ergebnis einer Funktion zurückgegeben werden kann.
Schließlich, weil wir keine Werte in Variablen haben, sind while und for Schleifen für die Funktionsprogrammierung ungewöhnlich und werden durch Rekursion ersetzt.
Zeigen Sie mir den Code!
Es reicht Sprechen und Philosophie für eine Stunde. Lassen Sie uns Code schreiben!
Richten Sie ein PHP-Projekt in Ihrer bevorzugten IDE oder im Code-Editor ein. Erstellen Sie darin einen "Tests"-Ordner. Erstellen Sie zwei Dateien: FunSets.php im Ordner des Projekts und FunSetsTest.php im Ordner Tests. Wir erstellen eine Anwendung mit Tests, die das Konzept von Mengen darstellen.
In der Mathematik ist eine Menge eine Sammlung verschiedener Objekte, die als eigenständiges Objekt betrachtet werden. (Wikipedia)
Das bedeutet im Grunde, dass Sets eine Menge Dinge an einem einzigen Ort sind. Diese Mengen können und werden durch mathematische Operationen charakterisiert: Vereinigungen, Schnittpunkte, Unterschiede usw. Und durch umsetzbare Eigenschaften wie: enthält.
Unsere Programmierung-Beschränkungen
Also lassen Sie uns Code schreiben! Aber warten Sie. Wie? Nun, um die Konzepte der funktionalen Programmierung zu respektieren, müssen wir folgende Einschränkungen auf unseren Code anwenden:
- Keine Zuweisungen - Variablen dürfen keine Werte zugewiesen werden. Wir können jedoch Funktionen den Variablen zuweisen.
- Kein veränderlicher Zustand. - Es ist uns nicht gestattet, im Falle einer Abtretung den Wert dieser Abtretung zu ändern. Wir dürfen auch nicht den Wert einer Variablen ändern, deren Wert als Parameter für die aktuelle Funktion festgelegt wurde. Also keine Änderung der Parameter.
- Nein, während und für Schleifen. - Wir dürfen keine PHP-Befehle "while" und "for" verwenden. Wir können jedoch unsere eigene Methode definieren, um die Elemente einer Menge zu durchlaufen und foreach/for/while zu nennen.
Für Tests gelten keine Einschränkungen. Aufgrund der Natur von PHPUnit verwenden wir dort klassischen objektorientierten PHP-Code. Um unsere Tests besser unterzubringen, werden wir unseren gesamten Produktionscode in einer einzigen Klasse zusammenfassen.
Die definierende Funktion des Sets
Wenn Sie ein erfahrener Programmierer sind, sich aber mit der funktionalen Programmierung nicht auskennen, sollten Sie jetzt wie üblich aufhören zu denken und bereit sein, Ihre Komfortzone zu verlassen. Vergessen Sie alle Ihre bisherigen Denkweisen über ein Problem und stellen Sie sich alle Funktionen vor.
Die definierende Funktion einer Menge ist ihre "contains"-Methode.
1 |
function contains($set, $elem) { |
2 |
return $set($elem); |
3 |
}
|
OK ... Das ist nicht so offensichtlich, also sehen wir uns an, wie wir es benutzen würden.
1 |
$set = function ($element) {return true;}; |
2 |
contains($set, 100); |
Nun, das erklärt es ein bisschen besser. Die Funktion "contains" hat zwei Parameter:
-
$set- repräsentiert eine als Funktion definierte Menge. -
$elem- repräsentiert ein Element, das als Wert definiert ist.
In diesem Zusammenhang muss "contains" nur die Funktion in $set mit dem Parameter $elem angewendet werden. Lassen Sie uns alle in einen Test einwickeln.
1 |
class FunSetsTest extends PHPUnit_Framework_TestCase { |
2 |
|
3 |
private $funSets; |
4 |
|
5 |
protected function setUp() { |
6 |
$this->funSets = new FunSets(); |
7 |
}
|
8 |
|
9 |
function testContainsIsImplemented() { |
10 |
// We caracterize a set by its contains function. It is the basic function of a set.
|
11 |
|
12 |
$set = function ($element) {return true;}; |
13 |
$this->assertTrue($this->funSets->contains($set, 100)); |
14 |
}
|
15 |
}
|
Und binden Sie unseren Produktionscode in FunSets.php in eine Klasse ein:
1 |
class FunSets { |
2 |
|
3 |
public function contains($set, $elem) { |
4 |
return $set($elem); |
5 |
}
|
6 |
}
|
Sie können diesen Test tatsächlich ausführen und er wird bestanden. Die Menge, die wir für diesen Test definiert haben, ist nur eine Funktion, die immer true zurückgibt. Es ist eine "wahre Menge".
Das Singleton Set
Wenn das vorige Kapitel ein wenig verwirrend war oder in der Logik unbrauchbar aussah, wird es dies etwas klarer machen. Wir möchten ein Set mit einem einzelnen Element, einem Singleton-Set, definieren. Denken Sie daran, dass dies eine Funktion sein muss, und wir möchten sie wie im unten stehenden Test verwenden.
1 |
function testSingletonSetContainsSingleElement() { |
2 |
// A singleton set is characterize by a function which passed to contains will return true for the single element
|
3 |
// passed as its parameter. In other words, a singleton is a set with a single element.
|
4 |
|
5 |
$singleton = $this->funSets->singletonSet(1); |
6 |
$this->assertTrue($this->funSets->contains($singleton, 1)); |
7 |
}
|
Wir müssen eine Funktion namens "singeltonSet" mit einem Parameter definieren, der ein Element der Menge darstellt. Im Test ist das die Nummer Eins (1). Dann erwarten wir, dass unsere contains-Methode beim Aufruf mit einer Singleton-Funktion true zurückgibt, wenn der Parameter "send in" gleich eins ist. Der Code, der den Test bestanden hat, lautet wie folgt:
1 |
public function singletonSet($elem) { |
2 |
return function ($otherElem) use ($elem) { |
3 |
return $elem == $otherElem; |
4 |
};
|
5 |
}
|
Beeindruckend! Das ist verrückt. Die Funktion "singletonSet" erhält also als Parameter ein Element als $elem. Dann wird eine andere Funktion zurückgegeben, die einen Parameter $otherElem enthält. Diese zweite Funktion vergleicht $elem mit $otherElem.
Wie funktioniert das? Zuerst diese Zeile:
1 |
$singleton = $this->funSets->singletonSet(1); |
wird in das umgewandelt, was "singletonSet(1)" zurückgibt:
1 |
$singleton = function ($otherElem) { |
2 |
return 1 == $otherElem; |
3 |
};
|
Dann wird "contains($singleton, 1)" aufgerufen. Welches wiederum heißt, was in $singleton ist. Der Code wird also:
1 |
$singleton(1) |
Das führt den Code tatsächlich aus, wobei $otherElem den Wert eins hat.
1 |
return 1 == 1 |
Was natürlich stimmt und unser Test bestanden hat.
Lächeln Sie schon? Fühlen Sie, wie Ihr Gehirn anfängt zu kochen? Ich habe es sicherlich getan, als ich dieses Beispiel zum ersten Mal in Scala geschrieben habe, und ich habe es wieder getan, als ich dieses Beispiel zum ersten Mal in PHP geschrieben habe. Ich finde das außergewöhnlich. Es ist uns gelungen, eine Menge mit einem Element zu definieren, mit der Möglichkeit zu überprüfen, dass sie den Wert enthält, den wir in sie übergeben haben. Wir haben das alles ohne eine einzige Wertzuweisung gemacht. Wir haben keine Variable, die den Wert Eins enthält oder den Status Eins hat. Kein Zustand, keine Zuordnung, keine Veränderlichkeit, keine Schleifen. Wir sind hier auf dem richtigen Weg.
Vereinigung der Sets
Da wir nun einen Satz mit einem einzigen Wert erstellen können, müssen wir einen Satz mit mehreren Werten erstellen können. Der naheliegende Weg besteht darin, die Gewerkschaftsoperation auf unseren Sets zu definieren. Die Vereinigung zweier Singleton-Sets repräsentiert eine andere Vereinigung mit beiden Werten. Ich möchte, dass Sie sich eine Minute Zeit nehmen und über die Lösung nachdenken, bevor Sie zum Code scrollen. Nehmen Sie vielleicht einen Blick auf die unten aufgeführten Tests.
1 |
function testUnionContainsAllElements() { |
2 |
// A union is characterized by a function which gets 2 sets as parameters and contains all the provided sets
|
3 |
|
4 |
// We can only create singletons at this point, so we create 2 singletons and unite them
|
5 |
$s1 = $this->funSets->singletonSet(1); |
6 |
$s2 = $this->funSets->singletonSet(2); |
7 |
$union = $this->funSets->union($s1, $s2); |
8 |
|
9 |
// Now, check that both 1 and 2 are part of the union
|
10 |
$this->assertTrue($this->funSets->contains($union, 1)); |
11 |
$this->assertTrue($this->funSets->contains($union, 2)); |
12 |
// ... and that it does not contain 3
|
13 |
$this->assertFalse($this->funSets->contains($union, 3)); |
14 |
}
|
Wir wollen eine Funktion namens "union", die zwei Parameter erhält, beide Sätze. Denken Sie daran, dass Sets nur Funktionen für uns sind, daher erhält unsere "union"-Funktion zwei Funktionen als Parameter. Dann möchten wir mit "contains" überprüfen können, ob die Vereinigung ein Element enthält oder nicht. Daher muss unsere "union"-Funktion eine andere Funktion zurückgeben, die "contains" verwenden kann.
1 |
public function union($s1, $s2) { |
2 |
return function ($otherElem) use ($s1, $s2) { |
3 |
return $this->contains($s1, $otherElem) || $this->contains($s2, $otherElem); |
4 |
};
|
5 |
}
|
Das funktioniert eigentlich ganz gut. Und es ist vollkommen gültig, auch wenn Ihre Gewerkschaft zusammen mit einer anderen Gewerkschaft plus einem Einzelspieler aufgerufen wird. Es ruft contains in sich für jeden Parameter auf. Wenn es eine Gewerkschaft ist, wird es wiederkehren. So einfach ist das.
Überschneidung und Unterschied
Wir können dieselbe Einzeilenlogik mit geringfügigen Änderungen anwenden, um die nächsten zwei wichtigsten Funktionen zu erhalten, die eine Menge charakterisieren: Schnittmenge - enthält nur die gemeinsamen Elemente zwischen zwei Mengen - und Differenz - enthält nur die Elemente der ersten Menge, die nicht Teil sind vom zweiten Satz.
1 |
public function intersect($s1, $s2) { |
2 |
return function ($otherElem) use ($s1, $s2) { |
3 |
return $this->contains($s1, $otherElem) && $this->contains($s2, $otherElem); |
4 |
};
|
5 |
}
|
6 |
|
7 |
public function diff($s1, $s2) { |
8 |
return function ($otherElem) use ($s1, $s2) { |
9 |
return $this->contains($s1, $otherElem) && !$this->contains($s2, $otherElem); |
10 |
};
|
11 |
}
|
Ich werde Sie nicht mit dem Testcode für diese beiden Methoden überfluten. Die Tests werden geschrieben und Sie können sie überprüfen, wenn Sie in dem beigefügten Code nachsehen.
Filter-Set
Nun, das ist etwas komplizierter, wir können das nicht mit einer einzigen Codezeile lösen. Ein Filter ist eine Funktion, die zwei Parameter verwendet: eine Mengen- und eine Filterfunktion. Sie wendet die Filterfunktion auf eine Menge an und gibt eine andere Menge zurück, die nur die Elemente enthält, die die Filterfunktion erfüllen. Um es besser zu verstehen, hier ist der Test dafür.
1 |
function testFilterContainsOnlyElementsThatMatchConditionFunction() { |
2 |
$u12 = $this->createUnionWithElements(1, 2); |
3 |
$u123 = $this->funSets->union($u12, $this->funSets->singletonSet(3)); |
4 |
|
5 |
// Filtering rule, find elements greater than 1 (meaning 2 and 3)
|
6 |
$condition = function($elem) {return $elem > 1;}; |
7 |
|
8 |
// Filtered set
|
9 |
$filteredSet = $this->funSets->filter($u123, $condition); |
10 |
|
11 |
// Verify filtered set does not contain 1
|
12 |
$this->assertFalse($this->funSets->contains($filteredSet, 1), "Should not contain 1"); |
13 |
// Check it contains 2 and 3
|
14 |
$this->assertTrue($this->funSets->contains($filteredSet, 2), "Should contain 2"); |
15 |
$this->assertTrue($this->funSets->contains($filteredSet, 3), "Should contain 3"); |
16 |
}
|
17 |
|
18 |
private function createUnionWithElements($elem1, $elem2) { |
19 |
$s1 = $this->funSets->singletonSet($elem1); |
20 |
$s2 = $this->funSets->singletonSet($elem2); |
21 |
return $this->funSets->union($s1, $s2); |
22 |
}
|
Wir erstellen ein Set mit drei Elementen: 1, 2, 3. Und wir platzieren es in der Variablen $u123, damit es in unseren Tests leicht identifiziert werden kann. Dann definieren wir eine Funktion, die wir auf den Test anwenden möchten, und platzieren sie in die $condition. Zum Schluss rufen wir filter auf unseren $u123-Satz mit $condition auf und platzieren den resultierenden Satz in $filteredSet. Dann führen wir Assertions mit "contains" aus, um festzustellen, ob das Set so aussieht, wie wir es wollen. Unsere Bedingungsfunktion ist einfach. Sie gibt true zurück, wenn das Element größer als eins ist. Unser letzter Satz sollte also nur die Werte zwei und drei enthalten, und das prüfen wir in unseren Behauptungen.
1 |
public function filter($set, $condition) { |
2 |
return function ($otherElem) use ($set, $condition) { |
3 |
if ($condition($otherElem)) |
4 |
return $this->contains($set, $otherElem); |
5 |
return false; |
6 |
};
|
7 |
}
|
Und hier gehts. Wir haben eine Filterung mit nur drei Zeilen Code implementiert. Wenn die Bedingung für das bereitgestellte Element zutrifft, führen wir ein Set für das Set für dieses Element aus. Wenn nicht, kehren wir einfach false zurück. Das war's.
Elemente überlaufen
Im nächsten Schritt erstellen Sie verschiedene Schleifenfunktionen. Die erste "forall()" nimmt eine $set und eine $condition und gibt true zurück, wenn die $condition für alle Elemente der $set gilt. Das führt zu folgendem Test:
1 |
function testForAllCorrectlyTellsIfAllElementsSatisfyCondition() { |
2 |
$u123 = $this->createUnionWith123(); |
3 |
|
4 |
$higherThanZero = function($elem) { return $elem > 0; }; |
5 |
$higherThanOne = function($elem) { return $elem > 1; }; |
6 |
$higherThanTwo = function($elem) { return $elem > 2; }; |
7 |
|
8 |
$this->assertTrue($this->funSets->forall($u123, $higherThanZero)); |
9 |
$this->assertFalse($this->funSets->forall($u123, $higherThanOne)); |
10 |
$this->assertFalse($this->funSets->forall($u123, $higherThanTwo)); |
11 |
}
|
Wir haben die $u123-Erstellung aus dem vorherigen Test in eine private Methode extrahiert. Dann definieren wir drei verschiedene Bedingungen: höher als Null, höher als Eins und höher als Zwei. Da unser Set die Zahlen Eins, Zwei und Drei enthält, sollte nur die Bedingung 0 den Wert Wahr zurückgeben, der Rest sollte Falsch sein. In der Tat können wir den Test mit Hilfe einer anderen rekursiven Methode zum Durchlaufen aller Elemente durchführen.
1 |
private $bound = 1000; |
2 |
|
3 |
private function forallIterator($currentValue, $set, $condition) { |
4 |
if ($currentValue > $this->bound) |
5 |
return true; |
6 |
elseif ($this->contains($set, $currentValue)) |
7 |
return $condition($currentValue) && $this->forallIterator($currentValue + 1, $set, $condition); |
8 |
else
|
9 |
return $this->forallIterator($currentValue + 1, $set, $condition); |
10 |
}
|
11 |
|
12 |
public function forall($set, $condition) { |
13 |
return $this->forallIterator(-$this->bound, $set, $condition); |
14 |
}
|
Wir definieren zunächst einige Grenzen für unser Set. Die Werte müssen zwischen -1000 und +1000 liegen. Das ist eine vernünftige Einschränkung, die wir auferlegen, um dieses Beispiel einfach genug zu halten. Die Funktion "forall" ruft die private Methode "forallIterator" mit den erforderlichen Parametern auf, um rekursiv zu entscheiden, ob alle Elemente die Bedingung berücksichtigen. In dieser Funktion testen wir zuerst, ob wir uns außerhalb der Grenzen befinden. Wenn ja, kehren Sie zurück. Prüfen Sie dann, ob unser Set den aktuellen Wert enthält, und geben Sie den aktuellen, auf die Bedingung angewendeten Wert zusammen mit einem logischen "AND" mit einem rekursiven Aufruf an uns selbst mit dem nächsten Wert zurück. Ansonsten rufen Sie uns einfach mit dem nächsten Wert an und geben Sie das Ergebnis zurück.
Das funktioniert gut, wir können es genauso implementieren wie "exists()". Das gibt true zurück, wenn eines der Elemente die Bedingung erfüllt.
1 |
private function existsIterator($currentValue, $set, $condition) { |
2 |
if ($currentValue > $this->bound) |
3 |
return false; |
4 |
elseif ($this->contains($set, $currentValue)) |
5 |
return $condition($currentValue) || $this->existsIterator($currentValue + 1, $set, $condition); |
6 |
else
|
7 |
return $this->existsIterator($currentValue + 1, $set, $condition); |
8 |
}
|
9 |
|
10 |
public function exists($set, $condition) { |
11 |
return $this->existsIterator(-$this->bound, $set, $condition); |
12 |
}
|
Der einzige Unterschied ist, dass wir false zurückgeben, wenn außerhalb der Grenzen, und wir verwenden "OR" anstelle von "AND" im zweiten if.
Nun wird "map()" anders, einfacher und kürzer sein.
1 |
public function map($set, $action) { |
2 |
return function ($currentElem) use ($set, $action) { |
3 |
return $this->exists($set, function($elem) use ($currentElem, $action) { |
4 |
return $currentElem == $action($elem); |
5 |
});
|
6 |
};
|
7 |
}
|
Mapping bedeutet, dass wir eine Aktion auf alle Elemente einer Menge anwenden. Für Map benötigen wir keinen Helfer-Iterator. Wir können "exists()" wiederverwenden und die Elemente von "exists" zurückgeben, die das Ergebnis von $action erfüllen, das auf $element angewendet wird. Dies ist möglicherweise am ersten Standort nicht offensichtlich. Sehen wir uns also an, was passiert.
- Wir senden die Menge
{1, 2}und die Aktion$element * 2 (double)an die Karte. - Es wird natürlich eine Funktion zurückgegeben, die einen Parameter als Element hat und die Menge und die Aktion von einer höheren Ebene verwendet.
- Diese Funktion ruft
existsmit der Menge{1, 2}auf, und die Bedingungsfunktion$currentElemententspricht$elem * 2. -
exists()durchläuft alle Elemente zwischen -1000 und +1000, unseren Grenzen. Wenn ein Element gefunden wird, das doppelt aus dem, was von"contains"(dem Wert von$currentElement) kommt, wirdtruezurückgegeben. - Mit anderen Worten, der letzte Vergleich wird für den Aufruf von
truezurückgegeben und enthält den Wert zwei, wenn der mit zwei multiplizierte Wert des Stroms zwei ergibt. Für das erste Element des Sets, eins, wird es bei zwei true zurückgegeben. Für das zweite Element zwei auf Wert vier.
Ein praktisches Beispiel
Nun, funktionale Programmierung macht Spaß, ist aber bei PHP alles andere als ideal. Daher empfehle ich Ihnen nicht, ganze Anwendungen auf diese Weise zu schreiben. Nun, da Sie gelernt haben, was PHP funktional kann, können Sie Teile dieses Wissens in Ihren täglichen Projekten anwenden. Hier ist ein Beispiel für ein Authentifizierungsmodul. Die AuthPlugin-Klasse stellt eine Methode bereit, mit der ein Benutzer und ein Kennwort empfangen werden, um den Benutzer zu authentifizieren und seine Berechtigungen festzulegen.
1 |
class AuthPlugin { |
2 |
|
3 |
private $permissions = array(); |
4 |
|
5 |
function authenticate($username, $password) { |
6 |
$this->verifyUser($username, $password); |
7 |
|
8 |
$adminModules = new AdminModules(); |
9 |
$this->permissions[] = $adminModules->allowRead($username); |
10 |
$this->permissions[] = $adminModules->allowWrite($username); |
11 |
$this->permissions[] = $adminModules->allowExecute($username); |
12 |
}
|
13 |
|
14 |
private function verifyUser($username, $password) { |
15 |
// ... DO USER / PASS CHECKING
|
16 |
// ... LOAD USER DETAILS, ETC.
|
17 |
}
|
18 |
|
19 |
}
|
Nun, das hört sich vielleicht OK an, hat aber ein großes Problem. 80% der Methode "authenticate()" verwendet Informationen aus den "AdminModules".



Dies schafft eine sehr starke Abhängigkeit.Es wäre viel sinnvoller, die drei Aufrufe anzunehmen und eine einzige Methode für AdminModules zu erstellen.



Durch die Umstellung der Generation auf AdminModules konnten wir also drei Abhängigkeiten auf nur eine reduzieren. Die öffentliche Schnittstelle von AdminModules wurde ebenfalls von drei auf nur eine einzige Methode reduziert. Wir sind jedoch noch nicht da. AuthPlugin hängt noch direkt von AdminModules ab.
Ein objektorientierter Ansatz
Wenn unser Authentifizierungs-Plugin von einem beliebigen Modul verwendet werden soll, müssen wir eine gemeinsame Schnittstelle für diese Module definieren. Lassen Sie uns die Abhängigkeit einführen und eine Schnittstelle einführen.
1 |
class AuthPlugin { |
2 |
|
3 |
private $permissions = array(); |
4 |
private $appModule; |
5 |
|
6 |
function __construct(ApplicationModule $appModule) { |
7 |
$this->appModule = $appModule; |
8 |
}
|
9 |
|
10 |
function authenticate($username, $password) { |
11 |
$this->verifyUser($username, $password); |
12 |
|
13 |
$this->permissions = array_merge( |
14 |
$this->permissions, |
15 |
$this->appModule->getPermissions($username) |
16 |
);
|
17 |
}
|
18 |
|
19 |
private function verifyUser($username, $password) { |
20 |
// ... DO USER / PASS CHECKING
|
21 |
// ... LOAD USER DETAILS, ETC.
|
22 |
}
|
23 |
|
24 |
}
|
AuthPlugin hat einen Konstruktor erhalten. Es erhält einen Parameter vom Typ ApplicationModule, eine Schnittstelle, und ruft "getPermissions()" für dieses eingefügte Objekt auf.
1 |
interface ApplicationModule { |
2 |
|
3 |
public function getPermissions($username); |
4 |
|
5 |
}
|
ApplicationModule definiert eine einzelne öffentliche Methode, "getPermissions()", mit einem Benutzernamen als Parameter.
1 |
class AdminModules implements ApplicationModule { |
2 |
// [ ... ]
|
3 |
}
|
Schließlich muss AdminModules die ApplicationModule-Schnittstelle implementieren.



Das ist jetzt viel besser. Unser AuthPlugin hängt nur von einer Schnittstelle ab. AdminModules hängt von derselben Schnittstelle ab, sodass das AuthPlugin modulunabhängig wurde. Wir können eine beliebige Anzahl von Modulen erstellen, die alle ApplicationModule implementieren, und AuthPlugin kann mit allen Modulen arbeiten.
Ein funktionaler Ansatz
Eine andere Möglichkeit, die Abhängigkeit umzukehren und AdminModule oder jedes andere Modul zur Verwendung des AuthPlugins zu veranlassen, besteht darin, in diese Module eine Abhängigkeit von AuthPlugin einzufügen. AuthPlugin bietet eine Möglichkeit zum Festlegen der Authentifizierungsfunktion, und jede Anwendung sendet eine eigene "getPermission()" -Funktion.
1 |
class AdminModules { |
2 |
|
3 |
private $authPlugin; |
4 |
|
5 |
function __construct(Authentitcation $authPlugin) { |
6 |
$this->authPlugin = $authPlugin; |
7 |
}
|
8 |
|
9 |
private function allowRead($username) { |
10 |
return "yes"; |
11 |
}
|
12 |
|
13 |
private function allowWrite($username) { |
14 |
return "no"; |
15 |
}
|
16 |
|
17 |
private function allowExecute($username) { |
18 |
return $username == "joe" ? "yes" : "no"; |
19 |
}
|
20 |
|
21 |
private function authenticate() { |
22 |
|
23 |
$this->authPlugin->setPermissions( |
24 |
function($username) { |
25 |
$permissions = array(); |
26 |
$permissions[] = $this->allowRead($username); |
27 |
$permissions[] = $this->allowWrite($username); |
28 |
$permissions[] = $this->allowExecute($username); |
29 |
return $permissions; |
30 |
}
|
31 |
);
|
32 |
$this->authPlugin->authenticate(); |
33 |
}
|
34 |
|
35 |
}
|
Wir beginnen mit AdminModule. Es implementiert nichts mehr. Es verwendet jedoch ein injiziertes Objekt, das die Authentifizierung implementieren muss. In AdminModule gibt es eine "authenticate()"-Methode, die "setPermissions()" für AuthPlugin aufruft und die zu verwendende Funktion übergibt.
1 |
interface Authentication { |
2 |
|
3 |
function setPermissions($permissionGrantingFunction); |
4 |
|
5 |
function authenticate(); |
6 |
|
7 |
}
|
Die Authentifizierungsschnittstelle definiert einfach die beiden Methoden.
1 |
class AuthPlugin implements Authentication { |
2 |
|
3 |
private $permissions = array(); |
4 |
private $appModule; |
5 |
private $permissionsFunction; |
6 |
|
7 |
function __construct(ApplicationModule $appModule) { |
8 |
$this->appModule = $appModule; |
9 |
}
|
10 |
|
11 |
function authenticate($username, $password) { |
12 |
$this->verifyUser($username, $password); |
13 |
$this->permissions = $this->permissionsFunction($username); |
14 |
}
|
15 |
|
16 |
private function verifyUser($username, $password) { |
17 |
// ... DO USER / PASS CHECKING
|
18 |
// ... LOAD USER DETAILS, ETC.
|
19 |
}
|
20 |
|
21 |
public function setPermissions($permissionGrantingFunction) { |
22 |
$this->permissionsFunction = $permissionGrantingFunction; |
23 |
}
|
24 |
|
25 |
}
|
Schließlich implementiert AuthPlugin die Authentifizierung und legt die eingehende Funktion in einem privaten Klassenattribut fest. Dann wird "authentication()" zu einer dummen Methode. Es ruft einfach die Funktion auf und setzt dann den Rückgabewert. Es ist völlig entkoppelt von dem, was kommt.



Wenn wir das Schema betrachten, gibt es zwei wichtige Änderungen:
- Anstelle von
AdminModuleimplementiertAuthPlugindie Schnittstelle. -
AuthPlugin"ruft" dasAdminModuleoder ein anderes Modul auf, das an die Berechtigungsfunktion gesendet wird.
Welches zu benutzen?
Auf diese Frage gibt es keine richtige Antwort. Ich würde argumentieren, dass der objektorientierte Ansatz angemessener ist, wenn der Prozess der Berechtigungsermittlung ziemlich vom Anwendungsmodul abhängt. Wenn Sie jedoch der Meinung sind, dass jedes Anwendungsmodul eine Authentifizierungsfunktion bereitstellen kann, und Ihr AuthPlugin nur ein Skelett ist, das die Authentifizierungsfunktionalität bereitstellt, ohne jedoch etwas über Benutzer und Verfahren zu wissen, können Sie sich für die Funktionsweise entscheiden.
Der funktionale Ansatz macht Ihr AuthPlugin sehr abstrakt und Sie können sich darauf verlassen. Wenn Sie jedoch vorhaben, Ihrem AuthPlugin mehr zu erlauben und mehr über Benutzer und Ihr System zu erfahren, wird es zu konkret, und Sie möchten sich nicht darauf verlassen. Wählen Sie in diesem Fall den objektorientierten Weg und lassen Sie das konkrete AuthPlugin von den abstrakteren Anwendungsmodulen abhängen.



