Как стать автором
Обновить

Contract Interfaces & Inheritance

Время на прочтение6 мин
Количество просмотров6K


Давайте поговорим о темной стороне силы, а именно – о проблемах в использовании контрактов. О тех милых мелочах, которые аккуратно обходятся разработчиками библиотеки и привносят такую немаленькую бочку дегтя в крохотный горшочек меда.


Вступление.

Будем считать, что классический документ по 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()));
Теги:
Хабы:
Всего голосов 8: ↑6 и ↓2+4
Комментарии0

Публикации