Introducción: ¿por qué necesitamos una máquina de estados?
En las aplicaciones, a menudo es necesario restringir el acceso a ciertas acciones en un objeto. Para esto, se utilizan módulos RBAC que resuelven el problema de restringir el acceso en función de los derechos del usuario. La tarea de administrar acciones, dependiendo del estado del objeto, permanece sin resolver. Este problema está bien resuelto utilizando una máquina de estados o una máquina de estados. Una conveniente máquina de estados le permite no solo recopilar en un solo lugar todas las reglas de transición entre los estados del objeto, sino que también pone cierto orden en el código al separar las reglas de transición, verificar las condiciones y el manejo y someterlas a reglas generales.
Quiero compartir la implementación de la máquina de estado para Zend Framework 3 usando Doctrine 2
para trabajar con la base de datos. El proyecto en sí se puede encontrar aquí .
Y aquí quiero compartir los principios básicos establecidos.
Empecemos
Almacenaremos la descripción del gráfico de transición en la tabla de la base de datos porque:
- Esto esta claro.
- Le permite utilizar el mismo diccionario de estado que se utiliza en el de interés.
nosotros un objeto que tiene un estado. - Le permite garantizar la integridad de la base de datos utilizando claves foráneas.
El uso de una máquina de estado finito no determinista aumentará la flexibilidad de nuestra solución.
El gráfico de transición se describirá utilizando un par de tablas A y B, conectadas por una relación de uno a muchos.
Tabla a:
CREATE TABLE `tr_a` ( `id` int(11) NOT NULL AUTO_INCREMENT, `src_id` varchar(32) COLLATE utf8_unicode_ci NOT NULL, `action_id` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT ' ', `condition` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ' ', PRIMARY KEY (`id`), KEY `IDX_96B84B3BFF529AC` (`src_id`), KEY `IDX_96B84B3B9D32F035` (`action_id`), CONSTRAINT `FK_96B84B3B9D32F035` FOREIGN KEY (`action_id`) REFERENCES `action` (`id`), CONSTRAINT `FK_96B84B3BFF529AC` FOREIGN KEY (`src_id`) REFERENCES `state` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Tabla B:
CREATE TABLE `tr_b` ( `id` int(11) NOT NULL AUTO_INCREMENT, `transition_a_id` int(11) NOT NULL, `dst_id` varchar(32) COLLATE utf8_unicode_ci NOT NULL, `weight` int(11) DEFAULT NULL COMMENT ' ,- , null- ', `condition` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ' ', `pre_functor` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ' , , ', `post_functor` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT ' , , ', PRIMARY KEY (`id`), KEY `IDX_E12699CB85F4C374` (`transition_a_id`), KEY `IDX_E12699CBE1885D19` (`dst_id`), CONSTRAINT `FK_E12699CB85F4C374` FOREIGN KEY (`transition_a_id`) REFERENCES `tr_a` (`id`), CONSTRAINT `FK_E12699CBE1885D19` FOREIGN KEY (`dst_id`) REFERENCES `state` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Omitiré la definición de clases de entidad; puedes ver un ejemplo aquí:
- mesa a
- tabla B
Aquí todo es estándar, incluida una descripción de la relación entre entidades.
Para usar la máquina de estado, solo necesitamos unos pocos métodos públicos
public function doAction($objE, $action, array $data = []) /** * * @param object $objE * @param string $action * @param array $data * @return bool */ public function hasAction($objE, $action, $data=[])
Hay varios métodos públicos más para un uso cómodo, pero aquí me gustaría prestar atención al algoritmo del método principal doAction()
.
Del objeto obtenemos su estado.
Al conocerlo y el identificador de acción, la entidad A se encuentra fácilmente en la tabla de transición A.
La condición obtenida por el identificador de condición, que se encuentra en la condition
de la entidad-A, le permite verificar si la acción se puede realizar. En particular, la cláusula RBAC mencionada al principio puede usarse en el validador de condición.
El validador será encontrado por el identificador del campo de condition
través del ValidatorPluginManager
y
debe implementar \Zend\Validator\ValidatorInterface
. Prefiero usar los herederos de ValidatorChain
. Esto facilita cambiar la composición de las condiciones controladas y reutilizar validadores simples como parte de las cadenas de prueba.
Determinamos la transición, verificamos la condición. Ya que tenemos un no determinista
máquina de estado , el resultado de la acción puede ser uno de varios estados.
Tales casos no son muy comunes, pero el proyecto propuesto es fácil de implementar.
Mediante la conexión A - <B, obtenemos una colección de posibles nuevos estados del objeto (entidad-B).
Para seleccionar un solo estado, verificamos a su vez las condiciones de la Entidad-B de la colección resultante, clasificándolas por weight
de mayor a menor en el campo de weight
. La primera comprobación exitosa de la condición nos da la Entidad-B, en la cual hay un nuevo estado del objeto (vea el campo dst_id
).
El nuevo estado está determinado. Ahora, antes de cambiar el estado, la máquina de estado se ejecutará
acciones definidas en el prefunctor, luego cambiará de estado y realizará acciones,
definido en la postfunción. La máquina de estado obtendrá functores basados en el nombre del campo pre_functor
para la prefunción y post_functor
para la función post utilizando el administrador de complementos y llamará al método __invoke () para los objetos recibidos.
No es necesario cambiar el estado utilizando functores. Esta es la tarea de la máquina de estado. Si no es necesario realizar acciones adicionales al cambiar el estado, establezca nulo en los campos anteriores.
Otras fichas:
- En los campos de las tablas de transición
condition
, pre_funtor
, post_functor
uso alias, en mi opinión, es conveniente. - Para mayor comodidad visual, cree una vista de las tablas A y B.
- Utilizo identificadores de cadena como la clave principal en los diccionarios de estado y acción. Esto no es necesario, pero conveniente. También se pueden usar diccionarios con identificadores numéricos.
- Dado que se utiliza una máquina de estados finitos no determinista, la acción no tiene que conducir a un cambio de estado. Esto le permite describir acciones como la visualización, por ejemplo.
- Además de los métodos para verificar una acción y realizar una acción, hay varios métodos públicos que permiten, por ejemplo, obtener una lista de acciones para un estado dado de un objeto o una lista de acciones disponibles para un estado dado de un objeto, teniendo en cuenta las comprobaciones. A menudo, en la interfaz en cuadrículas para cada registro, debe mostrar un conjunto de acciones. Estos métodos de máquinas de estado ayudarán a obtener la lista necesaria.
- Por supuesto, se pueden llamar otras máquinas de estado dentro de los functores, además, puede llamarse a sí mismo, pero con un objeto diferente o con el mismo objeto, pero después de un cambio de estado (es decir, en un post-functor). Esto a veces es útil para organizar transiciones en cascada bajo condiciones cambiantes "repentinas" del cliente;)
Conclusión
A pesar de las muchas tareas que son ideales para el uso de máquinas de estado, los programadores web las usan relativamente raramente. Las mismas decisiones que vi me parecieron monstruosas.
Espero que la solución propuesta ayude a alguien a ahorrar tiempo en la implementación.