воскресенье, 2 декабря 2007 г.

Проектирование по контракту - первые попытки

Сегодня я предложу вам один способ использования проектирования по контракту в .NET, точнее очередную библиотеку, автором которой и являюсь ;) Надеюсь, что вы напишите свои впечатления, замечания, пожелания и т.д. и т.п.
На сегодня, наверное, будет только один пример использования. Пример, который используется во многих статьях про проектирование по контракту - класс стек ;)
Сначала приведу код интерфейса - он довольно простой ;)
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. Да, я очень не хороший мальчик, но библиотеку с кодом всего этого я пока никуда не выложил ;)

Комментариев нет:

Отправить комментарий