Diseño por Contrato

Mencioné el Diseño por Contrato (Design by Contract) al hablar sobre Securizar, Autorizar, Validar y también se podía ver someramente cómo suelo aplicarlo en el modelo de ejemplo que usé al explicar cómo usar el patrón builder al escribir tests unitarios, pero creo que es un tema lo bastante interesante como para dedicarle su propio post.

¿Qué es el Diseño por Contrato?

Podéis encontrar una definición más completa de lo que es el Diseño por Contrato en la Wikipedia, pero a grandes rasgos podríamos decir que el Diseño por Contrato consiste en definir de manera formal el contrato expuesto por cada parte de un sistema. Y digo «parte» porque realmente el diseño por contrato lo podemos aplicar a distintos niveles: clases, módulos, métodos, etc.

El «contrato» marca varias cosas, incluyendo los tipos de datos sobre los que se trabaja, el tipo de resultado que se devuelve, las condiciones que deben cumplirse antes de poder ejecutar una operación (precondiciones), las condiciones que se cumplirán después (postcondiciones) y las condiciones que se cumplirán tanto antes como después (invariantes).

Por ejemplo, si tenemos un método que elimina un elemento de una lista que no contiene repeticiones, las precondiciones serían que la lista no es null, que el elemento no es null y que la lista contiene el elemento, y la postcondición sería que después de ejecutar el método la lista ya no contiene el elemento.

Las condiciones (ya sean pre, post o invariantes) no tienen por qué limitarse a los parámetros de entrada y salida, sino que pueden referirse al estado de otras variables. Por ejemplo, si tenemos un método que confirma un pedido de una tienda online, podríamos comprobar que cuando se llama a ese método al menos se ha añadido una línea al pedido.

¿Cómo se utiliza el Diseño por Contrato?

La manera más sencilla de aplicar Diseño por Contrato es utilizando aserciones en el código que generen un error en caso de que no se cumpla alguna condición. El nivel de detalle de esas aserciones depende de cada escenario, pero también del lenguaje de programación, por ejemplo, si estamos usando un lenguaje de programación con tipado estático, no será necesario realizar aserciones sobre los tipos de datos de entrada puesto que el compilador ya nos garantizará que son correctos; sin embargo, si usamos un lenguaje con tipos dinámicos, puede ser recomendable asegurarnos de que los datos de entrada son de los tipos adecuados.

Para realizar las aserciones, podemos lanzar excepciones o usar alguna librería más elaborada. Personalmente, suelo utilizar una librería muy simple que permite escribir las aserciones de forma cómoda y decidir, mediante flags de compilación, cuáles de estas aserciones queremos mantener en producción. Existen opciones más elaboradas, como Code Contracts que incluyen la posibilidad de realizar ciertos análisis estaticos mediante herramientas integradas en Visual Studio y detectar así más problemas en tiempo de compilación. En cualquier caso, y como he dicho muchas veces, lo importante no es el framework que utilices, sino que entiendas lo que te puede aportar esta técnica.

Volviendo al ejemplo (trivial) que veíamos antes del método que elimina un elemento de una lista que no contiene repeticiones, podríamos escribirlo así usando esta librería:

public string[] RemoveItem(string[] items, string item)
{
  Check.Require(items != null, "items cannot be null");
  Check.Require(items.Any(x => x == item), "items must contain item");
  Check.Invariant(items.GroupBy(x => x).All(x => x.Count() == 1), "items cannot contains duplicates");

  var result = items.Where(x => x != item).ToArray();

  Check.Ensure(items.All(x => x != item), "item must be removed");
  Check.Invariant(items.GroupBy(x => x).All(x => x.Count() == 1), "items cannot contains duplicates");
  return result;
} 

Normalmente suelo poner más enfásis en la validación de precondiciones e invariantes que en la validación de postcondiciones, principalmente porque muchas veces la postcondiciones de un método se convierte en la precondición del siguiente (el valor de retorno de un método lo usas como valor de entrada de otro), y al validar la precondición del siguiente método se suelen detectar posibles errores. Además, las postcondiciones son precisamente lo que se validan con los tests unitarios (a fin de cuentas, son el resultado de la ejecución), por lo que quedan cubiertas a través de ellos.

Independientemente del mecanismo que utilices para implementar Diseño por Contrato, es importante que tengas en cuenta que las excepciones que se lanzan por violaciones de contrato nunca deben ser capturadas. Una violación de contrato es un bug en la aplicación, no una circunstancia anómala de la que uno debe intentar recuperarse.

¿Qué aporta el Diseño por Contrato?

Al aplicar Diseño por Contrato en nuestras aplicaciones conseguidos dos cosas muy importantes: aumentamos la solidez de la aplicación y mejoramos su documentación.

La solidez de la aplicación aumenta porque cada vez que hacemos algo indebido, se lanzará una excepción y nos ayudará a detectar el problema rápidamente (fail fast). Además detectar estos problemas pronto evita que se propaguen a otras partes del sistema donde puedan causar más daño y ser más complicados de diagnosticar. Por ejemplo, podemos evitar que se guarde en la base de datos un valor que no tiene sentido y descubrirlo 4 meses después al sacar un informe en el que nada cuadra.

La mejora de documentación viene porque hacemos explícitas aquellas cosas que estamos suponiendo ciertas antes (o después) de ejecutar algo. En lugar de poner un comentario diciendo «el código de error devuelto será un valor negativo», podemos poner un Check.Require(result < 0) y estaremos seguros de que efectivamente será negativo y, además, al contrario que el comentario, no podrá quedar desactualizado porque generaría errores en al aplicación y los detectaríamos rápido.

Rendimiento

Cuando aplicamos Diseño por Contrato y empezamos a incluir aserciones en nuestras aplicaciones es necesario pensar en las implicaciones que tienen a nivel de rendimiento. Si cada vez que invocamos un método realizamos una serie de validaciones, estamos ejecutando más código del «estrictamente necesario» para completar la tarea que tenemos que realizar.

Que esto sea un problema o no depende del tipo de aplicación y de los cálculos necesarios para realizar las aserciones. Algunas librerías permiten seleccionar qué tipos de validaciones queremos mantener en cada momento, por ejemplo dejando todas activas cuando compilamos en debug y dejando sólo activas las validaciones de precondiciones en producción. En cualquier caso, es algo que debes tener en cuenta y actuar en consecuencia.

Conclusiones

El Diseño por Contrato es una técnica muy util a la hora de desarrolla que nos permite mejorar la calidad del software gracias a la detección de posibles errores. Además, nos facilita la depuracion porque podemos detectar el error en el momento y lugar en que se produce, evitando que se propague a otras áreas del sistema.

Por otra parte, ayuda a mejorar la documentación del código al hacer explícitas las expectativas que tenemos sobre el estado del sistema en determinados momentos.

Existen distintas librerías para aplicar Diseño por Contrato, pero incluso el mecanismo más simple (un par de métodos estáticos al estilo de Debug.Assert) puede servirnos para obtener grandes beneficios.

