Вам бонус- начислено 1 монета за дневную активность. Сейчас у вас 1 монета

Контрактное программирование с примерами

Лекция



контрактное программирование (design by contract (DbC), programming by contract, contract-based programming) — это метод проектирования программного обеспечения. Он предполагает, что проектировщик должен определить формальные, точные и верифицируемые спецификации интерфейсов для компонентов системы. При этом, кроме обычного определения абстрактных типов данных, также используются предусловия, постусловия и инварианты. Данные спецификации называются «контрактами» в соответствии с концептуальной метафорой условий и ответственности в гражданско-правовых договорах.

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

В отличие от обычной валидации, контрактное программирование помогает описать строгие правила взаимодействия с кодом, проверяя:

  1. Предусловия – правила, которые должны быть выполнены перед запуском метода. Например, аргументы метода должны быть в допустимом диапазоне.

  2. Постусловия – условия, которые должны быть выполнены после завершения метода. Например, возвращаемое значение метода должно соответствовать определенным критериям.

  3. Инварианты – состояния, которые должны всегда быть истинными для объекта. Например, свойство объекта не должно выходить за определенные границы.

Таким образом, контрактное программирование не просто проверяет корректность входных данных, а еще и гарантирует, что код работает в согласии с четко определенными правилами и ограничениями.

История

Термин предложил Бертран Мейер в связи с разработкой языка Eiffel. Контрактное программирование выросло из формальной верификации, формальной спецификациии логики Хоара. Контрактное программирование — это не только простая метафора, указывающая способ проектирования. Условия, облегчающие применение контрактного программирования:

  • наличие наследования и возможность динамического связывания,
  • способность обрабатывать исключения,
  • возможность автоматического документирования ПО.

Описание

Контрактное программирование с примерами

рис. проектирование модуля по контракту

Основная идея контрактного программирования — это модель взаимодействия элементов программной системы, основывающаяся на идее взаимных обязательств и преимуществ. Как и в бизнесе, клиент и поставщик действуют в соответствии с определенным контрактом. Контракт некоторого метода или функции может включать в себя:

  • конкретные обязательства, которые любой клиентский модуль должен выполнить перед вызовом метода — предусловия, которые дают преимущество для поставщика — он может не проверять выполнение предусловий;
  • конкретные свойства, которые должны присутствовать после выполнения метода — постусловия, которые входят в обязательства поставщика;
  • обязательства по выполнению конкретных свойств — инвариантов, которые должны выполняться при получении поставщиком сообщения, а также при выходе из метода.

Многие языки программирования позволяют учитывать такие обязательства. Контрактное программирование подразумевает эти требования критическими для корректности программ, поэтому они должны быть утверждены при проектировании. Таким образом, контрактное программирование предписывает начинать писать код с написания формальных утверждений корректности (assertions).

В объектно-ориентированном программировании контракт метода обычно включает следующую информацию:

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

При использовании контрактов сам код не обязан проверять их выполнение. Обычно в таких случаях в коде делают жесткое падение[уточнить] («fail-fast»), таким образом облегчая отладку выполнения контрактов. Во многих языках, таких как C, C++, Delphi, PHP, такое поведение реализуется оператором assert. В релизовом варианте кода это поведение может быть сохранено, либо проверки могут быть убраны чтобы повысить производительность.

Юнит-тесты проверяют модуль изолированно от других, проверяя, что модуль удовлетворяет предположениям контракта, а также свои контракты выполняют используемые им модули. Интеграционные тесты проверяют, что модули работают корректно вместе.

Контрактное программирование может повысить уровень повторного использования кода, поскольку обязательства модуля четко документированы. Вообще, контракт модуля можно рассматривать также как способ документации программного обеспечения.

Влияние на производительность

Условия контракта никогда не должны нарушаться во время выполнения программы без ошибок. Поэтому контракты обычно проверяются только в режиме отладки во время разработки программного обеспечения. Позже при выпуске проверки контрактов отключаются для максимальной производительности.

Во многих языках программирования контракты реализуются с помощью assert . Утверждения по умолчанию компилируются в режиме выпуска в C / C ++ и аналогичным образом деактивируются в C # и Java.

Запуск интерпретатора Python с «-O» (для «оптимизировать») в качестве аргумента аналогичным образом приведет к тому, что генератор кода Python не будет генерировать какой-либо байт-код для утверждений.

Это эффективно исключает затраты времени выполнения на утверждения в производственном коде - независимо от количества и вычислительных затрат на утверждения, используемые при разработке, - поскольку никакие такие инструкции не будут включены в производство компилятором.

Отношение к тестированию программного обеспечения

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

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

Использование утверждений можно рассматривать как форму тестового оракула , способ тестирования проекта путем реализации контракта.

Реализация в языках программирования

Поддержка DbC на языковом уровне

Языки, изначально поддерживающие средства для контрактного программирования:

  • Ada 2012
  • Clojure
  • Cobra
  • D
  • Eiffel
  • Fortress
  • Lisaac
  • Nice
  • Oxygene (бывший Chrome)
  • Racket
  • Sather
  • Scala
  • SPARK посредством статического анализа программ на Аде
  • Spec#
  • SeC (расширение языка С)
  • J@va (расширение языка Java)
  • C# (с .NET Framework 4.0)

Поддержка DbC с помощью сторонних библиотек

  • C и C++ посредством CTESK, библиотеки Contract++, препроцессора DBC for C, GNU Nana или компилятора C++ от Digital Mars.
  • C# посредством Code Contracts
  • Java посредством JavaTESK, iContract2, Contract4J, jContractor, Jcontract, C4J, CodePro Analytix, STclass, Jass preprocessor, OVal with AspectJ, Java Modeling Language (JML), SpringContracts для Spring Framework, или Modern Jass, Custos с использованием AspectJ, JavaDbC с использованием AspectJ, cofoja(разработанная компанией Google).
  • JavaScript посредством Cerny.js или ecmaDebug.
  • Lisp
    • Common Lisp с помощью макросов или протокола метаобъектов CLOS.
    • Scheme посредством расширения PLT, а именно тот факт, что любое нарушение контракта должно указывать на виновного и иметь точное объяснение.
  • Nemerle с помощью макросов.
  • Perl с помощью CPAN-модулей Class::Contract (Damian Conway) или Carp::Datum (Raphael Manfredi).
  • PHP с помощью PhpDeal
  • Python с помощью пакета zope.interface, PyDBC или Contracts for Python.
  • Ruby с помощью DesignByContract (от Brian McCallister), Ruby DBC или ruby-contract.

Общие инструменты

  • Perfect Developer посредством Perfect specification language может проверять контракт, используя статический анализ кода и генерируя программы на языках типа C++ и Java.

Пример контракта для метода и класса в PHP

Предположим, у нас есть функция divide, которая делит одно число на другое. Для нее установим:

  • Предусловие: делитель не должен быть равен нулю.
  • Постусловие: результат умножения делителя на результат должен быть равен делимому.

class Calculator
{
    // Метод для деления
    public function divide(float $numerator, float $denominator): float
    {
        // Предусловие
        assert($denominator !== 0, "Делитель не должен быть равен нулю.");

        // Основное тело метода
        $result = $numerator / $denominator;

        // Постусловие
        assert(abs(($result * $denominator) - $numerator) < 0.00001, "Результат не соответствует ожиданиям.");

        return $result;
    }
}

// Пример использования
$calc = new Calculator();

try {
    echo $calc->divide(10, 2); // Вывод: 5
    echo $calc->divide(10, 0); // Выдаст ошибку из-за деления на ноль
} catch (AssertionError $e) {
    echo "Ошибка контракта: " . $e->getMessage();
}

Объяснение кода:

  1. Предусловие проверяет, что делитель не равен нулю перед выполнением основной операции деления. Если это условие нарушено, генерируется ошибка AssertionError.
  2. Постусловие проверяет, что результат умножения делителя на result равен исходному numerator. Мы используем небольшую дельту 0.00001 для учета погрешности вычислений с плавающей запятой.
  3. Обработка ошибок помогает уловить исключение AssertionError и выводит сообщение о нарушении контракта.

Пример для инварианта класса

Для класса Calculator можно добавить инвариант, который будет гарантировать, что результат вычислений всегда будет числом, например, свойство, указывающее на последний результат операции. Инвариант для этого свойства будет состоять в том, что результат всегда должен быть числом, а не NaN или null. Это будет гарантировать, что объект класса всегда находится в "корректном" состоянии.

Добавим свойство $lastResult для хранения результата последней операции и реализуем инвариант, который будет проверяться после каждой операции.

 

interface CalculatorInterface
{
    public function add(float $a, float $b): float;
    public function subtract(float $a, float $b): float;
    public function multiply(float $a, float $b): float;
    public function divide(float $a, float $b): float;
}

