Invariantes de clase. Ejemplo práctico con PHP.
Básicamente, una clase tiene dos características fundamentales que la definen: estado y comportamiento. El estado viene definido por la información de sus propiedades y el comportamiento viene definido por las acciones, definidas en sus métodos, que puede realizar sobre su estado, es decir el estado son los atributos y el comportamiento los métodos que utilizarán dichos atributos.
Los invariantes de clase son propiedades globales de una clase que tienen que ser conservadas por todas las rutinas que la componen. Dichas propiedades capturan las propiedades, valga la redundancia, semánticas más profundas y las restricciones de integridad que caracterizan a la clase. Por lo tanto, los invariantes de clase solo pueden involucrar al estado de dicha clase.

También podemos definir un invariante de clase como una aserción que expresa restricciones de consistencia generales que se aplican a todos los objetos que se instancien de dicha clase.
¿Quién debe conservar los invariantes?
Como se ha dicho anteriormente, el comportamiento de una clase lo definen aquellos métodos capaces de modificar el estado de una clase, por lo tanto, son los métodos de la propia clase los que deben ‘defender’ los invariantes que contenga pero, ¿todas las rutinas de una clase están implicadas en esta defensa? NO.
Únicamente se deben satisfacer los invariantes en las llamadas a métodos públicos de la clase de forma que dichas llamadas deben partir de un estado que satisfagan los invariantes de clase y debe terminar con un estado que también los satisfagan. Las ejecución de métodos privados de la misma clase pueden ‘saltarse’ esta norma.
¿Puede variar el invariante? Puede parecer una incongruencia pero si, puede variar. El invariante no tiene que satisfacerse en todo momento. En un caso general, es perfectamente viable que un método para alcanzar su objetivo pueda perder de forma temporal el invariante pero siempre y cuando finalice con el invariante cumplido, es decir, antes de la llamada el invariante se debe cumplir y después de la salida también…durante la ejecución del mismo puede no satisfacerse.
Diseño por contrato
De manera muy resumida es una metodología que trata de diseñar software definiendo de manera explícita las relaciones existentes entre todos los módulos, clases y rutinas que lo conforman.
La clave del diseño por contrato es que ve las relaciones entre clases y sus clientes como un acuerdo formal, que expresa los derechos y obligaciones de cada parte, es decir, define de manera explícita las relaciones entre las clases, métodos, módulos y los llamadores de las mismas. Cada elemento del software tiene el objetivo de satisfacer las necesidades de otros elementos a través de contratos.
Un contrato establece los tipos de datos de entrada y salida con los que se trabaja, las condiciones que deben cumplirse antes de la llamada (precondiciones), las condiciones de salida (postcondiciones) y las condiciones que deben cumplirse antes y después (invariantes). Sobre las precondiciones y las poscondiciones ya escribiré más adelante en otro post, porque bien las merecen.
¿Qué nos aporta el Diseño por contrato?
La ventaja más significativa que obtenemos al desarrollar software por contrato es conseguir un nivel de confianza óptimo y un nivel alto de corrección en el software que desarrollemos, ya que esto solo puede lograrse a través de una especificación precisa de las afirmaciones y responsabilidades de cada módulo que componen el sistema de software.
Ahora, podríamos preguntarnos ¿cuándo un programa es correcto o incorrecto? La respuesta no es tan sencilla o inmediata. No podríamos decir que es correcto o incorrecto per se, sino que estrictamente hablando, un programa es correcto cuando cumple las especificaciones descritas que le correspondan, por lo tanto, si es correcto, o no, significa si es consistente, o no, con sus especificaciones.
La forma más sencilla y directa de validar las especificaciones definidas y de comprobar si se cumple o no el diseño por contrato es por medio de aserciones que ayuden a establecer la corrección del software.
Pero el Diseño por Contrato nos aporta además otras ventajas:
- Menos errores en el código porque tenemos una mayor especificación de todos los componentes y la forma en la que se usan.
- Precisamente el punto anterior nos deja que vamos a tener un sistema para detectar errores más específico.
- Una forma práctica de documentar el código con especificaciones del mundo real o del problema del negocio que estemos resolviendo.
Diseño por contrato en PHP. Ejemplo con invariantes de clase.
PHP no soporta de manera nativa invariantes de clase, precondiciones y postcondiciones. No obstante existen algunos componentes que realizan esta tarea y nos podemos ayudar de ellos para implementar estas características. Uno de ellos es PhpDeal, que nos va a ayudar a definir precondiciones, postcondiciones para los métodos e invariantes para nuestras clases.
Yo mismo he implementado un ejemplo sencillo para probar el uso de invariantes de clase y cual sería el efecto de “romper” el contrato que definen. El código está compartido en Github y se puede ver en el siguiente enlace:
(*) PhpDeal
Como digo, PhpDeal es un framework para Diseño por Contrato en PHP. Funciona añadiendo mediante anotaciones, los invariantes de clase en la propia definición de la clase, de forma que nos queda algo similar a lo siguiente:
use PhpDeal\Annotation as Contract;
/**
* Product Class
*
* @Contract\Invariant("!empty($this->name)")
* @Contract\Invariant("is_numeric($this->price)")
* @Contract\Invariant("$this->price >= 0")
*/
class Product
{
/**
* @var string
*/
private $name = '';
/**
* @var float
*/
private $price = 0.0; //...
//...
Como se puede ver en el código anterior, estamos definiendo el invariante de clase con tres restricciones sobre las propiedades name y price que conforman el estado de nuestra clase Product.
Con la siguiente restricción estamos indicando que el atributo ‘name’ nunca puede ser una cadena vacía.
@Contract\Invariant("!empty($this->name)")
Con la siguiente restricción indicamos que ‘price’ es un atributo numérico.
@Contract\Invariant("is_numeric($this->price)")
Y con la siguiente restricción estamos indicando que ‘price’ nunca puede ser un valor numérico negativo.
@Contract\Invariant("$this->price >= 0")
Como digo, estas tres restricciones conforman el invariante de nuestra clase Product, lo que significa que la ejecución de todos sus métodos siempre deben satisfacerlas.
Pero, ¿que ocurre si no satisfacemos el invariante de clase? imaginemos que tenemos el siguiente código:
$product = new DemoContract\Product('Ball', 0);
$product->setPrice(-1);
Podemos observar que estamos estableciendo un precio menor que cero, en este caso no estamos conservando el invariante de clase sobre el atributo price ya que definimos que este tenía que tener un valor igual o mayor a cero. La creación del objeto Product estará generando un Fatal Error con una excepción del tipo DomainException, que nos indica también que hemos realizado una ‘violación del contrato’.
( ! ) Fatal error: Uncaught DomainException: Invalid return value received from the assertion body, only boolean or void can be returned in /var/www/html/contract/Product.php on line 38( ! ) PhpDeal\Exception\ContractViolation: Contract $this->price >= 0 violated for DemoContract\Product->setPrice in /var/www/html/contract/Product.php on line 38
Otro ejemplo. A continuación, tenemos un método notSatisfyInvariant() dentro de nuestra clase Product que cuando es llamado si se satisface el invariante de clase pero luego en su propia ejecución deja de satisfacerse al finalizar el método, dejando ‘name’ vacío, por lo que volvemos a generar en la ejecución una excepción DomainException:
/**
* this method that does not satisfy the class invariant
*/
public function notSatisfyInvariant()
{
$this->price += 1;
$this->name = 'other name';
$this->price = 7;
$this->name= '';
$this->price = 9*2;
}
Y la llamada al método por ejemplo podría ser así
$product = new DemoContract\Product('Ball', 0);
$product->setPrice(2);
// Hasta aquí se satisface el invariante de clase, pero una vez llamamos a la
$product->notSatisfyInvariant();
El resultado de la ejecución de notSatisfyInvariant() es nuevamente otra excepción indicando que no satisfacemos la condición para que ‘name’ tenga un valor no vacío:
Fatal error: Uncaught DomainException: Invalid return value received from the assertion body, only boolean or void can be returned in /var/www/html/contract/Product.php on line 57( ! ) PhpDeal\Exception\ContractViolation: Contract !empty($this->name) violated for DemoContract\Product->breakInvariantToExit in /var/www/html/contract/Product.php on line 57
Y, como último ejemplo, anteriormente comenté que los métodos de la clase pueden dejar de satisfacer un invariante de clase de manera temporal siempre y cuando al finalizar su ejecución si lo satisfagan. El siguiente método de nuestra clase Product deja temporalmente el atributo ‘name’ vacío, pero sigue satisfaciendo el invariante de clase porque al final, le asigna un valor. Por lo tanto la ejecución de este método es correcta.
/**
* this method that does satisfy the class invariant
*/
public function satisfyInvariant()
{
$this->price += 1;
$this->name = 'foo';
$this->price = 7;
$this->name= '';
$this->price = 9*2;
$this->name = 'foo bar';
}
Y hasta aquí una introducción a los invariantes de clase, diseño por contrato y un ejemplo en PHP. Para próximas entradas en el blog que completen este artículo hablaré de aserciones, precondiciones y postcondiciones.
Como siempre cualquier duda, sugerencia o corrección será muy bienvenida.
¡Chimpún!
Nota
(*) La instalación de php-deal tal y como se indica en su documentación no funciona correctamente. Al array de configuración hay que añadirle al menos la siguiente configuración marcada en negrita:
ContractApplication::getInstance()->init(array(
'debug' => true,
'appDir' => __DIR__,
'cacheDir' => __DIR__.'/cache/',
'includePaths' => [__DIR__],
'excludePaths' => [__DIR__.'/vendor/']
));
De no hacerlo, nos encontraremos con un error similar a este: Fatal error: Uncaught Error: Class ‘Go\ParserReflection\Instrument\PathResolver’ not found
Bibliografía
- Construcción de software orientado a objetos — Bertrand Meyer — ISBN: 978–84–8322–040–5
- https://es.wikipedia.org/wiki/Dise%C3%B1o_por_contrato
- Rossel, Gerardo y Manna, Andrea (2003) “Diseño por Contratos: construyendo software confiable” [en línea]. Revista Digital Universitaria. 01 octubre 2003, vol. 5 no. 5 <http://www.revista.unam.mx/vol.4/num5/art11/art11.htm> [Consulta: 01de octubre de 2003].
- http://blog.koalite.com/2014/01/diseno-por-contrato/
- La imagen del ejemplo del invariante en Java ha sido obtenida de la página de la Wikipedia: https://en.wikipedia.org/wiki/Class_invariant