Основы Code Contracts (часть 2)

Давайте посмотрим как можно определить контракт для интерфейса. Кроме того, разберемся как настраивать реакцию на ошибки под собственные потребности.

Контракты для интерфейсов

Обратим внимание на еще одну интересную возможность Code Contracts: указание контракта для интерфейса. Это обязывает все реализующие его классы, соблюдать заданные правила.

Чтобы объявить контракт, необходимо создать класс, который будет содержать только код проверки. Остается только связать его и интерфейс с помощью атрибутов: [ContractClass] и [ContractClassFor]. Все это наглядно демонстрируется следующим примером:

namespace CodeContractsDemo
{
    using System;
    using System.Diagnostics.Contracts;

    [ContractClass(typeof(PersonContract))]
    public interface IPerson
    {
        string Name { get; set; }
        int Age { get; set; }

        void ShowInfo();
    }

    [ContractClassFor(typeof(IPerson))]
    public abstract class PersonContract : IPerson
    {
        public abstract string Name { get; set; }
        public abstract int Age { get; set; }

        public void ShowInfo()
        {
            Contract.Requires(!string.IsNullOrEmpty(this.Name));
            Contract.Requires(this.Age > 5);
        }
    }

    public class Person : IPerson
    {

        #region IPerson Members

        public string Name { get; set; }

        public int Age { get; set; }

        public void ShowInfo()
        {
            Console.WriteLine("Name: {0}", this.Name);
            Console.WriteLine("Age: {0}", this.Age);
        }

        #endregion
    }
}

В результате, при вызове метода ShowInfo() класса Person будут проверяться установленные предусловия. Такой подход очень удобен при проектировании и описании интерфейсов.

Свои сообщения и реакция на ошибку

У исключений, используемых в блоках if-throw, легко можно настроить сообщение об ошибке. Таким же свойством обладают и методы Code Contracts. Все рассмотренные выше методы имеют перегруженные варианты, позволяющие указать нужный текст.

Contract.Requires(payment != null, "payment can't be null");
.........
Contract.Ensures(Contract.Result<double>() >= 0, "result can be less than zero");
.........
Contract.Invariant(this.Y >= 0, "Y should be greater or equals 0");

Кроме того, можно определить свою реакцию на невыполнение контракта. Например, запись в журнал или любая другая дополнительная обработка. Для этого необходимо подписаться на событие ContractFailed класса Contract:

public static event EventHandler<ContractFailedEventArgs> ContractFailed;

Например:

namespace CodeContractsDemo
{
    using System;
    using System.Diagnostics.Contracts;

    class Program
    {
        static void Main(string[] args)
        {
            Contract.ContractFailed += 
                new EventHandler<ContractFailedEventArgs>(Contract_ContractFailed); 

            .........         
        }

        static void Contract_ContractFailed(object sender, ContractFailedEventArgs e)
        {
            Console.WriteLine(e.Message);
            // e.SetUnwind();
            e.SetHandled();
        }
    }
}

Информация о произошедшей ошибке содержится в параметре типа ContractFailedEventArgs. При её успешной обработки можно продолжит нормальное выполнение программы. Поэтому, отменить выброс исключения необходимо вызвать e.SetHandled().

Обратите внимание, что если обработчиков несколько, то любой их них может произвести такую блокировку. Но при необходимости можно вызвать e.SetUnwind(). Это гарантирует выброс исключения, независимо от того были ли до этого и будут ли после обращения к e.SetHandled().


Исходный код проекта (C#, Visual Studio 2010): CodeContractsDemo.zip

Комментарии (4) -

Hennadii Omelchenko 14.07.2011 15:16:40

Отличные статьи! Спасибо!
Кстати, еще есть очень удобная перегрузка Contract.Requires<TException>(), если нужно переехать с if-throw

Единственная (но очень большая) проблема с контрактными интерфейсами заключается в том, что абстрактные классы не могут наследоваться при этом. Что не позволяет на их основе строить нормальную развлетвленную архитектуру интерфейсов.

Событие  public static event EventHandler<ContractFailedEventArgs> ContractFailed; - надо писать в классе Person?

Arvalon мы подписываемся, а не создаем события.

Добавить комментарий