![Контрактное программирование](http://habrastorage.org/getpro/habr/post_images/853/88b/cfd/85388bcfd9c8f4c4ce656fe013ce1e92.jpg)
Введение
Сама идея контрактного программирования возникла в 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 PhpDeal\Annotation as Contract; /** * Simple trade account class * @Contract\Invariant("$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 * * @Contract\Verify("$amount>0 && is_numeric($amount)") * @Contract\Ensure("$this->balance == $__old->balance+$amount") */ public function deposit($amount) { $this->balance += $amount; } /** * Returns current balance * * @Contract\Ensure("$__result == $this->balance") * @return float */ public function getBalance() { return $this->balance; } }
Как вы заметили, все контракты описываются в виде аннотаций внутри док-блоков и содержат необходимые условия внутри самой аннотации. Не нужно менять оригинальный исполняемый код класса, он остается таким же чистым, как и код без контрактов.
Предусловия задаются с помощью аннотации Verify
и определяют те проверки, которые будут выполнены в момент вызова метода, но до выполнения самого тела метода. Предусловия работают в области видимости метода класса, поэтому имеют доступ ко всем свойствам, включая приватные, а также имеют доступ к параметрам метода.
Постусловия задаются аннотацией, имеющей стандартное название Ensure
в терминах контрактного программирования. Код имеет аналогичную область видимости, что и сам метод, помимо этого, доступны переменные $__old
с состоянием объекта до выполнения метода и переменная $__result
, содержащая в себе то значение, которое было возвращено из данного метода.
Благодаря использованию АОП стало возможным реализовать даже инварианты — они элегантно описываются в виде аннотаций Invariant
в док-блоке класса и ведут себя аналогично постусловиям, но для всех методов.
Во время экспериментов с кодом я обнаружил удивительное сходство контрактов с интерфейсам в PHP. Если стандартный интерфейс определят требования к стандарту взаимодействия с классом, то контракты позволяют описывать требования к состоянию инстанса класса. Применяя описание контракта в интерфейсе, удается описывать требования как к взаимодействию с объектом, так и к состоянию объекта, которое будет потом реализовано в классе:
use PhpDeal\Annotation as Contract; /** * Simple trade account contract */ interface AccountContract { /** * Deposits fixed amount of money to the account * * @param float $amount * * @Contract\Verify("$amount>0 && is_numeric($amount)") * @Contract\Ensure("$this->balance == $__old->balance+$amount") */ public function deposit($amount); /** * Returns current balance * * @Contract\Ensure("$__result == $this->balance") * * @return float */ public function getBalance(); }
Дальше начинается самое интересное: при создании класса и определении нужного метода любая современная IDE переносит все аннотации из описания метода в интерфейсе в сам класс. А это позволяет движку PhpDeal их находить и обеспечивать автоматическую проверку контрактов в каждом конкретном классе, реализующем данный интерфейс. Для желающих пощупать все собственными руками — можно скачать проект с гитхаба, установить все зависимости с помощью композера, настроить локальный веб-сервер на эту папку и потом просто открыть в браузере код из папки demo
Заключение
Контрактное программирование в PHP — абсолютно новая парадигма, которая может использоваться для защитного программирования, для улучшения качества кода и обеспечения читаемости контрактов, определяемых в виде требований и спецификаций. Большой плюс данной реализации в том, что код классов остается читаемым, сами аннотации читаются как документация, а также то, что в боевом режиме проверка может быть полностью отключена и не требует абсолютно никакого времени на дополнительные ненужные проверки в коде. Интересный факт: сам фреймоврк содержит лишь пару аннотаций и один класс аспекта, который связывает эти аннотации с конкретной логикой.
Благодарю за внимание!
Ссылки по теме:
ссылка на оригинал статьи http://habrahabr.ru/post/214371/
Добавить комментарий