Más sobre Code Contracts…

Hola a todos… después de que Jorge (en http://geeks.ms/blogs/jorge/archive/2009/04/26/precondiciones-y-microsoft-code-contracts.aspx) yo mismo (en http://geeks.ms/blogs/etomas/archive/2009/05/04/pexcando-errores-en-nuestro-c-243-digo.aspx) comentasemos algo de Code Contracts, voy a comentar algunas cosillas más que me he encontrado con Code Contracts usándolos en un proyecto real.

Aunque están en fase “beta”, la tecnología está lo suficientemente madura para ser usada en proyectos “reales”, al menos teniendo en cuenta de que las builds donde suelen activarse todos los contratos son las builds de debug. En nuestro caso tenemos activados todos los contratos en debug y sólo las precondiciones en release.

Precondiciones

La última versión de Code Contracts salió el 20 de mayo, y una de las principales diferencias es que la excepción de violación de contrato (ContractException) ha dejado de ser pública para ser interna. Por ello en lugar de Contract.Requires() es mejor usar Contract.Requieres<TEx>() que lanza una excepción de tipo TEx en caso de que la condición no se cumpla:

public double Sqrt(double d)
{
    Contract.Requires<ArgumentException>(d >= 0, "d");
}

Si la precondición no se cumple la excepción que se generará será una ArgumentException en lugar de la interna ContractException.

Recordad que fundamentalmente las precondiciones vienen a decir que nosotros como desarrolladores de una clase, no nos hacemos responsables de lo que ocurra si el cliente no cumple el contrato… por lo tanto es de cajón que el cliente debe poder comprobar si su llamada cumple las precondiciones o no. O dicho de otro modo: las precondiciones de un método público deben ser todas ellas públicas. Esto no es correcto:

class Foo
{
    List<int> values;
    public Foo()
    {
        this.values = new List<int>();
    }
    public void Add(int value) 
    {
        Contract.Requires<ArgumentException>
            (!values.Contains(value));
        values.Add(value);
    }       
}

Como va a comprobar el cliente que su llamada a Add cumple el contrato, si no le damos ninguna manera de que pueda validar que el parámetro que pase no esté en la lista? Lo suyo es hacer algo así como:

class Foo
{
    List<int> values;
    public IEnumerable<int> Values {
        get { return this.values; }
    }
    public Foo()
    {
        this.values = new List<int>();
    }
    public void Add(int value) 
    {
        Contract.Requires<ArgumentException>
            (!Values.Contains(value));
        values.Add(value);
    }       
}

Interfaces

Métodos que implementen métodos de una interfaz NO pueden añadir precondiciones. Es decir, esto no compila si teneis los contratos activados:

interface IFoo
{
    double Sqrt(double d);
}

class Foo : IFoo
{
    public double Sqrt(double d)
    {
        Contract.Requires<ArgumentException>(d >= 0, "d");
        return Math.Sqrt(d);
    }
}

El compilador se quejará con el mensaje: Interface implementations (ConsoleApplication7.Foo.Sqrt(System.Double)) cannot add preconditions. La razón de esto es que las precondiciones deben añadirse a nivel de interfaz y no a nivel de implementación, por la razón de que la interfaz es en muchos casos lo único que vamos a hacer público.

Dado que C# no nos deja meter código en una interfaz, la sintaxis para definir las precondiciones de una interfaz es un poco… curiosa: consiste en declarar una clase que no hace nada salvo contener las precondiciones, y usar un par de atributos (ContractClass y ContractClassFor) para vincular esta clase “de contratos” con la interfaz:

[ContractClass(typeof(IFooContracts))]
interface IFoo
{
    double Sqrt(double d);
}
[ContractClassFor(typeof(IFoo))]
class IFooContracts : IFoo
{
    double IFoo.Sqrt(double d)
    {
        Contract.Requires<ArgumentException>(d >= 0, "d");
        return default(double);
    }
}
class Foo : IFoo
{
    public double Sqrt(double d)
    {
        return Math.Sqrt(d);
    }
}

La clase IFooContracts contiene sólo los contratos para la interfaz IFoo. El valor de retorno usado es ignorado (es sólo para que compile el código). Si ejecutais el código paso a paso, vereis que cuando haceis new Foo().Sqrt() se ejecutan primero los contratos definidos en IFooContracts.Sqrt y luego el código salta al método Foo.Sqrt.

Code Contracts requiere que la implementación de la interfaz sea explícita.

Invariantes

El invariante de un objeto es un conjunto de condiciones que se cumplen siempre a lo largo del ciclo de vida del objeto (excepto cuando es destruído). A nivel de contratos esto significa que son condiciones que se evalúan inmediatamente después de cualquier método público, y que todas ellas deben cumplirse. Si alguna de ellas falla, el invariante se considera incumplido y el contrato roto.

Los invariantes se declaran en un método de la clase decorado con el atributo ContractInvariantMethodAttribute y consisten en varias llamadas a Contract.Invariant con las condiciones que deben cumplirse:

class Foo
{
    public int Value { get; private set;}
    public void Inc()
    {
        this.Value++;
    }
    public void Dec()
    {
        this.Value--;
    }
    [ContractInvariantMethod]
    protected void ObjectInvariant()
    {
        Contract.Invariant(this.Value > 0);
    }
}

En este código hemos definido que el invariante de Foo es que el valor de la propiedad Value debe ser siempre mayor o igual a cero. Esta condición se evaluará al final de cualquier método público de Foo. Por lo tanto en el siguiente código:

Foo foo = new Foo();
foo.Inc();
foo.Dec();
foo.Dec();

La segunda llamada a Dec() provocará una excepción de contrato.

Métodos puros

Un método “Puro” es aquel que no tiene ningún “side-effect”, es decir no modifica el estado de ninguno de los objetos que recibe como parámetro (incluyendo this). Un método puro se puede llamar infinitas veces con los mismos parámetros y se obtendran siempre los mismos resultados.

El código que se ejecuta para evaluar los contratos debe ser código puro. La razón principal es que los contratos pueden desactivarse en función del tipo de build, por lo que añadir código que no sea puro puede causar que el comportamiento cambie en función de si los contratos están o no habilitados. Si llamamos a un  método NO PURO desde un contrato nos va a salir un warning. Fijaos en el siguiente código:

[ContractClass(typeof(IFooContracts))]
interface IFoo
{
    IEnumerable<int> Values { get;}
    void Bar(int value);
}
[ContractClassFor(typeof(IFoo))]
sealed class IFooContracts : IFoo
{
    private IEnumerable<int> values; // Para que compile
    IEnumerable<int> IFoo.Values { get { return values; } }
    void IFoo.Bar(int value)
    {
        Contract.Requires(CheckValue(value));
    }
    public bool CheckValue(int value)
    {
        return (value % 2) == 0 && !((IFoo)this).Values.Contains(value);
    }
}
class Foo : IFoo
{
    private readonly List<int> values;
    public IEnumerable<int> Values
    {
        get { return this.values; }
    }
    public Foo()
    {
        this.values = new List<int>();
    }
    public void Bar(int value)
    {
        this.values.Add(value);
    }
}

Antes que nada: que no os líe la variable privada IEnumerable<int> values declarada en IFooContracts: es simplemente para que compile el código, pero realmente nunca es usada… Realmente nunca se instancian (ni usan) objetos de las clases de contrato: en el contexto de ejecución del método CheckValue el valor de this NO es un objeto IFooContracts, si no un objeto Foo (mirad la imagen si no me creeis :p).

image

Bueno… que me desvío del tema. A lo que íbamos: La clase de contratos para  IFoo, define un método CheckValue que sirve para evaluar la precondición del método Bar. Si compilais os aparecerá un warning:

Detected call to impure method ‘ConsoleApplication7.IFooContracts.CheckValue(System.Int32)’ in a pure region in method ‘ConsoleApplication7.IFooContracts.ConsoleApplication7.IFoo.Bar(System.Int32)’

Como sabe Code Contracts que mi método CheckValue no es puro? Pues simplemente porque yo no lo he declarado como tal. Para ello basta con decorarlo con el atributo Pure:

[Pure]
public bool CheckValue(int value)
{
    return (value % 2) == 0 && !((IFoo)this).Values.Contains(value);
}

Actualmente Code Contracts no comprueba que mi método que dice ser puro, lo sea de verdad… como en MS no se fían mucho de nosotros (los desarrolladores… ¿por que será? :p) están preparando mecanismos de validación de forma que cuando un método diga ser puro, lo sea efectivamente de verdad. A dia de hoy, tanto si poneis [Pure] como si no, funciona todo igual, así que el atributo Pure sirve para poco, al menos en lo que a ejecución se refiere. De todos modos creo que documentalmente es un atributo muy interesante: Indica que quien hizo el método lo hizo con la intención de que fuese puro, así que quizá debemos vigilar un poco las modificaciones que hagamos en este método…

Personalmente me hubiese encantado que Pure formase parte de C# a nivel de palabra reservada incluso… para que el compilador nos pudiese avisar si estamos modificando algún estado de algún objeto (o sea llamando a un método no-puro) desde un método puro. Pero si no nos quieren ni dar referencias const, mucho menos todavía nos van a dar esto… 🙁

Bueno… cierro el rollo aquí… han quedado bastantes cosillas de contracts pero espero que al menos esto os ayude y os anime a dar el paso de empezar a utilizarlos en vuestras aplicaciones, porque realmente creo que vale mucho la pena…

… y más cuando Sandcastle sea capaz de sacar la información de los contratos de cada clase en la documentación, tal y como parece ser intención de microsoft (http://social.msdn.microsoft.com/Forums/en-US/codecontracts/thread/cb5556e9-9dc9-45ed-8016-567294236af3).

Saludos!

2 comentarios sobre “Más sobre Code Contracts…”

  1. Hola Eduard

    Hace ya bastante tiempo tuve la oportunidad a través del libro «Diseño de software orientado a objetos» de Bertrand Meyer de conocer el diseño por contratos y las precondiciones y postcondiciones; entonces saqué una conclusión: «Esto sirve para crear software de calidad y determinar exactamente que puntos del código son los que provocan un bug».

    Yo no soy si en este pensamiento estoy en lo cierto me gustaría conocer tu opinión.

    Además ahora me estoy imaginando como aplicar esto a mis desarrollos y se me ocurre un punto de enlace para contrats en mi código (además de dotar de pre/post condiciones) allí donde se intercepta el evento AplictionExceptionNotAttached (o algo así, no recuerdo el nombre) donde se me da la oportunidad de obtener acceso a una excepción no direccionada en la aplicación, y allí logear el error, informar si procede al usuario etc… ¿Aqui crees que voy en buen camino o ha de hacerse las comprobaciones en ubicaciones físicas en el código más cercanas a dicha excepción?

  2. Hola Julio,

    Personalmente estoy de acuerdo con la frase que comentas: el diseño por contratos ayuda a detectar donde se genera un error. No se respetan las precondiciones? El error es del cliente. No se respetan las postcondiciones? El error está en el método. Simple y sencillo.

    Por otro lado, ya hablando de MS Code Contracts, una gran ventaja es que te permite a la vez que defines los contratos, indicar que debe ocurrir cuando haya una violación de contrato: por defecto en debug aparece un Assert, pero en release puede no interesarnos (de hecho, no nos interesará) pero en su lugar podemos simplemente guardar en un log las violaciones de contratos y luego proseguir normalmente. Seguramente nuestra aplicación «rebentará» pero sabremos que ha sido a causa de una violación de contrato (y de que contrato se trata).
    Incluso podemos deshabilitar totalmente los contratos en release (cuando el rendimiento sea crítico).

    Sobre lo de las excepciones: el uso de contratos no te exime que utilices la gestión de excepciones de forma análoga a como lo harías sin contratos. De hecho, generalmente es MALA idea capturar con catch una excepción de contrato (esa es la razón por la que ContractException es interna en la última versión). La razón es que una violación de contrato es significado de código «erroneo»… de hecho si el compilador pudiese detectar violaciones de contrato, este código no debería ni compilar. Así que no tiene sentido capturar violaciones de contrato: que el programa «rebiente» está bien… Como esto puede ser no aceptable en el mundo real, existe Contract.Requires que convierte una violación de contrato en una excepción de tipo TEx, que si se desea se podría tratar.
    Por ejemplo, en el proyecto en el que trabajo, en Release tenemos habilitados los contratos (sólo las precondiciones, las postcondiciones las probamos exhaustivamente con unit test en debug) y tenemos el sistema configurado para que nos guarde en un log cualquier violación de contrato y lance una excepción que NO capturamos… el programa rebienta (antes se guarda en otro log la excepción, lo mismo que ocurre en cualquier otra excepción no controlada sea o no de contratos). Para el usuario NO es distinto una violación de contrato que cualquier otra excepción no capturada. Pero para nosotros es totalmente distinto, puesto que en el primer caso tenemos el log de contratos y sabemos que contrato falló, y por ende donde puede estar el error.
    Mi opinión sobre las excepciones (en general) es que hay que tratarlas lo más cerca posible del código que la ha producido, pero solamente siempre y cuando se pueda hacer algo al respecto (algo distinto de grabar en un log Y/o mostrar un messagebox). Si no se puede hacer nada específico para solucionar la excepción, etonces lo mejor es que siga su curso hacia arriba. Si llega a «arriba del todo» entonces sí: se graba un log, se muestra un mensaje si es de menester y que el programa finalice.

    Espero haberte sido útil!
    Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *