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

в 5:56, , рубрики: php, Веб-разработка, контракты, разработка, метки: , ,

Контрактное программирование В реальной жизни мы повсюду сталкиваемся с различными контрактами: при устройстве на работу, при выполнении работ, при подписании взаимных соглашений и многими другими. Юридическая сила контрактов гарантирует нам защиту интересов и не допускает их нарушения без последствий, что дает нам уверенность в том, что те пункты, которые описаны в контракте — будут выполнены. Эта уверенность помогает нам планировать время, планировать расходы, а также планировать необходимые ресурсы. А что если и программный код будет описываться контрактами? Интересно? Тогда добро пожаловать под кат!

Введение

Сама идея контрактного программирования возникла в 90-х годах у Бертрана Мейера при разработке объектно-ориентированного языка программирования Eiffel. Суть идеи Бертрана была в том, что нужно было иметь инструмент для описания формальной верификации и формальной спецификации кода. Такой инструмент давал бы конкретные ответы: «метод обязуется сделать свою работу, если вы выполните условия, необходимые для его вызова». И контракты как нельзя лучше подходили для данной роли, потому что позволяли описать что будет получено от системы (спецификация) в случае соблюдения предусловий (верификация). С тех пор появилось множество реализаций данной методики программирования как на уровне конкретного языка, так и в виде отдельных библиотек, позволяющих задавать контракты и проводить их верификацию с помощью внешнего кода. К сожалению, в PHP нет поддержки контрактного программирования на уровне самого языка, поэтому реализация может быть выполнена только с помощью сторонних библиотек.

Контракты в коде

Так как контрактное программирование было разработано для объектно-ориентированного языка, то не сложно догадаться, что основными рабочими элементами для контрактов являются классы, методы и свойства.

Предусловия

Самым простым вариантом контракта являются предусловия — требования, которые должны быть выполнены перед конкретным действием. В рамках ООП все действия описываются методами в классах, поэтому предусловия применяются к методами, а их проверка происходит в момент вызова метода, но до выполнения самого тела метода. Очевидное использование — проверка валидности переданных параметров в метод, их структуры и корректности. То есть с помощью предусловий мы описываем в контракте все то, с чем мы точно работать не будем. Это же здорово!

Чтобы не быть голословным, давайте рассмотрим пример:

class BankAccount
{
    protected $balance = 0.0;

    /**
     * Deposits fixed amount of money to the account
     *
     * @param float $amount
     */
    public function deposit($amount)
    {
        if ($amount <= 0 || !is_numeric($amount)) {
            throw new InvalidArgumentException("Invalid amount of money");
        }
        $this->balance += $amount;
    }
}

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

В плане реализации предусловий, в PHP существует специальная конструкция для проверки утверждений — assert(). Большое ее преимущество в том, что проверки можно отключать в боевом режиме, заменяя весь код команды на единственный NOP. Давайте посмотрим на то, как можно описать предусловие с помощью данной конструкции:

class BankAccount
{
    protected $balance = 0.0;

    /**
     * Deposits fixed amount of money to the account
     *
     * @param float $amount
     */
    public function deposit($amount)
    {
        assert('$amount>0 && is_numeric($amount); /* Invalid amount of money /*');

        $this->balance += $amount;
    }
}

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

Постусловия

Следующая категория контрактов — постусловия. Как можно догадаться из названия, данный тип проверки выполняется после того, как было выполнено тело метода, но до момента возврата управления в вызывающий код. Для нашего метода deposit из примера мы можем сформировать следующее постусловие: баланс счета после вызова метода должен равняться предыдущему значению баланса плюс величина пополнения. Осталось дело за малым — описать все это в виде утверждения в коде. Но вот здесь нас поджидает первое разочарование: как же сформировать это требование в коде, ведь мы сперва изменим баланс в теле самого метода, а потом попытаемся проверить утверждение, где нужно старое значение баланса. Здесь может помочь клонирование объекта перед выполнением кода и проверка пост-условий:

class BankAccount
{
    protected $balance = 0.0;

    /**
     * Deposits fixed amount of money to the account
     *
     * @param float $amount
     */
    public function deposit($amount)
    {
        $__old = clone $this;
        assert('$amount>0 && is_numeric($amount); /* Invalid amount of money /*');

        $this->balance += $amount;
        assert('$this->balance == $__old->balance+$amount; /* Contract violation /*');
    }
}

Еще одно разочарование поджидает нас при описании постусловий для методов, возвращающих значение:

class BankAccount
{
    protected $balance = 0.0;

    /**
     * Returns current balance
     */
    public function getBalance()
    {
        return $this->balance;
    }
}

Как здесь описать контрактное условие, что метод должен возвращать текущий баланс? Так как пост-условие выполняется после тела метода, то мы наткнемся на return раньше, чем сработает наша проверка. Поэтому придется изменить код метода, чтобы сохранить результат в переменную $__result и сравнить потом с $this->balance:

class BankAccount
{
    protected $balance = 0.0;

    /**
     * Returns current balance
     */
    public function getBalance()
    {
        $__result = $this->balance;
        assert('$__result == $this->balance; /* Contract violation /*');
        
        return $__result;
    }
}

