Más sobre Code Contracts…

 ·  ☕ 7 min  ·  ✍️ eiximenis

    Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí

    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() 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 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!

    Si quieres, puedes invitarme a un café xD

    eiximenis
    ESCRITO POR
    eiximenis
    Compulsive Developer