Programación por contrato

INTRODUCCIÓN

Las ideas del Diseño por Contratos (Bertrand, 1992, 1997 y Mitchell, 2002 ), tienen sus raíces en los métodos formales para la construcción de software, pero mantienen una visión más pragmática. Requieren muy poco esfuerzo extra pero generan software mucho más confiable. La importancia de comprender el diseño por contratos, queda clara en la afirmación del pionero de la ingeniería de software Tom DeMarco: “Yo creo que el uso de contratos a la Eiffel entre módulos es la no-práctica más importante en el mundo del software hoy. Por esto yo quiero decir que no hay ninguna otra práctica siendo impulsada actualmente que tenga mayor capacidad de mejorar la calidad del software producido.” (DeMarco 1997).

El diseño por contratos puede ser visto como la aplicación a la construcción de software de los contratos que rigen los asuntos de las personas. Cuando dos personas establecen un contrato se desprenden, de éste, las obligaciones y beneficios de cada una. Este tipo de contratos en software especifican, en forma no ambigua, las relaciones entre las rutinas y los llamadores de las mismas. Podemos ver un sistema como un conjunto de elementos de software interrelacionados, donde cada uno de los elementos tiene un objetivo con vistas a satisfacer las necesidades de los otros. Dichos objetivos son los contratos. Los contratos deben cumplir, por lo menos, con dos propiedades: ser explícitos y formar parte del elemento de software en sí mismo.

El Diseño por Contratos da una visión de la construcción de sistemas como un conjunto de elementos de software cooperando entre sí. Los elementos juegan en determinados momentos alguno de los dos roles principales proveedores o clientes. La cooperación establece claramente obligaciones y beneficios, siendo la especificación de estas obligaciones y beneficios los contratos. Un contrato entre dos partes protege a ambas. Por un lado protege al cliente por especificar cuanto debe ser hecho y por el otro al proveedor por especificar el mínimo servicio aceptable.

Claramente, las obligaciones de una parte son los beneficios de la otra parte. Una de las reglas más importantes para el diseño de contratos es el principio de No Cláusulas Ocultas. Es decir que, si el contrato establece que el cliente debe cumplir con determinado requisito, ese requisito es todo lo que el cliente debe cumplir, o sea, no hay sorpresas. Tómese en cuenta que existen regulaciones externas que forman parte del contexto más general donde se desenvuelve el contrato (el paquete no puede contener una bomba). En el software, estas regulaciones externas, como se verá luego, toman la forma de invariantes de clase.

Dado que los ejemplos que se mostrarán son realizados en el lenguaje Eiffel se usará la terminología del mismo (Meyer,1992b): las clases están compuestas de características ( features ) las cuales pueden ser rutinas (procedimientos o funciones) o atributos. En otros contextos la terminología cambia (funciones miembro y variables miembro en C++, métodos y variables de instancia en Smalltalk).

 

ASERCIONES PARTE 1

 

Los contratos de software se especifican mediante la utilización de expresiones lógicas ( más una forma de especificar el valor anterior a una computación: old ) denominadas aserciones. Existen diferentes tipos de aserciones. En el Diseño por Contratos se utilizan tres tipos de aserciones:

• Precondiciones
• Poscondiciones
• Invariante de Clase.

Antes de explicar las aserciones y su rol en el Diseño por Contratos, se detallará el concepto de tripleta de Hoare (Hoare 1969) la cual es una notación matemática que viene de la validación formal de programas. Sea A alguna computación y P, Q aserciones, entonces la siguiente expresión:

{ P } A { Q }

representa lo que se llama fórmula de corrección. La semántica de dicha fórmula es la siguiente: cualquier ejecución de A que comience en un estado en el cual se cumple P dará como resultado un estado en el cual se cumple Q. Por ejemplo

{ x > 10 } x := x /2 { x >= 5}

O sea que si se parte de un estado en el cual x es mayor que 10 y luego se aplica la computación x := x/2 entonces como resultado x será mayor o igual a 5. A la aserción P se la llama precondición y a Q se la llama poscondición. En el ejemplo la precondición es x > 10 y la poscondición x >=5.

Se puede entonces definir el principio de no-redundancia mencionado anteriormente: Bajo ninguna circunstancia el cuerpo de una rutina deberá chequear el cumplimiento de la precondición. La precondición compromete al cliente, ya que define las condiciones por las cuales una llamada a la rutina es válida. Las poscondiciones comprometen a la clase (donde se implementa la rutina) ya que establecen las obligaciones de la rutina. Es muy importante notar que la precondición y la poscondición que definen el contrato forman parte del elemento de software en sí.

 

ASERCIONES. PARTE 2

 

Es posible establecer aserciones más fuertes que otras. Ahora bien ¿qué significa que una aserción es más fuerte que otra? Se puede definir, que dadas dos aserciones P y Q, P es más fuerte o igual que Q si P implica Q. El concepto de fortificar o debilitar aserciones es usado en la herencia cuando es necesario redefinir rutinas. En Eiffel (y en general en las herramientas disponibles para otros lenguajes) el lenguaje para soportar aserciones tiene algunas diferencias con el cálculo de predicados: no tiene cuantificadores (aunque el concepto de agentes provee un mecanismo para especificarlos) , soporta llamadas a funciones y además existe la palabra reservada old (usada en las poscondiciones) para indicar el valor anterior a la ejecución de la rutina de algún elemento. Supóngase una clase que representa una cuenta bancaria que cuenta con una rutina depositar que recibe un importe como parámetro y agrega ese importe al saldo:

depositar (importe: REAL) is

— Depositar sum en la cuenta

require

cantidad_valida: importe >= 0

do

agregar_deposito(importe)

ensure

incremeto_balance: balance = old balance + sum importe

end

La expresión old balance en este caso, indica el valor anterior a la ejecución de la rutina del balance. Es importante ver la diferencia existente entre la sentencia

agregar_deposito(sum)

y la expresión

balance = old balance + sum

 

El primer caso es prescriptivo y operacional, mientras el segundo es descriptivo y denotacional. Uno representa el cómo y otro representa el qué.

Como consecuencia directa de que las precondiciones forman parte de la interfaz de una rutina, surge la siguiente regla: Toda función que aparezca en la precondición de una rutina r debe estar disponible a todo cliente al cual esté disponible dicha rutina r. Si esto no fuera así, podría ser imposible para el cliente garantizar que se cumple la precondición antes de llamar a la rutina.

 

INVARIANTES DE CLASE

 

Los invariantes de clase sirven para expresar propiedades globales de las instancias de una clase, mientras que las pre y poscondiciones describen las propiedades de rutinas particulares. Desde el punto de vista de la metáfora de los contratos, los invariantes de clase establecen regulaciones generales aplicables a todos los contratos. Los invariantes de clase provienen del concepto de invariante de datos de Hoare (Hoare, 1972).

Por ejemplo la clase ARRAY en Eiffel cuenta (entre otros) con el siguiente invariante de clase:

invariant

non_negative_count: count >= 0

Este invariante establece que un ARRAY no puede tener una cantidad de elementos menor que cero. Similarmente si se tiene una clase PERSONA se puede establecer como invariante que la edad debe ser mayor que cero y además que si es casada entonces el cónyuge tiene como cónyuge a sí mismo. En Eiffel se vería de la siguiente forma:

invariant

edad_no_negativa: edad >= 0

matrimonio _ correcto: soltero or else conyuge.conyuge = Current

En el invariante con etiqueta matrimonio _ correcto se establece que, o bien se es soltero o sino el cónyuge tiene como cónyuge a Current (o sea a sí mismo).

Los invariantes de clase deben satisfacerse en dos momentos: luego de la creación del objeto y luego de la llamada a una rutina por un cliente.

 

 

CORRECCIÓN DE UNA CLASE

Ahora se pueden establecer las condiciones de corrección de una clase. Para ello se usará la notación de la tripleta de Hoare. Una clase es correcta con respecto a sus aserciones sí y sólo si se cumplen las siguientes condiciones (Meyer,1997):

• Para toda rutina de creación rc: {prerc } dorc { postrc and INV}

• Para toda rutina exportada r: {INV and prer } dor { postr and INV}

Donde prea y posta representan la pre y poscondición respectivamente de una rutina a, INV es el invariante de la clase donde se implementa a y doa representa la ejecución de a.

De aquí se deduce que el rol principal del procedimiento de creación (algunas veces llamado constructor) es establecer el invariante de clase.

 

CONTRATOS Y AFIRMACIÓN DE CALIDAD.

¿Qué significa la violación de un contrato?. La violación de un contrato representa un defecto en el software. Dependiendo del tipo de aserción es la ubicación del defecto. Si se viola una precondición, entonces existe un defecto en el cliente. Si se viola la poscondición o el invariante de clase, existe un defecto en el proveedor. Es importante que se puedan monitorear las aserciones en tiempo de ejecución con propósitos de testeo, afirmación de calidad (las actividades de QA se basan en lo previsto por los contratos) y depuración. Es decir, no son solamente un mecanismo de especificación y documentación (en Eiffel existe una forma de visualizar una clase llamada contract form que muestra la clase con sus rutinas publicadas con pre y poscondiciones y el invariante de clase), sino que además cumplen un rol importante durante todas las etapas del desarrollo de software.

Es importante que el desarrollador tenga la opción de elegir en tiempo de compilación que aserciones monitorear. Luego de la depuración y el testeo, es posible generar el sistema de producción con las aserciones sin habilitar (evitando el chequeo en tiempo de ejecución). En general es recomendable mantener habilitado el chequeo de las precondiciones. Meyer hace una detallada explicación de esto (Meyer,1997). La violación de una aserción en tiempo de ejecución produce un excepción que puede ser posteriormente manejada.