28 comentarios en “Diseño por Contrato

  1. Estando en general de acuerdo con tu exposicion y aplicacion del diseño por contrato no me acaba de encajar lo de utilizarlo mas para las precondiciones que las postcondiciones (en lo de los invariantes no hay duda): en principio el diseño del codigo deberia ir en el sentido de relajar las precondiciones y reforzar las postcondiciones, para conseguir que el codigo sea los mas reutilizable y flexible posible. Esto es porque cuanto menos exijamos al codigo cliente menos dependera este de nuestro diseño/implementacion. Sobre todo si el metodo es de un api publica y no controlamos como va a gestionar los posibles errores ese codigo cliente. En el mismo sentido, cuanto mas estrictos seamos en la salida menos posibilidades habra de romper el codigo del cliente.
    Ya sabemos que la cobertura de los tests unitarios es «discreta» o «analitica», solo cubrimos los casos concretos que probemos, a no ser que usemos una libreria como QuickCheck.
    En todo caso tanto asegurar la salida con contratos, como con tests unitarios exhaustivos como con tests basados en propiedades supone un coste que es correlativo a su utilidad: al final el codigo que asegura la salida tiende a ser casi igual de complejo que el codigo que la genera.

  2. Hay, sin embargo, otra opcion: usar un sistema de tipado lo suficientemente potente (hasta llegar a tipos dependientes) para que, en tiempo de compilacion, asegure lo mas posible la salida sin tener que escribir una linea de codigo de validacion.
    O sea, usar un sistema de validacion logico generico, independiente del codigo concreto que se valide para que con las indicaciones declarativas (y no codigo) minimas posibles gracias a la inferencia, conseguir el mayor grado de validacion posible.
    Y para eso hasta haskell se queda corto a veces, es tiempo de mirar hacia idris o coq.

  3. «para conseguir que el codigo sea los mas reutilizable y flexible posible»

    Si tienes una especificación (y el contrato lo es), no hay margen para la «flexibilización» (o la especificación está mal o es incompleta).

    «Hay, sin embargo, otra opcion: usar un sistema de tipado lo suficientemente potente»

    Elemental querido Watson (¿tu programas en Clojure no? XD XD).

    PD: a mí ésto de «Diseño por contrato» me suena a «TDD», poner un nombre bonito a algo que se ha hecho desde siempre.

  4. @josejuan

    «Si tienes una especificación (y el contrato lo es), no hay margen para la “flexibilización” (o la especificación está mal o es incompleta).»

    Una especificación puede ser flexible en determinados aspectos. Por ejemplo, el contrato puede marcar que devuelvo un IList<T> pero no indicar la implementación concreta.

  5. Estoy de acuerdo contigo en la idea de que si “se es flexible en lo que se acepta y rígido en lo que se devuelve”, tendría más sentido poner énfasis en la validación de postcondiciones, pero en la vida real no lo suelo hacer (aunque sí trate te aplicar la idea al implementar el método).

    El motivo principal es que las postcondiciones las acabo validando más a través de tests (especialmente unitarios) lo que me permite escribir comprobaciones “más elaboradas” sin meter mucho ruido en el código de la función (aunque a cambio alejo la comprobación del código, nada es perfecto).

    En cuanto al tema de tipos, lo dejo caer en el post de pasada pero he preferido no profundizar mucho porque no lo domino. Es verdad que cuando he visto algunas de las cosas que se pueden hacer con sistemas de tipos medianamente avanzados (con clojure/core.typed, sin ir más lejos), y me ha parecido una técnica muy potente, aunque me da un poco de miedo el exceso de rigidez y acoplamiento que puede implicar entre los distintos componentes del sistema.

  6. Pues no, en ese caso la especificación es incompleta.

    Acabas de poner un ejemplo en el que el contrato NO ES FORMAL (cuando has dicho que debe serlo).

    O se admiten entradas genéricas (en cuyo caso tu código debe admitir entradas genéricas) o se especifica una entrada concreta (en cuyo caso tu código debe admitir ÚNICAMENTE esa entrada concreta).

    Yo no he puesto las normas, únicamente destado las incoherencias :)

  7. Bueno, segun lo que aprendi hace poco de semantica axiomatica una especificacion (definida como precondicion+invariante+postcondicion) siempre puede a posteriori relajar las precondiciones y reforzar las postcondiciones y mantenerse igual de correcta. Lo contrario sin embargo es falso.

    Clojure tiene un nuevo y flamante sistema de tipado estatico (no en tiempo de compilacion pero si previo a la ejecucion) que puedes usar opcionalmente. Tambien tiene (casi desde el principio) la posibilidad de anotar las funciones con tipos para conseguir mayor rendimiento. Curiosamente ambos usos del tipado esta descomplectadas :-P

    El diseño por contrato es bastante «antiguo» (Bertrand Meyer, Eiffel, 1986) , y esta basado precisamente en la sematica axiomatica y el trabajo de Hoare y otros (de finales de los 60)

  8. Me temo que la validacion del codigo, se haga como se haga, añade ruido y rigidez a un sistema de software. De hecho creo que los tests unitarios lo hacen casi mas que las otras:
    * Los tests son tambien codigo que hay que cuidar y mantener en si mismo (incluido todo el rollo patatero de los mocks, doubles, spies, dopplegangers™ y demas)
    * Los tests es codigo que tiene bugs, que a veces cuesta mas arreglar que el codigo de produccion.
    * Cuando cambias la implementacion (a veces, segun lo que hayas invertido en su escritura) y el diseño (casi siempre) tienes que cambiar los tests, proporcionalmente a lo radical que sea el cambio. Eso provoca que o bien no lo lleves a cabo o dejas los tests obsoletos o los eliminas/ignoras.

  9. Ahí, ahí, mezclando churras con merinas, para que terminemos de enternos todos XD XD XD

    «siempre puede a posteriori relajar las precondiciones y reforzar las postcondiciones y mantenerse igual de correcta»

    Pero vamos a ver, «igual de correcta» ¡para tu axiomática!, es decir, que seguirá cumpliendo las propiedades que se le hayan exigido.

    O he leído mal (que bien puede ser) o Juan ha hablado en general y desde un punto de vista de ingeniería del software:

    «definir de manera formal el contrato expuesto por cada parte de un sistema. Y digo “parte” porque realmente el diseño por contrato lo podemos aplicar a distintos niveles: clases, módulos, métodos, etc»

    Así, el primer ejemplo que se me pasó por la cabeza (malvado de mí) fué SOAP, que fija FORMALMENTE un contrato entre las partes (que tanto me gusta a mí y que tan poco a Juan).

    De ahí, que cuando dices «en principio el diseño del codigo deberia ir en el sentido de relajar las precondiciones y reforzar las postcondiciones» no esté de acuerdo, porque para mí, se debe cumplir que «precondiciones == postcondiciones», de ahí que diga que si «precondiciones < postcondiciones" es porque la especificación (precondición) está mal o es incompleta.

  10. SOAP no me disgusta tanto por la formalidad o falta de ella, sino porque me parece excesivamente complejo, pero ese es otro tema :-P

    En cuanto a lo del IList, a lo mejor no me he explicado bien, pero no veo por qué es una especificación incompleta. Impongo que el tipo de resultado debe cumplir determinadas condiciones (en este caso, implementar el interface IList), pero no impogo cómo las debe implementar (qué implementación concreta de IList usar).

    Si vamos por ese camino, toda especificación sería incompleta, excepto el propio código del método que sí definiría exactamente lo que está haciendo.

  11. emmm, ¿pero sigue siendo la especificacion igual de correcta o no? No entiendo como afecta a eso que hablemos de forma genera y desde el punto de vista de software o de otra…
    Tampoco entiendo los de pre==post o pre<post: las precondiciones se relajan en si mismas, eso no deberia afectar a las post, ¿no?
    Lo de "…deberia ir en el sentido de relajar…" es con el objetivo de que tu codigo sea mas flexible al ser mas dificil romper el codigo del cliente.

  12. «pero no impogo cómo las debe implementar»

    vale, quería pensar que lo habías escrito mal (pero no).

    Si tu especificación indica IList, no entiendo que tiene que ver aquí la implementación interna, para la especificación, el contenido es una caja negra y le da igual lo que hagas (mientras lo hagas bien).

    Así, no veo de que forma vas a relajar (generalizar) ese IList (o cualquier otro requisito de la especificación) mediante la implementación.

    (Ya que andamos con los gifs)
    Estoy espectante por saberlo

    http://computer-mind.com/uploads/empiezaquemeparto.gif

  13. @jneira, tú has dicho

    «relajar las precondiciones y reforzar las postcondiciones»

    si relajas la precondición ¡estás cambiando los requitos!, si refuerzas las postcondición ¡es porque la precondición no lo exigía y por tanto estaba mal o incompleta!

    Por eso, precondición == postcondición si queremos ser estrictos en el contrato.

    «de que tu codigo sea mas flexible al ser mas dificil romper el codigo del cliente»

    me ponga un ejemplo por favor (por no andar liando más el tema)

  14. Vale, poniendo de ejemplo lo de soap: vamos a imaginar que en la nueva version de la especificacion, ademas de aceptar un xml con un formato determinado *tambien* empieza a permitir mandar lo mismo en json (que cumpla todas las reglas de elementos y tipos del xml original).
    En ese caso has relajado las precondiciones ya que no solo aceptas el formato xml sino tambien en json. Todos los clientes previos pueden mantenerse sin ningun cambio y los clientes nuevos pueden elegir uno u otro.
    Otro ejemplo podria ser añadir un tag xml opcional a la especificacion del mensaje de entrada que permita afectar a la implementacion (haciendola mas eficiente por ejemplo) sin que la salida cambie en absoluto.

  15. «empieza a permitir mandar lo mismo en json»

    (las mayúsculas sólo enfatizan el concepto, no son gritos ¿eh?)

    ¡PUES MAL! (y debería ser más que obvio), porque tus especificaciones EXIGEN aceptar únicamente XML (no JSON).

    Entiendo perfectamente qué estás diciendo, pero debe quedar muy claro cuando discutimos algo el OBJETO de la discusión y, en este caso, es el CUMPLIMIENTO del contrato PREESTABLECIDO (por quien sea).

    Si estás aceptando JSON pues muy mal.

    OTRA COSA, será que **DENTRO DE TU CAJA NEGRA** preveas que puede escribirse tal o cual cosa de una forma más general. Dicho de otro modo, desde fuera, no debe poderse ver si haces una cosa u otra.

    Pero que quede claro, que las precondiciones y postcondiciones EN UN CONTRATO, deben coincidir; en otro caso, alquien no hace lo que debe.

  16. «Otro ejemplo podria ser añadir un tag xml opcional»

    ahí vuelves a hacer lo mismo (haces que aplicas un argumento a un objeto de discusión, pero lo haces en otro).

    Si haces eso de añadir el tag, o estás modificando el contrato (pero con el requisito de retrocompatibilidad) o no lo estás cumpliendo (porque admites cosas que no admite el contrato).

    Fíjate que curiosamente, si el contrato dice algo como «aquí los tags que se quieran indicar» ¡las precondiciones == postcondiciones!.

    Para mi es muy simple, sólo es seguir el contrato a rajatabla, si las piezas no encajan, alquien lo ha hecho mal (por eso me gustan tanto los contratos, ej. tipos, soap, …).

  17. Hola Juanma,
    Magnífico post!
    Un par de dudas sencillas para «relajar», no ya las precondiciones sino el nivelazo de los comentarios :)
    Aunque quizás fuera deseable aplicar DbC a todos los métodos con independencia de su visibilidad (por las ventajas que cuentas en tu post) puestos a elegir entiendo que donde NO pueden faltar es en la parte pública, al fin y al cabo es ahí donde la entrada podría estar menos controlada.
    Por otro lado, en lenguajes dinámicos como Javascript ¿Utilizas alguna librería en concreto? ¿Llegas a aplicar DbC en JS?
    También comentas que DbC se puede aplicar a cualquier nivel (clase, método, etc.) ¿Cómo aplicar DbC a una clase? A un método está claro (pre, pro e invariantes), pero a una clase? Entiendo que los tiros irán por comprobar el estado (propiedades) de la clase clase ¿Es correcto?
    Un saludo y mil gracias!

  18. Gracias Sergio por dejar un comentario sin gif animado :-P

    Con respecto a usar DbC en métodos privados, yo a veces lo uso para aclararme y dejar «por escrito» en qué me estoy basando al implementarlo.

    En cuanto a Javascript, se utiliza mucho para validar el tipo de los parámetros de entrada (ya que no hay un compilador). Es un uso muy básico de DbC, pero ayuda bastante. Algo similar a lo que aparece en este ejemplo: https://github.com/robconery/minty-cms/blob/master/models/article.js

    Por último, sobre aplicar DbC a nivel de clase, generalmente se trata de unir el estado de la clase con la operación a realizar. Por poner un ejemplo de tu mundo, si una vez que un albarán está facturado no se pueden añadir líneas, una precondición del método AddLine sería que Check.Require(this.status != Status.Invoiced).

    Para conseguir esto se puede aprovechar el sistema de tipos (mira este ejemplo con tipos fantasma en Java: http://gabrielsw.blogspot.com.es/2012/09/phantom-types-in-java.html), meter asserts o aplicar técnicas clásicas de OOP. Por ejemplo en el caso del albarán podrías aplicar un patrón state para impedir que se ejecuten operaciones que no están permitidas en el estado actual.

    La parte del invariante es un poco más complicada (o tediosa) de representar en C#, pero puedes utilizar IL-weaving (estilo PostSharp) para definir el invariante como un método e «inyectarlo» justo antes y después del cuerpo de cada método.

    Por último, y aun a riesgo de que vengan con antorchas a perseguirme por saltarme la «formalidad» que menciono en el post, a veces lo más importante no es cómo lo pongas en el código, o ni siquiera si lo pones en el código. Lo fundamental es que tengas claro cual es el invariante que debe mantener una clase y hagas todo lo posible por que se respete. Si puedes forzarlo en tiempo de compilación (con tipos) o en tiempo de ejecución (con asserts), mucho mejor, pero al menos deberías tener pensado que es lo que siempre se debe cumplir cuando te encuentras una instancia de esa clase.

    Espero no haberte liado más.

  19. Hola Juanma,
    Te has explicado de maravilla, gracias por contestar.
    Una duda chorra ¿El invariante de una clase entiendo que puede cambiar en función de su estado? Es decir, es «invariante» en relación al estado actual del objeto. Lógicamente, un albarán no entregado sí permite agregar líneas, mientras que uno no entregado, no lo permite. Perdona si la pregunta es muy tonta :)
    Por otro lado ¿Tienes alguna experiencia positiva/negativa con PostSharp o frameworks similares? A mí me gusta más la idea de una clase tipo «Check» pero por contra, meten mucho ruido, ¿no?
    Ya por último, ¿Dónde podría ampliar el tema de DbC? ¿Hay algún libro o referencia de «obligada» lectura? La verdad es que lo único que recuerdo de la uni es pre-condición, post-condición e invariante, poco más…
    Gracias!

  20. Si el invariante de la clase cambia es que es poco invariante. En el ejemplo que pones el invariante podrías verlo como:

    (no_entregado Y puedo_añadir_líneas) O (entregado Y no_puedo_añadir_líneas)

    Nunca he usado PostSharp para esto ni CodeContracts ni nada parecido, pero no creo que ahorren tanto ruido en cuanto a la validación de pre/post condiciones. El caso del invariante es especial porque con una clase tipo Check tienes que meterlo al principio y final de todos los métodos y es más incómodo, pero para el resto no hace falta mucho. Tal vez CodeContracts merezca la pena por la parte de análisis estático que hace el plugin de Visual Studio (al estilo de lo que ya te hace resharper).

    En cuanto a libros, está bien el de Bertrand Meyer (http://en.wikipedia.org/wiki/Object-Oriented_Software_Construction). Creo recordar que había una edición con ejemplos en C++ en lugar de Eiffel, pero no estoy seguro.

  21. Un invariante sobre un conjunto (ej. el conjunto de todas las instancias del objeto Factura) es una propiedad que nunca cambia y que siempre toma el mismo valor para cualquier elemento del conjunto (ej. x.NúmeroLineasFactura >= 0 para todo x € Facturas).

    Como bien dice Juan, poco invariante será si varía.

    Peeeeero (y aún a riesgo de ser confundido por el mismísimo Sauron), diré que el hecho de que una propiedad en un conjunto no sea un invariante, no impide que sea un invariante para otro conjunto.

    ¿Y ésto para que sirve?

    Pues eso depende de tí, pero si tienes una clase A en la que cierta propiedad toma los valores 1 y 2, quizás te interesaría crear las clases A1 y A2 en las que dicha propiedad sí es invariante.

    Cuando codifiques (depures, testees, …) A1 y A2, puedes asumir en ambos casos la existencia de ese invariante (que antes no era un invariante).

    En tal caso, podría ser aconsejable que la clase A esté «hueca» y únicamente wrapee las A1 y A2.

    De hecho, es lo que intuitivamente hacemos al no meter en el mismo saco los Pedidos y las Facturas, porque poder se puede, pero perderíamos invariantes (ej. «no está facturado es un invariante» en Pedidos) :)

  22. Aunque he aplicado esa idea un millón de veces, nunca lo había visto desde esa perspectiva.

    Es una buena heurística para ver una posible oportunidad de separar responsabilidades en dos clases y hacer cada una de ellas más cohesiva al tener un invariante más «definido».

  23. Gracias a ambos por la respuesta, voy a intentar explorar más este tema de Design By Contract porque me ha llamado mucho la atención pero claramente tengo que reciclarme. :)
    Gracias!

Comentarios cerrados.