Программирование по контракту C# — Code Contracts C# – это метод построения архитектуры программного обеспечения, в соответствии с которым для интерфейсов компонентов системы должны быть разработаны условия определяющие допустимые значения и ожидаемое поведение системы. То есть, мы заранее задаем правила для метода и сигнализируем при их нарушении. Данный подход позволяет на порядок повысить качество и надежность кода, в то же время упрощая тестирование и отладку. Давайте более подробно рассмотрим эту идею.
Программирование (проектирование) по контрактам подразумевает написание дополнительных проверочных условий в методах класса. Данные условия должны гарантировать корректность входных и возвращаемых данных. То есть, данная идея состоит в том, что перед использованием входных данных внутри метода необходимо проверить, удовлетворяют ли они минимальным условиям корректности, а также удостовериться, что после выполнения работы мы получили корректный результат.
Еще одним преимуществом использования данного метода программирования можно считать документируемость кода. За счет декларации в методе требований становится более понятной логика его работы. Кроме того, существуют специализированные утилиты, позволяющие автоматически создавать документацию по коду или создавать модульные тесты. Это не только экономит время разработчику, но и в целом улучшает стиль программирования.
Выделяют три вида условий:
- Предусловия (Preconditions) – условия корректности входных аргументов метода;
- Постусловия (Postconditions) – условия корректности результата работы метода;
- Инварианты (Invariants) – условия корректности переменных метода на протяжении всего его выполнения.
В .NET версии 4.0 и старше для реализации данного метода программирования используется пространство имен System.Diagnostics.Contracts. Давайте рассмотрим, как ей можно воспользоваться.
Для начала нам необходимо создать символ CONTRACTS_FULL или скачать специальную утилиту расширение для Visual Studio. Для того чтобы использовать первый вариант нам достаточно добавить директиву в начале каждого файла, в котором будут использоваться контракты.
#define CONTRACTS_FULL
Рассмотрим элементарную модель клиента магазина. Создадим интерфейс, в котором будет информация о имени и деньгах клиента, а также предоставим возможность покупать товары.
Предусловия (Preconditions)
Предусловия задаются с в начале метода в следующем формате:
Contract.Requires<ArgumentNullException> ( !string.IsNullOrWhiteSpace(productName), $"Передаваемое название товара в методе {nameof(Bay)} имеет значение null или пустое." );
Здесь мы говорим, что переменная productName не должны быть равна null, пустой или состоять из одних пробелов. В противном случае мы генерируем исключение ArgumentNullException с сообщением об ошибке указанном в параметре.
Постусловия (Postconditions)
Постусловия тоже задаются в начале метода.
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()));
Здесь мы получаем результат выполнения метода с помощью команды Contract.Result<T>() и говорим что этот результат не может быть равным null или пустым.
Инварианты (Invariants)
Инварианты тоже задаются в начале метода.
Contract.Invariant(Money >= 0);
Здесь мы говорим, что значение переменной Money должно быть больше либо равно нулю на всем протяжении выполнения метода.
Интерфейсы
Важной особенностью использования данной библиотеки является то, что мы можем создавать контракты для интерфейсов. Тогда все классы реализующие данный интерфейс будут обязаны соблюдать эти контракты. Давайте рассмотрим пример кода.
/// <summary> /// Базовый интерфейс клиента. /// </summary> [ContractClass(typeof(ClientContract))] // Задаем имя класса, в котором будут описаны контракты. public interface IClient { /// <summary> /// Имя клиента. /// </summary> string Name { get; } /// <summary> /// Количество денег клиента. /// </summary> decimal Money { get; } /// <summary> /// Купить товар. /// </summary> /// <param name="productName"> Название товара. </param> /// <param name="price"> Цена товара. </param> /// <returns> Сообщение о покупке. </returns> string Bay(string productName, decimal price); }
Теперь создадим класс, в котором опишем все необходимые условия с помощью контрактов.
[ContractClassFor(typeof(IClient))] // Указываем к какому интерфейсу относится класс контрактов. internal abstract class ClientContract : IClient { /// <summary> /// Контракт для свойства Имя. /// Имя не может быть пустым или равным null. /// </summary> public string Name { get { Contract.Requires<ArgumentNullException> ( !string.IsNullOrWhiteSpace(Name), $"Имя клиента в свойстве {nameof(Name)} имеет значение null или пустое." ); return string.Empty; } } /// <summary> /// Контракт для свойства Деньги. /// Количество денег должно быть больше нуля. /// </summary> public decimal Money { get { Contract.Requires<ArgumentException> ( Money >= 0, $"Количество денег клиента {Name} в свойстве {nameof(Money)} имеет значение ({Money}) меньше либо равное нулю." ); return decimal.One; } } /// <summary> /// Контракт на метод покупки. /// </summary> /// <param name="productName"> Название продукта не должно быть пустым. </param> /// <param name="price"> /// Стоимость не может быть ниже либо равна нулю. /// Стоимость не может превышать количество денег у клиента. /// </param> /// <returns> Возвращаемое сообщение не может быть пустым. </returns> public string Bay(string productName, decimal price) { Contract.Requires<ArgumentNullException> ( !string.IsNullOrWhiteSpace(productName), $"Передаваемое название товара в методе {nameof(Bay)} имеет значение null или пустое." ); Contract.Requires<ArgumentException> ( price > 0, $"Стоимость товара {productName} в методе {nameof(Bay)} имеет значение ({price}) меньше либо равное нулю." ); Contract.Requires<ArgumentException> ( price <= Money, $"Стоимость товара {productName} в методе {nameof(Bay)} имеет значение ({price}) большее чем количество денег ({Money}) у клиента {Name}." ); Contract.Invariant(Money >= 0); Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); return string.Empty; } }
Теперь создадим класс реализующий данный интерфейс.
#define CONTRACTS_FULL using System; using System.Diagnostics.Contracts; namespace CodeContracts { /// <summary> /// Клиент. /// </summary> public class Client : IClient { /// <summary> /// Имя. /// </summary> public string Name { get; private set; } /// <summary> /// Деньги. /// </summary> public decimal Money { get; private set; } /// <summary> /// Создать экземпляр клиента. /// </summary> /// <param name="name"> Имя. </param> /// <param name="money"> Деньги. </param> public Client(string name, decimal money) { Contract.Requires<ArgumentNullException> ( !string.IsNullOrWhiteSpace(name), $"Имя клиента ({nameof(name)}) в конструкторе класса {nameof(Client)} имеет значение null или пустое." ); Contract.Requires<ArgumentException> ( money >= 0, $"Количество денег в свойстве {nameof(money)} в конструкторе класса {nameof(Client)} имеет значение ({money}) меньше нуля." ); Name = name; Money = money; } /// <summary> /// Купить товар. /// </summary> /// <param name="productName"> Название товара. </param> /// <param name="price"> Стоимость товара. </param> /// <returns> Сообщение о покупке. </returns> public string Bay(string productName, decimal price) { Money -= price; return $"Client {Name} bought {productName} at a price {price}$ {DateTime.Now.ToString("dd.MM.yyyy HH:mm")}"; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Имя. </returns> public override string ToString() { return Name; } } }
Ну и теперь нам остается только вызвать наш код в основном методе программы.
using System; namespace CodeContracts { class Program { static void Main(string[] args) { var client = new Client("John", 1000); var message = client.Bay("Pizza", 10); Console.WriteLine(message); Console.ReadLine(); } } }
Программирование по контракту — Заключение
Исходный код можно также посмотреть в репозитории github.
Также рекомендую изучить статью Голосовой движок на C# . А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.