Давайте поговорим о темной стороне силы, а именно – о проблемах в использовании контрактов. О тех милых мелочах, которые аккуратно обходятся разработчиками библиотеки и привносят такую немаленькую бочку дегтя в крохотный горшочек меда.
Вступление.
Будем считать, что классический документ по MS Contracts мы читали, поэтому сразу перейдем к такой удобной вещи, как спецификация интерфейсов для контрактного программирования.
http://research.microsoft.com/en-us/projects/contracts/
http://download.microsoft.com/download/C/2/7/C2715F76-F56C-4D37-9231-EF8076B7EC13/userdoc.pdf
Если у нас есть несколько public полей или методов, то один раз задав спецификацию для этих параметров в рамках интерфейса мы сможем использовать подобный контракт для реальных классов без дополнительной писанины. Описав контракт, можно затем его задействовать как удобную палочку-выручалочку.
Например, мы создаем представителя другой стороны:
public class TheDarkLord
{
public string Name { get; set; }
private string SoName { get; set; }
public TheDarkLord(string name, string soName)
{
this.Name = name;
this.SoName = soName;
}
public string ModifyBaseName(string updatedName)
{
this.Name = updatedName;
return this.Name;
}
}
Вполне возможно, что ради будущих темных падованов потребуется использовать описанный класс как базовый. И, чтобы не обременять себя лишними проверками, зададим спецификацию для public раздела, а также дополним проверку и private заодно.
а) опишем интерфейс
б) не забудем на него создать абстрактную реализацию, которая вберет в себя контрактную спецификацию
в) и расширим основной класс поддержкой проверки private раздела
Реализация:
а) интерфейс и ссылка на контрактную реализацию
[ContractClass(typeof(ITheDarkLordContract))]
public interface ITheDarkLord
{
string Name { get; set; }
string ModifyBaseName(string updatedName);
}
б) контрактная спецификация
[ContractClassFor(typeof(ITheDarkLord))]
public abstract class ITheDarkLordContract : ITheDarkLord
{
public string Name
{
get
{
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()),
"[ITheDarkLordContract] Name of Dark Lord is null or empty.");
return default(string);
}
set
{
Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(value),
"[ITheDarkLordContract] Name of Dark Lord is null or empty.");
}
}
public string ModifyBaseName(string updatedName)
{
Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(updatedName),
"[ITheDarkLordContract.ModifyBaseName] The new name of Dark Lord is null or empty.");
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()),
"[ITheDarkLordContract.ModifyBaseName] The updated name of Dark Lord is null or empty.");
return default(string);
}
}
в) измененный ключевой класс, на который мы прицепили интерфейс и проверку private
public class TheDarkLord : ITheDarkLord
{
public string Name { get; set; }
private string SoName { get; set; }
[ContractInvariantMethod]
private void DarkLordVerification()
{
Contract.Invariant(!string.IsNullOrEmpty(SoName));
}
public TheDarkLord(string name, string soName)
{
Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(name));
Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(soName));
this.Name = name;
this.SoName = soName;
}
public string ModifyBaseName(string updatedName)
{
this.Name = updatedName;
return this.Name;
}
}
Интрига сюжета, которая та самая неожиданная…
Все у нас было хорошо до этого момента. А потом мы сделали шаг номер два – использовали описанный класс как производную для:
[ContractClass(typeof(ITheWhiteLordContract))]
public interface ITheWhiteLord : ITheDarkLord
{
string UpdateAllNames(string name, string soName);
}
[ContractClassFor(typeof(ITheWhiteLord))]
public abstract class ITheWhiteLordContract : ITheDarkLordContract, ITheWhiteLord
{
public string UpdateAllNames(string name, string soName)
{
return default(string);
}
}
Вроде все красиво – создали наследника для интерфейса, доработали новый абстрактный класс и попытались скомпилировать. После чего совершенно неожиданно наступили на ограничение номер раз для библиотеки контрактов: мы не можем контрактные классы наследовать от кого-либо, кроме Object… Хлоп – и понятие наследование для контрактных интерфейсов развалилось…
Хорошо, что же нам рекомендуют разработчики? А они поступают просто фантастическим способом: «возьмите абстрактный класс, пропишите в нем спецификацию методов предка»…
[ContractClass(typeof(ITheWhiteLordContract))]
public interface ITheWhiteLord : ITheDarkLord
{
string UpdateAllNames(string name, string soName);
}
[ContractClassFor(typeof(ITheWhiteLord))]
public abstract class ITheWhiteLordContract : ITheWhiteLord
{
public string Name { get; set; }
public string ModifyBaseName(string updatedName)
{
return default(string);
}
public string UpdateAllNames(string name, string soName)
{
Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(name),
"[ITheWhiteLordContract.UpdateAllNames] The new name of White Lord is null or empty.");
Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(name),
"[ITheWhiteLordContract.UpdateAllNames] The new soname of White Lord is null or empty.");
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()),
"[ITheWhiteLordContract.UpdateAllNames] The updated name of White Lord is null or empty.");
return default(string);
}
}
Теперь представим, что в базовом классе у нас штук 10-20 полей, которые придется «протаскивать» наверх. А иерархия может быть не из одного слоя, а наследоваться через группу классов. И после этого предложенная рекомендация сродни издевательству.
Что получили?
Варианты с использованием интерфейсов, как точек для будущих Injections, уже срабатывают плохо. Придется убирать наследование в контрактных интерфейсах и описывать вызовы нужных нам объектов как реальных классов.
а) Специфицируем контракт для потомка
[ContractClass(typeof(ITheWhiteLordContract))]
public interface ITheWhiteLord
{
string UpdateAllNames(string name, string soName);
}
[ContractClassFor(typeof(ITheWhiteLord))]
public abstract class ITheWhiteLordContract : ITheWhiteLord
{
public string UpdateAllNames(string name, string soName)
{
Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(name),
"[ITheWhiteLordContract.UpdateAllNames] The new name of White Lord is null or empty.");
Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(name),
"[ITheWhiteLordContract.UpdateAllNames] The new soname of White Lord is null or empty.");
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()),
"[ITheWhiteLordContract.UpdateAllNames] The updated name of White Lord is null or empty.");
return default(string);
}
}
б) изменяем наследование для реального класса
public class TheWhiteLord : TheDarkLord, ITheWhiteLord
{
public string UpdateAllNames(string name, string soName)
{
return this.Name;
}
}
И теперь вместо использования интерфейсов, будем использовать доступ к спецификации классов:
было
public class SpaceSheep
{
public ITheDarkLord AbstractLord { get; set; }
}
реализация в виде класса
public class SpaceSheep
{
public TheDarkLord AbstractLord { get; set; }
}
С точки зрения работоспособности системы мы ничего не потеряли. Если у нас есть класс TheWhiteLord, то его легко отнаследовать от темной стороны, получив доступ к нужным свойствам. При этом работоспособность используемых фреймворков, оперирующих IOC – не поменяется. Фактически, мы можем для спецификации зависимостей оперировать любой реализацией – интерфейсами, абстрактными классами или реальными.
Но осадочек остался. Кто мешал создателям Contracts.Library дать нам возможность оперировать интерфейсами не только для проверки допустимых public свойств, но и выстраивать целостную архитектуру наследования. Увы…
На закуску.
Еще одно мелкое неудобство, о котором желательно помнить. Специализация private свойств через ContractInvariantMethod накладывает небольшое ограничение. Поля, которые анализируются в инвариатных методах, должны быть специфицированы в конструкторе. Вот наш класс:
public class TheDarkLord : ITheDarkLord
{
public string Name { get; set; }
private string SoName { get; set; }
[ContractInvariantMethod]
private void DarkLordVerification()
{
Contract.Invariant(!string.IsNullOrEmpty(SoName));
}
...
}
Вот образец его создания:
TheDarkLord lord = new TheDarkLord("James", "Bond");
Если же мы добавим пустой конструктор по умолчанию, то получим предупреждение. Потому что инвариантный метод после выхода из конструктора проверит условия и вернет Exception:
CodeContracts: invariant is false: !string.IsNullOrEmpty(SoName)
И достаточно удобный способ инициализации свойств класса через «отложенное» присвоение не будет работать:
TheDarkLord secondLord = new TheDarkLord() { Name = "James" };
Можно переписать private свойство через дополнительную переменную и проверку при помощи контрактов, отказавшись от инвариантов, но это увеличит объем кода и лишит возможности использовать достаточно мощный и удобный инструмент post-condition verification…
PS. Про выводимые контрактами сообщения даже не заикаюсь. Казалось бы – кто мешает добавить обработчик разборки стэка вызова и генерацию имени автоматически. Но – создатели разрешают использовать лишь статические строки, лишая нас возможности заменить подобного рода код
Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(name),
"[ITheWhiteLordContract.UpdateAllNames] The new name of White Lord is null or empty.");
на что-то схожее с
Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(name),
string.Format("{0} The new name of White Lord is null or empty.",GetCurrentMethod()));