Spanish (Español) translation by steven (you can also view the original English article)
El nuevo bombo en programación tiene que ver con los paradigmas de programación funcional. Los lenguajes funcionales se utilizan cada vez más en aplicaciones grandes y mejores. Scala, Haskel, etc. están prosperando y otros lenguajes más conservadores como Java comenzaron a adoptar algunos de los paradigmas de programación funcional (mira los closures en Java7 y lazy eval para listas en Java8). Sin embargo, lo que pocas personas saben es que PHP es bastante versátil cuando se trata de programación funcional. Todos los principales conceptos de programación funcional se pueden expresar en PHP. Por lo tanto, si eres nuevo en la programación funcional, prepárate para dejar volar tu mente, y si ya estás familiarizado con la programación funcional, prepárate para divertirte mucho con este tutorial.
Paradigmas de programación
Sin paradigmas de programación, podríamos hacer lo que queramos de la forma que queramos. Si bien esto conduciría a una flexibilidad extrema, también conduciría a arquitecturas imposibles y un código muy inflado. Así, se inventaron paradigmas de programación para ayudarnos a nosotros, los programadores, a pensar de una manera específica sobre un programa específico y de esta manera, limitar nuestra capacidad de expresar nuestra solución.
Cada paradigma de programación nos quita una libertad:
- La programación modular elimina el tamaño ilimitado del programa.
- La programación estructurada y procedimental está eliminando el "ir a" y limita al programador a la secuencia, selección e iteración.
- La programación orientada a objetos elimina los indicadores de funciones.
- La programación funcional elimina la asignación y el estado mutable.
Principios de programación funcional
En la programación funcional, no tienes datos representados por variables.
En la programación funcional todo es una función. Y me refiero a todo. Por ejemplo, un conjunto, como en matemáticas, se puede representar como varias funciones. Una matriz o lista también es una función o un grupo de funciones.
En la programación orientada a objetos, todo es un objeto. Y un objeto es una colección de datos y métodos que realizan acciones sobre esos datos. Los objetos tienen un estado, un estado volátil y mutable.
En la programación funcional, no tienes datos representados por variables. No hay contenedores de datos. Los datos no se asignan a una variable. Se pueden definir y asignar algunos valores. Sin embargo, en la mayoría de los casos son funciones asignadas a "variables". He puesto "variables" entre comillas porque en la programación funcional son inmutables. Aunque la mayoría de los lenguajes de programación funcional no imponen la inmutabilidad, de la misma manera la mayoría de los lenguajes orientados a objetos no imponen objetos, si cambias el valor después de una asignación, ya no estás haciendo programación funcional pura.
Debido a que no tienes valores asignados a las variables, en la programación funcional no tienes ningún estado.
Debido a que no hay estado ni asignaciones, en la programación funcional las funciones no tienen efectos secundarios. Y debido a las tres razones anteriores, las funciones siempre son predecibles. Básicamente, esto significa que si llamas a una función con los mismos parámetros una y otra y otra vez... siempre obtendrás el mismo resultado. Esta es una gran ventaja sobre la programación orientada a objetos y reduce en gran medida la complejidad de las aplicaciones multiproceso y las que usan multiproceso masivamente.
Pero, si queremos expresar todo en funciones, necesitamos poder asignarlos a parámetros o devolverlos desde otras funciones. Por tanto, la programación funcional requiere el soporte de funciones de orden superior. Básicamente, esto significa que una función puede asignarse a una "variable", enviarse como parámetro a otra función y devolverse como resultado de una función.
Finalmente, debido a que no tenemos valores en las variables, los bucles while y for son inusuales en la programación funcional y se reemplazan por recursividad.
¡Muéstrame el código!
Suficiente charla y filosofía para una lección. ¡Codifiquemos!
Configura un proyecto PHP en tu IDE o editor de código favorito. Crea en él una carpeta llamada "Tests". Crea dos archivos: FunSets.php en la carpeta del proyecto y FunSetsTest.php en la carpeta Tests. Crearemos una aplicación, con pruebas, que representará el concepto de conjuntos.
En matemáticas, un conjunto es una colección de objetos distintos, considerados como un objeto por derecho propio. (wikipedia)
Eso básicamente significa que los conjuntos son un montón de cosas en un solo lugar. Estos conjuntos pueden estar y se caracterizan por operaciones matemáticas: uniones, intersecciones, diferencias, etc. Y por propiedades accionables como: 'contains'.
Nuestras limitaciones de programación
¡Así que vamos a programar! Pero espera... ¿cómo lo hacemos? Pues bien, para respetar los conceptos de programación funcional tendremos que aplicar las siguientes restricciones a nuestro código:
- Sin asignaciones. - No se nos permite asignar valores a las variables. Sin embargo, podemos asignar funciones a las variables.
- Sin estado mutable. - No se nos permite, en caso de una cesión, cambiar el valor de esa cesión. Tampoco se nos permite cambiar el valor de ninguna variable que tenga su valor establecido como parámetro para la función actual. Por lo tanto, no hay cambios de parámetros.
- No hay bucles while y for. - No se nos permite usar comandos de PHP como "while" y "for". Sin embargo, podemos definir nuestro propio método para recorrer los elementos de un conjunto y llamarlo foreach/for/while.
No se aplican limitaciones a las pruebas. Debido a la naturaleza de PHPUnit, usaremos código PHP clásico orientado a objetos allí. Además, para acomodar mejor nuestras pruebas, empaquetaremos todo nuestro código de producción en una sola clase.
La función definitoria del conjunto
Si eres un programador experimentado, pero no estás familiarizado con la programación funcional, ahora es el momento de dejar de pensar como lo haces habitualmente y estar listo para salir de tu zona de confort. Olvídate de todas tus formas anteriores de razonar sobre un problema e imagina todo en funciones.
Definiendo la función de un conjunto con su método "contains".
1 |
function contains($set, $elem) { |
2 |
return $set($elem); |
3 |
}
|
Ok ... Esto no es tan obvio, así que veamos cómo la usaríamos.
1 |
$set = function ($element) {return true;}; |
2 |
contains($set, 100); |
Bueno, esto lo explica un poco mejor. La función "contains" tiene dos parámetros:
-
$set- representa un conjunto definido como una función. -
$elem- representa un elemento definido como un valor.
En este contexto, todo lo que tiene que hacer "contains" es aplicar la función en $set con el parámetro $elem. Envolvamos todo en una prueba.
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 |
}
|
Y envolvamos nuestro código de producción dentro de FunSets.php en una clase:
1 |
class FunSets { |
2 |
|
3 |
public function contains($set, $elem) { |
4 |
return $set($elem); |
5 |
}
|
6 |
}
|
De hecho, puedes ejecutar esta prueba y pasará. El conjunto que definimos para esta prueba es solo una función que siempre devuelve verdadero. Es un "conjunto verdadero".
El conjunto Singleton
Si el capítulo anterior fue un poco confuso o parecía inútil en lógica, este te aclarará un poco. Queremos definir un conjunto con un solo elemento, un conjunto singleton. Recuerda, esto tiene que ser una función, y queremos usarla como en la prueba a continuación.
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 |
}
|
Necesitamos definir una función llamada "singeltonSet" con un parámetro que represente un elemento del conjunto. En la prueba, ese es el número uno (1). Después, esperamos que nuestro método contains, cuando se llama con una función singleton, devuelva true si el parámetro enviado es igual a uno. El código que hace que la prueba pase es el siguiente:
1 |
public function singletonSet($elem) { |
2 |
return function ($otherElem) use ($elem) { |
3 |
return $elem == $otherElem; |
4 |
};
|
5 |
}
|
¡Genial! Eso es una locura. Entonces, la función "singletonSet" obtiene como parámetro un elemento como $elem. Luego devuelve otra función que tiene un parámetro $otherElem y esta segunda función comparará $elem con $otherElem.
¿Entonces, cómo funciona esto? Primero, esta línea:
1 |
$singleton = $this->funSets->singletonSet(1); |
se transforma en lo que devuelve "singletonSet(1)":
1 |
$singleton = function ($otherElem) { |
2 |
return 1 == $otherElem; |
3 |
};
|
Luego se llama a "contains($singleton, 1)". Lo cual, a su vez, llama a lo que esté en $singleton. Entonces el código se convierte en:
1 |
$singleton(1) |
Lo que realmente ejecuta el código en él con $otherElem que tiene el valor uno.
1 |
return 1 == 1 |
Lo cual, por supuesto, es cierto y nuestra prueba pasa.
¿Ya estas sonriendo? ¿Sientes que tu cerebro empieza a hervir? Ciertamente lo hice cuando escribí este ejemplo por primera vez en Scala y lo hice nuevamente cuando escribí este ejemplo por primera vez en PHP. Creo que esto es extraordinario. Logramos definir un conjunto, con un elemento, con la capacidad de comprobar que contiene el valor que le pasamos. Hicimos todo esto sin una sola asignación de valor. No tenemos ninguna variable que contenga el valor uno o que tenga un estado de uno. Sin estado, sin asignación, sin mutabilidad, sin bucles. Estamos en el camino correcto aquí.
Unión de conjuntos
Ahora que podemos crear un conjunto con un solo valor, necesitamos poder crear un conjunto con varios valores. La forma obvia de hacerlo es definir la operación de unión en nuestros conjuntos. La unión de dos conjuntos singleton representará otra unión con ambos valores. Quiero que te tomes un minuto y pienses en la solución antes de desplazarte al código, tal vez eches un vistazo a las pruebas a continuación.
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 |
}
|
Queremos una función llamada "union" que obtenga dos parámetros, ambos conjuntos. Recuerda, los conjuntos son solo funciones para nosotros, por lo que nuestra función "union" obtendrá dos funciones como parámetros. Después, queremos poder verificar con "contains" si la unión contiene un elemento o no. Entonces, nuestra función "union" debe devolver otra función que "contains" pueda usar.
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 |
}
|
En realidad, esto está funcionando bastante bien. Y es perfectamente válido incluso cuando tu unión se llama con otra unión más un singleton. Esto llama a contains dentro de sí mismo para cada parámetro. Si es una unión, se repetirá. Es así de simple.
Intersección y diferencia
Podemos aplicar la misma lógica de una línea con cambios menores para obtener las siguientes dos funciones más importantes que caracterizan un conjunto: intersección - contiene solo los elementos comunes entre dos conjuntos - y diferencia - contiene solo aquellos elementos del primer conjunto que no son parte del segundo conjunto.
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 |
}
|
No te inundaré con el código de prueba para estos dos métodos. Las pruebas están escritas y puedes comprobarlas si miras en el código adjunto.
Conjunto de filtros
Bueno, esto es un poco más complicado, no podremos resolverlo con una sola línea de código. Un filtro es una función que utiliza dos parámetros: un conjunto y una función de filtrado. Aplica la función de filtrado a un conjunto y devuelve otro conjunto que contiene solo los elementos que satisfacen la función de filtrado. Para entenderlo mejor, aquí está la prueba.
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 |
}
|
Creamos un conjunto con tres elementos: 1, 2, 3. Y lo colocamos en la variable $u123 para que sea fácilmente identificable en nuestras pruebas. Luego definimos una función que queremos aplicar a la prueba y la colocamos en $condition. Finalmente, llamamos a 'filter' en nuestro conjunto $u123 con $condition y colocamos el conjunto resultante en $filterSet. Después ejecutamos aserciones con "contains" para determinar si el conjunto se ve como queremos. Nuestra función de condición es simple, devolverá verdadero si el elemento es mayor que uno. Por lo tanto, nuestro conjunto final debe contener solo los valores dos y tres, y esto es lo que verificamos en nuestras afirmaciones.
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 |
}
|
Y aquí tienes. Implementamos el filtrado con solo tres líneas de código. Más precisamente, si la condición se aplica al elemento proporcionado, ejecutamos un 'contiene' en el conjunto para ese elemento. Si no, simplemente devolvemos false. Eso es todo.
Bucle sobre elementos
El siguiente paso es crear varias funciones de bucle. El primero, "forall()", tomará un $set y $condition y devolverá true si $condition se aplica a todos los elementos del $set. Esto conduce a la siguiente prueba:
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 |
}
|
Extrajimos la creación de $u123 de la prueba anterior en un método privado. Luego definimos tres condiciones diferentes: mayor que cero, mayor que uno y mayor que dos. Como nuestro conjunto contiene los números uno, dos y tres, solo la condición superior a cero debe devolver verdadero, el resto debe ser falso. De hecho, podemos hacer que la prueba pase con la ayuda de otro método recursivo que se usa para iterar sobre todos los elementos.
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 |
}
|
Comenzamos definiendo algunos límites para nuestro conjunto. Los valores deben estar entre -1000 y +1000. Esta es una limitación razonable que imponemos para que este ejemplo sea lo suficientemente simple. La función "forall" llamará al método privado "forallIterator" con los parámetros necesarios para decidir de forma recursiva si todos los elementos respetan la condición. En esta función, primero probamos si estamos fuera de los límites. Si es así, devuelve verdadero. Luego verifica si nuestro conjunto contiene el valor actual y devuelve el valor actual aplicado a la condición junto con un "Y" lógico con una llamada recursiva a nosotros mismos con el siguiente valor. De lo contrario, simplemente llámenos con el siguiente valor y devolvemos el resultado.
Esto funciona bien, podemos implementarlo de la misma manera que "exists()". Esto devuelve true si alguno de los elementos satisface la condición.
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 |
}
|
La única diferencia es que devolvemos false cuando estamos fuera de límites y usamos "O" en lugar de "Y" en el segundo "if".
Ahora, "map()" será diferente, más simple y más corto.
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 |
}
|
Mapear significa que aplicamos una acción a todos los elementos de un conjunto. Para el mapa, no necesitamos un iterador auxiliar, podemos reutilizar "exists()" y devolver los elementos de "exists" que satisfacen el resultado de $action aplicado a $element. Es posible que esto no sea obvio en el primer sitio, así que veamos qué está sucediendo.
- Enviamos el conjunto
{1, 2}y la acción$element * 2 (double)al mapa. - Devolverá una función, obviamente, que tiene un parámetro como elemento y usa el conjunto y la acción de un nivel superior.
- Esta función llamará a
existscon el conjunto{1, 2}y la función de condición$currentElementes igual a$elem * 2. -
exists()iterará sobre todos los elementos entre -1000 y +1000, nuestros límites. Cuando encuentra un elemento, el doble de lo que proviene de"contains"(el valor de$currentElement) devolverátrue. - En otras palabras, la última comparación devolverá
truepara la llamada a 'contains' con valor dos, cuando el valor de la actual multiplicación por dos, da como resultado dos. Entonces, para el primer elemento del conjunto, uno, devolverá verdadero en dos. Para el segundo elemento, dos, cuando su valor sea cuatro.
Un ejemplo práctico
Bueno, la programación funcional es divertida, pero está lejos de ser ideal en PHP. Por lo tanto, no te recomiendo que escribas aplicaciones completas de esta manera. Sin embargo, ahora que has aprendido lo que PHP puede hacer funcionalmente, puedes aplicar partes de este conocimiento en tus proyectos diarios. Aquí hay un módulo de autenticación de ejemplo. La clase AuthPlugin proporciona un método que recibe un usuario y una contraseña y hace su magia para autenticar al usuario y establecer sus permisos.
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 |
}
|
Ahora, esto puede sonar bien, pero tiene un gran problema. El 80% del método "authenticate()" utiliza información de los "AdminModules". Esto crea una dependencia muy fuerte.



