В реальной жизни мы повсюду сталкиваемся с различными контрактами: при устройстве на работу, при выполнении работ, при подписании взаимных соглашений и многими другими. Юридическая сила контрактов гарантирует нам защиту интересов и не допускает их нарушения без последствий, что дает нам уверенность в том, что те пункты, которые описаны в контракте — будут выполнены. Эта уверенность помогает нам планировать время, планировать расходы, а также планировать необходимые ресурсы. А что если и программный код будет описываться контрактами? Интересно? Тогда добро пожаловать под кат!
Введение
Сама идея контрактного программирования возникла в 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 — абсолютно новая парадигма, которая может использоваться для защитного программирования, для улучшения качества кода и обеспечения читаемости контрактов, определяемых в виде требований и спецификаций. Большой плюс данной реализации в том, что код классов остается читаемым, сами аннотации читаются как документация, а также то, что в боевом режиме проверка может быть полностью отключена и не требует абсолютно никакого времени на дополнительные ненужные проверки в коде. Интересный факт: сам фреймоврк содержит лишь пару аннотаций и один класс аспекта, который связывает эти аннотации с конкретной логикой.
Благодарю за внимание!
Ссылки по теме:
- Wikipedia — контрактное программирование
- Фреймворк PhpDeal для контрактного программирования в PHP
- Фреймворк Go! AOP для аспектно-ориентированного программирования в PHP
Автор: NightTiger