Los contratos de software sirven como vehículo de comunicación entre desarrolladores, entre gerentes y desarrolladores, para realizar manuales, etc. Pero además son una de las herramientas más importantes para realizar reuso de software. El contrato permite conocer qué se está reusando y bajo qué condiciones. Un interesantísimo artículo de Jézéquel y Meyer dan cuenta de como la utilización de contratos podría haber evitado pérdidas millonarias en un proyecto de software (Jézéquel, 1997).

 

CONTRATOS Y HERENCIA

 

En este apartado, se discutirá como afecta la herencia a los contratos. Al generar una subclase de una clase existente, es posible que sea necesario redefinir algunas características. Surgen naturalmente las preguntas ¿qué pasa con los contratos? ¿qué ocurre con el invariante de clase?

Se analizará primero el caso del invariante de clase. Se rige por la regla de la herencia del invariante (Invariant inheritance rule) (Meyer,1997):

• La propiedad invariante de una clase es la conjunción lógica (and) de las aserciones que aparecen en su cláusula invariante y de las propiedades invariantes de sus padres, si hay alguno.

Se puede ver que esta regla es recursiva comprendiendo así a los invariantes de todos los ancestros. La regla anterior surge naturalmente de la propia naturaleza de la herencia. Las instancias de una clase son también instancias de los ancestros. Se tiene la siguiente jerarquía de clases:

 

Los triángulos y los rectángulos son a su vez polígonos. Los polígonos tienen por lo menos tres lados. Esta restricción se puede expresar como un invariante de clase : cant_lados >= 3. Para que los triángulos y rectángulos sean polígonos, deben respetar dicha restricción. Dada la regla de la herencia del invariante, ambos tienen como propiedad invariante cant_lados >= 3.

Ahora bien, como los triángulos tienen tres lados y los rectángulos cuatro lados, se puede agregar a cada una de estas clases, una cláusula invariante para expresar dichas restricciones.

 

 

La regla de herencia del invariante permite razonar acerca de la corrección de las clases y de la jerarquía de herencia. Si hubiera una subclase de polígono con una cláusula invariante indicando, por ejemplo, cant_lados = 2, dicha clase sería inválida ya que la propiedad invariante sería:

cant_lados >= 3 and cant_lados = 2

Esto indica que se está usando mal la herencia ya que, o bien, los polígonos pueden tener menos de tres lados o la nueva clase no es subclase de polígono (no es un polígono).

 

PRINCIPIOS

 

En (Mitchel, 2002) R.Mitchell y J.McLim se enumeran una serie de principios a seguir cuando se diseñan clases utilizando contratos de software. Ellos realizan una extensa discusión y ejemplificación de dichos principios. En este artículo se enumeran los mismos:

 

Principio 1: Separar consultas de comandos. Este principio también es explicado detalladamente en (Meyer,1997). La idea es que las rutinas de una clase deben ser (en lo posible) o comandos o consultas pero no ambas cosas. Las consultas devuelven un valor (ej. funciones) y los comandos pueden cambiar el estado interno del objeto.

 

Principio 2: Separar consultas básicas de consultas derivadas. La intención es conseguir un conjunto de especificación formado por consultas que denominamos básicas, de tal forma que el resto de las consultas puedan derivarse de las básicas.

 

Principio 3: Para cada consulta derivada escribir una poscondición especificando su resultado en términos de una o más consultas básicas. Esto permite conocer el valor de las consultas derivadas conociendo el valor de las consultas básicas. Idealmente, sólo el conjunto minimal de especificación tiene la obligación de ser exportado públicamente.

 

Principio 4: Para cada comando escribir una precondición que especifique el valor de cada consulta básica. Dado que el resultado de todas las consultas puede visualizarse a través de las consultas básicas, con este principio se garantiza el total conocimiento de los efectos visibles de cada comando.

 

Principio 5: Para toda consulta o comando decidir una precondición adecuada. Este principio se auto explica ya que permite definir claramente el contrato de cada rutina.

 

Principio 6: Escribir el invariante para definir propiedades de los objetos. La idea aquí es ayudar al lector a construir un modelo conceptual apropiado de la clase.

Junto a estos principios los autores también explican una serie de guías y clases auxiliares para escribir contratos. Los principios enumerados junto al estudio de bibliotecas de clases (ej. EiffelBase) son una importante fuente de aprendizaje para el diseño de contratos.

 

CONCLUSIONES

 

Requieren muy poco esfuerzo extra pero generan software mucho más confiable.

 

El Diseño por Contratos da una visión de la construcción de sistemas como un conjunto de elementos de software cooperando entre sí. Los elementos juegan en determinados momentos alguno de los dos roles principales proveedores o clientes.

 

 

BIBLIOGRAFÍA

http://www.revista.unam.mx/vol.4/num5/art11/conl.htm

 

About omaracostacasas

ING SOFTWARE
This entry was posted in Ingenieria de Software. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s