class Calculator implements CalculatorInterface
{
    // Свойство для хранения последнего результата
    private float $lastResult = 0.0;

    public function add(float $a, float $b): float
    {
        $this->lastResult = $a + $b;
        $this->checkInvariant();
        return $this->lastResult;
    }

    public function subtract(float $a, float $b): float
    {
        $this->lastResult = $a - $b;
        $this->checkInvariant();
        return $this->lastResult;
    }

    public function multiply(float $a, float $b): float
    {
        $this->lastResult = $a * $b;
        $this->checkInvariant();
        return $this->lastResult;
    }

    public function divide(float $a, float $b): float
    {
        if ($b === 0) {
            throw new InvalidArgumentException("Делитель не должен быть равен нулю.");
        }
        $this->lastResult = $a / $b;
        $this->checkInvariant();
        return $this->lastResult;
    }

    // Проверка инварианта
    private function checkInvariant(): void
    {
        // Инвариант: $lastResult всегда должен быть числом
        assert(is_float($this->lastResult), "Результат последней операции должен быть числом.");
    }

    public function getLastResult(): float
    {
        return $this->lastResult;
    }
}

// Пример использования
$calculator = new Calculator();

try {
    echo "Сложение: " . $calculator->add(5, 3) . PHP_EOL;
    echo "Вычитание: " . $calculator->subtract(5, 3) . PHP_EOL;
    echo "Умножение: " . $calculator->multiply(5, 3) . PHP_EOL;
    echo "Деление: " . $calculator->divide(5, 2) . PHP_EOL;
    echo "Последний результат: " . $calculator->getLastResult() . PHP_EOL;
} catch (AssertionError $e) {
    echo "Ошибка инварианта: " . $e->getMessage();
}


Объяснение

  1. Свойство $lastResult: хранит результат последней операции, и инвариант требует, чтобы оно всегда оставалось числом.

  2. Метод checkInvariant(): проверяет, что $lastResult является числом. Этот метод вызывается после каждой операции для гарантии выполнения инварианта.

  3. Исключение: если инвариант нарушен, выбрасывается AssertionError, указывающий на проблему в работе класса.

Такой инвариант помогает убедиться, что объект класса Calculator всегда находится в "валидном" состоянии, независимо от того, какая операция была вызвана.

Таким образом, контрактное программирование в PHP с использованием assert помогает контролировать выполнение условий и обеспечивать более надежное выполнение кода.

Пример на языке D

double divide(double dividend, double divisor)
in{
  assert(divisor != 0);
}
out (result){
  assert(result < 10);
}
body{
  return dividend/divisor;
}

Эта функция сначала проверит, равен ли второй аргумент нулю, и если да, то выдаст исключение; в противном случае он разделит первый аргумент на второй. Затем она проверит, меньше ли результат 10. Если он меньше 10, функция вернет значение. В противном случае будет выдано исключение.

Модульный тест также можно считать частным случаем управления выходом .

Вау!! 😲 Ты еще не читал? Это зря!

создано: 2017-07-03
обновлено: 2024-10-29
64



Рейтиг 9 of 10. count vote: 2
Вы довольны ?:


Поделиться:

Найди готовое или заработай

С нашими удобными сервисами без комиссии*

Как это работает? | Узнать цену?

Найти исполнителя
$0 / весь год.
  • У вас есть задание, но нет времени его делать
  • Вы хотите найти профессионала для выплнения задания
  • Возможно примерение функции гаранта на сделку
  • Приорететная поддержка
  • идеально подходит для студентов, у которых нет времени для решения заданий
Готовое решение
$0 / весь год.
  • Вы можите продать(исполнителем) или купить(заказчиком) готовое решение
  • Вам предоставят готовое решение
  • Будет предоставлено в минимальные сроки т.к. задание уже готовое
  • Вы получите базовую гарантию 8 дней
  • Вы можете заработать на материалах
  • подходит как для студентов так и для преподавателей
Я исполнитель
$0 / весь год.
  • Вы профессионал своего дела
  • У вас есть опыт и желание зарабатывать
  • Вы хотите помочь в решении задач или написании работ
  • Возможно примерение функции гаранта на сделку
  • подходит для опытных студентов так и для преподавателей

Комментарии


Оставить комментарий
Если у вас есть какое-либо предложение, идея, благодарность или комментарий, не стесняйтесь писать. Мы очень ценим отзывы и рады услышать ваше мнение.
To reply

Разработка программного обеспечения и информационных систем

Термины: Разработка программного обеспечения и информационных систем