На сегодня, наверное, будет только один пример использования. Пример, который используется во многих статьях про проектирование по контракту - класс стек
;)Сначала приведу код интерфейса - он довольно простой
;)public interface IStack<T>
{
int Count { get; }
bool Empty { get; }
void Put(T item);
T Remove();
T GetItem();
}
Теперь можно сказать, какие предусловия, постусловия и инварианты можно придумать для данного интерфейса. Инвариантом может послужить условие о том, что количество элементов всегда больше или равно нулю. Предусловием для методов
Remove и
GetItem будет то, что количество элементов в стеке больше нуля. Постусловием для метода
Put будет то, что свойство
Count увеличивается на единицу, для
Remove - уменьшается на единицу, для
GetItem - свойство
Count не изменяется.
По схеме, которую хочу предложить вам я, все утверждения записываются в отдельном классе
(хотя их может быть и больше). Для каждого метода этого отдельного класса можно указать чем именно он является - предусловием, постусловием или инвариантом. Так же, необходимо произвести что то вроде
mapping`а или
биндинга параметров этих методов к параметрам проверяемого метода, а так же свойствам и полям. Все это делается через атрибуты.
Приведу пример проверяющего класса:
internal class StackCheck
{
[Invariant("Количество элементов больше или равно 0")]
public bool CheckCount(int Count)
{
return Count >= 0;
}
[Ensure("Количество элементов увеличивается на единицу", "Put")]
public bool EnsurePut(
[Old][PropName("Count")] int oldCount,
[PropName("Count")] int count)
{
return count == oldCount + 1;
}
[Ensure("Количество элементов уменьшается на единицу", "Remove")]
public bool EnsureRemove(
[Old][PropName("Count")] int oldCount,
[PropName("Count")] int count)
{
return count == oldCount - 1;
}
[Ensure("Количество элементов не изменяется", "GetItem")]
public bool EnsureItem(
[Old][PropName("Count")] int oldCount,
[PropName("Count")] int count)
{
return count == oldCount;
}
[Require("Стек не должен быть пустым", "GetItem")]
[Require("Стек не должен быть пустым", "Remove")]
public bool RequireNotEmpty([PropName("Count")] int count)
{
return count > 0;
}
}
Несложно заметить, что все проверочные методы должны возвращать
bool, в противном случае этот метод будет опущен при проверке.
Атрибут
Invariant задает, что метод является инвариантом класса, в данном случае такой метод один -
CheckCount. В параметр конструктора атрибута можно передать описание инварианта. В случае, если инвариант не выполнится, то это сообщение будет использоваться в качестве сообщения ошибки, так же этот текст может быть использован для документации
(подумываю позже написать что нибудь и для этого ;).
Перед параметром
Count этого проверочного метода нет никакого атрибута, в таком случае для метода инварианта этот параметр будет пытаться биндиться к свойству проверяемого класса с соответствующим именем
(регистр в данном случае важен), если такого свойства не будет, то к полю с таким названием, если не будет и поля, то сгенерируется исключение. В общем случае перед параметром проверочного метода инварианта можно указать один из двух атрибутов -
PropName (биндится к свойству с указанным именем) или
FieldName (биндится к полю с указанным именем).
Следующий метод помечен атрибутом
Ensure. Этот атрибут принимает два параметра: описание
(использование аналогично инварианту), а так же название метода, к которому относится постусловие. Кроме уже имеющихся возможных атрибутов параметров, в постусловиях можно использовать атрибут
Old. Он говорит о том, что значение свойства
(или чего то другого, в зависимости от второго параметра) должно браться до того, как был вызван проверяемый метод. Так же, кроме атрибутов биндинга к свойству и полю можно использовать атрибут биндинга к параметру метода -
PropName. Если никакого атрибута не проставлено, то считается, что стоит атрибут
PropName с названием аналогичному названию параметра. Сам метод
EnsurePut проверяет, что количество элементов после вызова метода
Put в проверочном классе будет увеличено ровно на единицу. Следующий метод -
EnsureRemove, практически идентичен предыдущему, только он проверяет, что количество элементов уменьшилось на единицу после вызова метода
Remove. Далее идет метод
EnsureItem - он проверяет, что после вызова метода
GetItem количество элементов не изменится.
Следующий метод помечен атрибутом
Require (даже двумя ;) Параметры атрибута
Require аналогичны атрибуту
Ensure, только он говорит о том, что помеченый им метод является предусловием, а не постусловием. К параметрам метода предусловия могут применены все те же атрибуты, что и к методам
Ensure, за исключением
Old (он просто опускается и не учитывается). Как видно из кода, здесь проверяется, что перед вызовом методов
Remove и
GetItem количество элементов больше нуля.
В принципе, логичнее проверять не свойство
Count, а свойство
Empty. В таком случае код можно переписать так:
[Require("Стек не должен быть пустым", "GetItem")]
[Require("Стек не должен быть пустым", "Remove")]
public bool RequireNotEmpty([PropName("Empty")] bool empty)
{
return !empty;
}
Как можно заметить, сам класс помечен как
internal. По крайней мере для рефлексии это имеет не такое большое значение
;)Теперь, имея такой класс, мы можем сказать интерфейсу стека, что он должен использовать его в качестве проверочного - для этого служит атрибут
ContractsType, которому необходимо передать тип проверяемого класса. Еще одно немаловажное свойство, в связи с этим, проверочный класс
(в нашем случае StackCheck) должен иметь пустой публичный конструктор
(хотя по поводу публичного - кто знает эту рефлексию ;)[ContractsType(typeof(StackCheck))]
public interface IStack;
{
//Описание интерфейса...
}
Наверное теперь у вас возникает вопрос - как все это использовать? А использовать все это можно через специальный класс, который называется
ContractsManager - он создается для экземпляра определенного класса. Например можно использовать вот так:
Stack<int> stack = new Stack();
ContractsManager contractsManager = new ContractsManager(stack);
int res = contractsManager.RunMethod<int>("Remove");
В данном примере мы создаем класс
"обертку", которая может вызвать метод вместе с проверками. Заметьте, что используется
generic метод - для того, что бы указать, результат какого типа вернет метод. Так же, обратите внимание на то, что здесь используется класс
Stack - наследник интерфейса
IStack - т.е. все проверки наследуются по иерархии.
Т.к. стек создается по умолчанию пустой, то в данном случае сгенерируется исключение - RequireException с текстом сообщения - "Стек не должен быть пустым".
Если необходимо вызвать метод без каких либо проверок, то можно просто вызвать метод у экземпляра класса. Но это еще не все. На самом деле у каждого из атрибутов (Invariant, Require и Ensure) имеется еще один параметр, который можно задать - это приоритет (по умолчанию он равен 0). У класса ContractsManager в свое время так же есть 3 свойства которые задают максимальные приоритеты для инвариантов, предусловий и постусловий (по умолчанию тоже равны 0). Соотвественно эти свойства можно менять во время исполнения программы тем самым "балансируя" производительностью.
Естественно, что все это пока только... бета ;) Уже сейчас есть некоторые проблемы, например с generic - скажем так, пока что я особо не обращаю на них внимание. Так же, есть еще несколько идей относительно того, что можно было бы еще добавить. В общем, жду так же и ваших комментариев ;)
P.S. Да, я очень не хороший мальчик, но библиотеку с кодом всего этого я пока никуда не выложил ;)