Skip to content

Instantly share code, notes, and snippets.

@eterekhin
Last active January 22, 2020 06:20
Show Gist options
  • Save eterekhin/f03537c37c7343f634574371755751c9 to your computer and use it in GitHub Desktop.
Save eterekhin/f03537c37c7343f634574371755751c9 to your computer and use it in GitHub Desktop.

Рихтер описывает то, как можно делать обработку в .net framework, но мы сейчас в .net core, я сейчас кратко опишу как происходит обработка контрактов в .net framework: Контракты бывают трех видов:

  • Предусловия - проверяются до начала работы метода
  • Постусловия - проверяются при окончании работы метода
  • Инварианты - проверяются после завешения любого метода в коде Как можно уже понять по определению инвариантов - контракты отрабатывают, переписывая код. В данном случае Рихтер пишет про IL код. Утилита для этого поставляется отдельно - CCRewrite.ext, после того, как в VS поставить флажок Perform Runtime Contract Checking, VS станет автоматически вызывать эту утилиту после компиляции сборки. Пример контракта:
    public class SalaryCalculator
    {
        private int _sum;

        public int Sum(int a, int b)
        {
            // Preconditions
            Contract.Requires(a > 0);
            Contract.Requires(b > 0);

            //PostCondition
            Contract.Ensures(_sum == a + b);
            // code
        }
        
        [ContractInvariantMethod]
        private void ObjectInvariant(){
          Contract.Invariant(_sum > 0);
        }
    }

PostConditions будут выполнены в конце метода(будет переписал IL код).

Статические методы класса контракт имеют атрибут Condition("DEBUG") или(и) Condition("CONTRACT_FULL"), CONTRACT_FULL - полное включение контрактов, предполагает переписывание кода сборки(вынесения кода контрактов в отдельную сборку), DEBUG - активируются при включении конфиграции Debug, не переписывают сборку, только статически отрабатывают, выполняя проверку прямыми вызовами методов.

В классе Contract есть 2 метода, которые можно использовать без rewriter'a, Assume и Assert;

  • bool Assert(bool condition,string errorMessage) - выбрасывает ошибку, если condition == false
  • bool Assume(bool condition,string errorMessage) - выбрасывает ошибку, если condition == false, дополнительно информирую тулзы статического анализа, что это условие истинно, потому что точно также как проверка на null информирует r#, что объект не null. Тем самым Assume расширяет Assert добавляя игнорирование предупреждений средствами статического анализа.

Сейчас .NET Contract умерла ей никто не занимается. Но это не мешает рассмотреть контракты как концепцию, они важны, если вы проектируете иерархию объектов и базовый класс, определяет несколько виртуальных методов, тогда для каждого метода можно задать контракт в явном виде, чтобы наследники его придерживались.

    public class Aggregator<T>
    {
        public T[] _arr;

        public string Aggregator<T>(string path) => _path = path;

        protected virtual void Aggregate()
        {
            Debug.Assert(_arr.Length == 0);
            // что-то делаем(реализация по дефолту
            Debug.Assert(_arr.Length != 0);
        }

        protected virtual void Reset()
        {
            Debug.Assert(_arr.Length != 0);

            for (var i = 0; i < _arr.Length; i++)
            {
                _arr[i] = default;
            }

            Debug.Assert(_arr.All(x => x.Equals(default)));
        }
    }

    public class WrongAggregator<T> : Aggregator<T>
    {
        protected override void Reset()
        {
            Debug.Assert(_arr.Length > 10);

            for (var i = 0; i < _arr.Length; i++)
            {
                if (i > _arr.Length / 2)
                    return;
                _arr[i] = default;
            }

            Debug.Assert(_arr.Take(_arr.Length / 2).All(x => x.Equals(default)));
        }
    }

    public class RightAggregator<T> : Aggregator<T>
    {
        protected override void Reset()
        {
            Debug.Assert(_arr != null);
            _arr = new T[] { };
            Debug.Assert(_arr.Length == 0);
        }
    }

Рассмотрим класс Aggregator и его метод Reset, предусловие - массив не пуст, постусловие - массив состоит из дефолтных элементов

Посмотрим на WrongAggregator, он переопределил метод Reset и поставил precondition, что массив содержит больше 10 элементов, а такой код этого не ожидает:

Aggregator<int> aggregator = factory.GetAggregator<int>("input.txt") // возвращается WrongAggregator
aggregator.Aggregate(); - после вызова _arr содержит 4 элемента
// logic
aggregator.Reset(); - ошибка

Это из-за того, что мы усилили предусловие, предусловие базового класса - массив должен содержать элементы, условие произвольного класса - массив должен содержать более 10 элементов. Нельзя усиливать предусловия определенные в базовом классе

Но также возможна и такая ситуация:

Aggregator<int> aggregator = factory.GetAggregator<int>("input.txt") // возвращается WrongAggregator
aggregator.Aggregate(); - после вызова _arr содержит 4 элемента
// logic
aggregator.Reset();
CalculateSalary(aggregator.arr)
return aggregator.arr.Sum() // <- рассчитываем, массив уже пуст и заполняем его зп сотрудника, далее возвращаем ему значение зп за год, получается ошибка

В данном случае постусловие было ослаблено(базовый класс утверждал, все элементы массива буду дефолтными, наследник же - что только половина), вызывающий код не рассчитывал на это.

Тем самым можно сформировать принцип Барбары Лисков:

  • Нельзя усиливать предусловия
  • Нельзя ослаблять постусловия
  • Исторические ограничения («правило истории») — подкласс не должен создавать новых мутаторов свойств базового класса. Если базовый класс не предусматривал методов для изменения определенных в нем свойств, подтип этого класса так же не должен создавать таких методов. Иными словами, неизменяемые данные базового класса не должны быть изменяемыми в подклассе. Данная концепция, представленная Лисков и Винг, являлась новаторской для теории программной архитектуры.

Третье свойство в C# покрывается инкапсуляцией и свойствами, если у свойства нет модификатора set - его менять нельзя. Я бы трактовал третий пункт по-другому, если базовый класс не давал в публичное апи какую-нибудь часть своей функциональности(но давал наследникам) - то наследник не может раскрыть эту часть использую свои публичные методы

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment