Вот возможное решение в качестве доказательства концепции. С этим связаны различные проблемы, не в последнюю очередь то, что все объекты будут поддерживаться в кеше, и мы используем метод расширения для эффективного подвоха структуры кода, позволяющей нам поддерживать состояние, но это хотя бы демонстрирует, что этот договорный тест возможен.
Код ниже определяет различные вещи:
- Интерфейс
IRuntimeProperty
со свойством AlwaysTheSame
, которое возвращает целое число. Нам все равно, что это за значение, но хотелось бы, чтобы оно всегда возвращало одно и то же.
- Статический класс
RuntimePropertyExtensions
, который определяет метод расширения IsAlwaysTheSame
, который использует кэш предыдущих результатов IRuntimeProperty
объектов .
- Класс
RuntimePropertyContracts
, который вызывает метод расширения для проверки возвращаемого значения из AlwaysTheSame
.
- Класс
GoodObject
, который реализует AlwaysTheSame
так, как нам нравится, поэтому он всегда возвращает одно и то же значение для данного объекта.
- Класс
BadObject
, который реализует AlwaysTheSame
способом, который нам не нравится, поэтому последовательные вызовы возвращают разные значения.
- A
Main
метод для проверки контракта.
Вот код:
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
namespace SameValueCodeContracts
{
[ContractClass(typeof(RuntimePropertyContracts))]
interface IRuntimeProperty
{
int AlwaysTheSame { get; }
}
internal static class RuntimePropertyExtensions
{
private static Dictionary<IRuntimeProperty, int> cache = new Dictionary<IRuntimeProperty, int>();
internal static bool IsAlwaysTheSame(this IRuntimeProperty runtime, int newValue)
{
Console.WriteLine("in IsAlwaysTheSame for {0} with {1}", runtime, newValue);
if (cache.ContainsKey(runtime))
{
bool result = cache[runtime] == newValue;
if (!result)
{
Console.WriteLine("*** expected {0} but got {1}", cache[runtime], newValue);
}
return result;
}
else
{
cache[runtime] = newValue;
Console.WriteLine("cache now contains {0}", cache.Count);
return true;
}
}
}
[ContractClassFor(typeof(IRuntimeProperty))]
internal class RuntimePropertyContracts : IRuntimeProperty
{
public int AlwaysTheSame
{
get
{
Contract.Ensures(this.IsAlwaysTheSame(Contract.Result<int>()));
return default(int);
}
}
}
internal class GoodObject : IRuntimeProperty
{
private readonly string name;
private readonly int myConstantValue = (int)DateTime.Now.Ticks;
public GoodObject(string name)
{
this.name = name;
Console.WriteLine("{0} initialised with {1}", name, myConstantValue);
}
public int AlwaysTheSame
{
get
{
Console.WriteLine("{0} returning {1}", name, myConstantValue);
return myConstantValue;
}
}
}
internal class BadObject : IRuntimeProperty
{
private readonly string name;
private int myVaryingValue;
public BadObject(string name)
{
this.name = name;
Console.WriteLine("{0} initialised with {1}", name, myVaryingValue);
}
public int AlwaysTheSame
{
get
{
Console.WriteLine("{0} returning {1}", name, myVaryingValue);
return myVaryingValue++;
}
}
}
internal class Program
{
private static void Main(string[] args)
{
int value;
GoodObject good1 = new GoodObject("good1");
value = good1.AlwaysTheSame;
value = good1.AlwaysTheSame;
Console.WriteLine();
GoodObject good2 = new GoodObject("good2");
value = good2.AlwaysTheSame;
value = good2.AlwaysTheSame;
Console.WriteLine();
BadObject bad1 = new BadObject("bad1");
value = bad1.AlwaysTheSame;
Console.WriteLine();
BadObject bad2 = new BadObject("bad2");
value = bad2.AlwaysTheSame;
Console.WriteLine();
try
{
value = bad1.AlwaysTheSame;
}
catch (Exception e)
{
Console.WriteLine("Last call caused an exception: {0}", e.Message);
}
}
}
}
Это дает вывод следующим образом:
good1 initialised with -2080305989
good1 returning -2080305989
in IsAlwaysTheSame for SameValueCodeContracts.GoodObject with -2080305989
cache now contains 1
good1 returning -2080305989
in IsAlwaysTheSame for SameValueCodeContracts.GoodObject with -2080305989
good2 initialised with -2080245985
good2 returning -2080245985
in IsAlwaysTheSame for SameValueCodeContracts.GoodObject with -2080245985
cache now contains 2
good2 returning -2080245985
in IsAlwaysTheSame for SameValueCodeContracts.GoodObject with -2080245985
bad1 initialised with 0
bad1 returning 0
in IsAlwaysTheSame for SameValueCodeContracts.BadObject with 0
cache now contains 3
bad2 initialised with 0
bad2 returning 0
in IsAlwaysTheSame for SameValueCodeContracts.BadObject with 0
cache now contains 4
bad1 returning 1
in IsAlwaysTheSame for SameValueCodeContracts.BadObject with 1
*** expected 0 but got 1
Last call caused an exception: Postcondition failed: this.IsAlwaysTheSame(Contract.Result())
Мы можем создать столько GoodObject
экземпляров, сколько захотим. Звонок AlwaysTheSame
на них всегда удовлетворит контракт.
Напротив, когда мы создаем BadObject
экземпляров, мы можем вызвать AlwaysTheSame
для каждого только один раз ; как только мы вызываем его во второй раз, контракт вызывает исключение, потому что возвращаемое значение не соответствует тому, что мы получили в прошлый раз.
Как я сказал в начале, я бы с осторожностью использовал этот подход в рабочем коде. Но я согласен с тем, что это полезная вещь, которую нужно указать по контракту, и было бы здорово, если бы была поддержка такой неизменности возвращаемого значения во время выполнения, встроенная в структуру контрактов кода.