И это для простого метода, не говоря уже о том случае, когда метод большой и в нем несколько точек возврата. Как вы уже догадались, на этом этапе идеи об использовании контрактного программирования в проекте на PHP быстро умирают, так как язык не поддерживает необходимых управляющих конструкций. Но есть решение! И о нем будет написано ниже, наберитесь немного терпения.

Инварианты

Нам осталось рассмотреть еще один важный тип контрактов: инварианты. Инварианты — это специальные условия, которые описывают целостное состояние объекта. Важной особенностью инвариантов является то, что они проверяются всегда после вызова любого публичного метода в классе и после вызова конструктора. Так как контракт определяет состояние объекта, а публичные методы — единственная возможность изменить состояние извне, то мы получаем полную спецификацию объекта. Для нашего примера хорошим инвариантом может быть условие: баланс счета никогда не должен быть меньше нуля. Однако, с инвариантами в PHP дело обстоит еще хуже чем с постусловиями: нет никакой возможности легко добавить проверку во все публичные методы класса, чтобы после вызова любого публичного метода можно было проверить необходимое условие в инварианте. Также нет возможности обращаться к предыдущему состоянию объекта $__old и возвращаемому результату $__result. Без инвариантов нет контрактов, поэтому долгое время не было никаких средств и методик для реализации данного функционала.

Новые возможности

Встречайте, PhpDeal — экспериментальный DbC-фреймворк для контрактного программирования в PHP.
После того, как был разработан фреймворк Go! AOP для аспектно-ориентированного программирования в PHP, у меня в голове крутились мысли насчет автоматической валидации параметров, проверки условий и много-много другого. Триггером к созданию проекта для контрактного программирования послужило обсуждение на PHP.Internals . Удивительно, но с помощью АОП задача решалась всего в пару действий: нужно было описать аспект, который будет перехватывать выполнение методов, помеченных с помощью контрактных аннотаций, и выполнять нужные проверки до или после вызова метода.

Давайте посмотрим на то, как можно использовать контракты с помощью этого фреймворка:

use PhpDealAnnotation as Contract;

/**
 * Simple trade account class
 * @ContractInvariant("$this->balance > 0")
 */
class Account implements AccountContract
{

    /**
     * Current balance
     *
     * @var float
     */
    protected $balance = 0.0;

    /**
     * Deposits fixed amount of money to the account
     *
     * @param float $amount
     *
     * @ContractVerify("$amount>0 && is_numeric($amount)")
     * @ContractEnsure("$this->balance == $__old->balance+$amount")
     */
    public function deposit($amount)
    {
        $this->balance += $amount;
    }

    /**
     * Returns current balance
     *
     * @ContractEnsure("$__result == $this->balance")
     * @return float
     */
    public function getBalance()
    {
        return $this->balance;
    }
}

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

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

Постусловия задаются аннотацией, имеющей стандартное название Ensure в терминах контрактного программирования. Код имеет аналогичную область видимости, что и сам метод, помимо этого, доступны переменные $__old с состоянием объекта до выполнения метода и переменная $__result, содержащая в себе то значение, которое было возвращено из данного метода.

Благодаря использованию АОП стало возможным реализовать даже инварианты — они элегантно описываются в виде аннотаций Invariant в док-блоке класса и ведут себя аналогично постусловиям, но для всех методов.

Во время экспериментов с кодом я обнаружил удивительное сходство контрактов с интерфейсам в PHP. Если стандартный интерфейс определят требования к стандарту взаимодействия с классом, то контракты позволяют описывать требования к состоянию инстанса класса. Применяя описание контракта в интерфейсе, удается описывать требования как к взаимодействию с объектом, так и к состоянию объекта, которое будет потом реализовано в классе:

use PhpDealAnnotation as Contract;

/**
 * Simple trade account contract
 */
interface AccountContract
{
    /**
     * Deposits fixed amount of money to the account
     *
     * @param float $amount
     *
     * @ContractVerify("$amount>0 && is_numeric($amount)")
     * @ContractEnsure("$this->balance == $__old->balance+$amount")
     */
    public function deposit($amount);

    /**
     * Returns current balance
     *
     * @ContractEnsure("$__result == $this->balance")
     *
     * @return float
     */
    public function getBalance();
}

Дальше начинается самое интересное: при создании класса и определении нужного метода любая современная IDE переносит все аннотации из описания метода в интерфейсе в сам класс. А это позволяет движку PhpDeal их находить и обеспечивать автоматическую проверку контрактов в каждом конкретном классе, реализующем данный интерфейс. Для желающих пощупать все собственными руками — можно скачать проект с гитхаба, установить все зависимости с помощью композера, настроить локальный веб-сервер на эту папку и потом просто открыть в браузере код из папки demo

Заключение

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

Благодарю за внимание!

Ссылки по теме:

  1. Wikipedia — контрактное программирование
  2. Фреймворк PhpDeal для контрактного программирования в PHP
  3. Фреймворк Go! AOP для аспектно-ориентированного программирования в PHP

Автор: NightTiger

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js