Sería mucho más razonable tomar las tres llamadas y crear un método único en AdminModules.



Entonces, al mover la generación a AdminModules, logramos reducir tres dependencias a solo una. La interfaz pública de AdminModules también se redujo de tres a un solo método. Sin embargo, todavía no hemos llegado. AuthPlugin todavía depende directamente de AdminModules.
Un enfoque orientado a objetos
Si queremos que nuestro complemento de autenticación sea utilizable por cualquier módulo, necesitamos definir una interfaz común para estos módulos. Inyectemos la dependencia e introduzcamos una interfaz.
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 tiene un constructor. Obtiene un parámetro de tipo ApplicationModule, una interfaz y llama a "getPermissions()" en este objeto inyectado.
1 |
interface ApplicationModule { |
2 |
|
3 |
public function getPermissions($username); |
4 |
|
5 |
}
|
ApplicationModule define un método público único, "getPermissions()", con un nombre de usuario como parámetro.
1 |
class AdminModules implements ApplicationModule { |
2 |
// [ ... ]
|
3 |
}
|
Finalmente, AdminModules necesita implementar la interfaz ApplicationModule.



Ahora, esto es mucho mejor. Nuestro AuthPlugin depende solo de una interfaz. AdminModules depende de la misma interfaz, por lo que AuthPlugin se convirtió en un módulo independiente. Podemos crear cualquier cantidad de módulos, todos implementando ApplicationModule, y AuthPlugin podrá trabajar con todos ellos.
Un enfoque funcional
Otra forma de revertir la dependencia y hacer que AdminModule, o cualquier otro módulo, use AuthPlugin es inyectar en estos módulos una dependencia a AuthPlugin. AuthPlugin proporcionará una forma de configurar la función de autenticación y cada aplicación enviará su propia función "getPermission()".
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 |
}
|
Comenzamos con AdminModule. Esto ya no implementa nada. Sin embargo, usa un objeto inyectado que tiene que implementar la autenticación. En AdminModule habrá un método "authenticate()" que llamará a "setPermissions()" en AuthPlugin y pasará la función que debe usarse.
1 |
interface Authentication { |
2 |
|
3 |
function setPermissions($permissionGrantingFunction); |
4 |
|
5 |
function authenticate(); |
6 |
|
7 |
}
|
La interfaz 'Authentication' simplemente define los dos métodos.
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 |
}
|
Finalmente, AuthPlugin implementa la autenticación y establece la función entrante en un atributo de clase privada. Por lo tanto, "authentication()" se convierte en un método tonto. Simplemente llama a la función y luego establece el valor de retorno. Está completamente desacoplado de lo que entra.



Si miramos el esquema, hay dos cambios importantes:
- En lugar de
AdminModule,AuthPlugines el que implementa la interfaz. -
AuthPlugin"llamará de vuelta a"AdminModule, o cualquier otro módulo enviado en la función de permisos.
¿Cuál usar?
No hay una respuesta correcta a esta pregunta. Yo diría que si el proceso de determinar los permisos depende bastante del módulo de la aplicación, entonces el enfoque orientado a objetos es más apropiado. Sin embargo, si crees que cada módulo de aplicación debería poder proporcionar una función de autenticación, y tu AuthPlugin es solo un esqueleto que proporciona la funcionalidad de autenticación, pero sin saber nada sobre los usuarios y los procedimientos, entonces puedes optar por el enfoque funcional.
El enfoque funcional hace que tu AuthPlugin sea muy abstracto y puedes confiar en él. Sin embargo, si planeas permitir que tu AuthPlugin haga más y sepa más sobre los usuarios y tu sistema, entonces se volverá demasiado concreto y no querrás depender de él. En ese caso, elige la forma orientada a objetos y deja que el AuthPlugin concreto dependa de los módulos de aplicación más abstractos.



