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

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

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/214371/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *