Введение
Многие из вас, вероятно, слышали об упоминаний контрактов во время обсуждения кода. Фразы наподобии: "Код должен соблюдать контракт интерфейса", "Юнит-тестами тестируется не код, а контракт класса", "Тестируйте не код, а контракты" и т.п. Сегодня постараемся понять: что такое контракты и что они дают. Статья будет состоят из двух частей:
Введение в контракты, что такое контракты, свойство контрактов и т.д
Примеры использование контрактов в коде и объяснение о том, что определенные выражения об контрактах (пример: "Тестировать нужно контракты, а не реализацию" и т.п).
В статье не будет упоминания о том, кем был выведен этот принцип и т.д, эта информация все равно забывается, если вам нужно об этом знать, можно прочитать об этом в википедии.
Содержание:
Что такое контракты, аналогии в реальном мире
Что такое контракт в коде
Из чего состоит контракт: постусловия, предусловия и инварианты
Виды контрактов
Что такое контракт?
Если быть честным, нет определения, прочитав которое, можно сразу понять, что такое контракты, а с определения из википедии, ничего мало что понятно. Походу статьи постараемся понять, как все это устроено. Начало будет с аналогии из реального мира.
Контракты в реальном мире - это когда у нас есть некое соглашение в котором прописываются пункты, которые должны соблюдать стороны и то, как будут реагировать, если условия не выполнены. Обычно контракт состоит из примерно таких пунктов:
Что мы ожидаем от того, с кем мы заключили контракт, т.е то что он должен выполнить
Какие последствия будут если наш контракт будет нарушен
И т.д
То есть, контракт - это когда мы описываем ряд условий, которые должны придерживаться обе стороны, если что-то будет идти не так по соглашению, то соответствующим образом среагировать на это.
И это понятие контракт из реального мира, попытались внедрить в код. Теперь объясним что такое контракт в рамках кода.
Что такое контракт в коде?
Контракт - это описание “правил” взаимодействия сущностей друг с другом в рамках кода. Примеры "правил" для класса бывают примерно такими: “метод этого класса обязуется предоставить результат, если будут выполнены все условия”. Если привести более понятный пример с кодом:
Допустим, у нас есть класс ProductService и у него метод uploadImage, и если попытаться составить контракт к этому методу, то он будет выглядит так:
Метод выполнится если:
-
Передан аргумент file - который является типом N-класса
В случае передачи неправильного типа - выходит ошибка
-
Переданный аргумент file имеет вес меньше 5мб
В случае передачи неправильного типа - выходит ошибка (исключение)
И т.д
Т.е мы описали контракт для этого метода, в котором указали, что методу нужно передать определенное количество аргументов и они должны иметь определенные характеристики, в случае не соблюдения "правил", то программа завершится с ошибкой.
В настоящее время, прописать контракт для метода становится легче, потому что в языки введены, type hint (указание типов передаваемых аргументов), возможность указать тип возвращаемого результата и т.д.
В последствии, писать проверки в теле метода на подобии: аргумент “a” соответствует типу “б” теряет актуальность, все это теперь работает на уровне ядра языка. Но из-за этого не снизилась значимость контрактов, потому что не все условия можно указать в аргументах.
Свойства контракта:
Обычно контракт состоит из некоторых свойств, с помощью которого описывается контракт:
Предусловия
Постусловия
Инварианты
Предусловия - это условие которое, мы выполняем в теле метода, до выполнение основного действия. Все это описывается именно в методе, с помощью простых if и т.д. Пример (будет псевдокод, укороченный PHP):
class ProductService
{
public function uploadImage(FileUpload file): bool
{
// Предусловия
if (file.size > 5000) {
throw new Exception('File size is more than 5mb');
}
// Выполнение основного кода
// ....
}
}
Думаю из примера понятно, что тут ничего сложного нету, предусловия - это проверки перед выполнение основного кода метода.
Постусловия - это такие же условия как и предусловия только наоборот. Под “наоборот”, я имею ввиду, что они выполняются после выполнение основного кода метода, перед возвращением результата. Тот же пример:
class ProductService
{
public function uploadImage(FileUpload file): bool
{
// Предусловия
if (file.size > 5000) {
throw new Exception('File size is more than 5mb');
}
// Выполнение основного кода
result = ......;
// Постусловия
if (!result) {
throw new Exception('Fail during upload file');
}
return true;
}
}
Инварианты - это проверки на то, что состояние объекта является валидным после изменения. Допустим пример, у нас есть класс Balance (Баланс) и метод который обновляет значение баланса. Инвариант в этом случае будет проверка состояния объекта (в нашем случае баланса), находится ли он в дозволенном, нашей системой состоянии.
Пример:
class Balance
{
protected balance;
/**
* @param int value
* @throw Exception
* @return bool
*/
public function changeBalance(int value): bool
{
// ....
// Основной код ...
// Инвариант
this.validateBalance();
return result.response;
}
/**
* @throw Exception
* @return void
*/
public function validateBalance(): void
{
if (this.balance < 0) {
throw new Exception('Balance cannot be less than 0');
}
}
}
Если мы обновим наш баланс с помощью метода changeBalance
, то состояние нашего объекта изменится, и инвариант в этом случае будет проверка на то, что наш баланс не меньше 0. То есть теперь определение будет более понятно, инварианты — это специальные условия, которые описывают целостное состояние объекта.
Важной особенностью инвариантов является то, что они проверяются всегда после вызова любого публичного метода в классе и после вызова конструктора. Так как контракт определяет состояние объекта, а публичные методы — единственная возможность изменить состояние извне, то мы получаем полную спецификацию объекта
Если рассматривать контракты в PHP, их там можно придерживаться только с некоторыми ограничения, проблема в том, что везде писать проверки на инварианты сложно (но есть реализации библиотек которые работают с помощью Reflection API, применяя парсинг док-блоков). Обычно программирование на контрактах называются “Контрактное программирование”.
Разные виды контрактов:
По-моему мнению, контракт можно описать двумя способами:
Контракт который описан с помощью интерфейса
Контракт который описан с помощью реализации без интерфейса или с интерфейсом.
Сейчас поговорим об их различии.
Контракт который описан с помощью интерфейса. Он имеет некоторые ограничения. Как мы знаем, в интерфейсе можно описать только сигнатуру метода, возвращаемый тип и т.д, но не тело самого метода. Из-за этого, у нас бывают некоторые ограничения, при описании контракта для метода, а именно, мы не можем определить в интерфейсе, предусловия, постусловия и инварианты. Рассмотрим пример:
interface ModuleA
{
/**
* @param int value
* @throw RuntimeException
* @return boolean
*/
public function update(int value): bool;
}
В этом примере, мы описали контракт с помощью док-блока и с помощью type hint. Контракт метода звучит так:
Передаваемый параметр value, должен иметь тип int
Возвращаемое значение метода будет типом boolean
Метод может выкинуть исключение RuntimeException
Как мы видим, мы описали контракт, но он неполноценен. Неполноценен, из-за того что, нету возможность описать свойства контракта (предусловия, постусловия и инварианты). Теперь рассмотрим пример со вторым видом контракта, которые лишен этих минусов.
Контракт с помощью реализации. Этот тип контракта, я называю полноценным, ибо он лишен минусов прошлого контракта. В отличие от предыдущего примера, мы можем описывать свойства контракта, а именно - предусловия, постусловия и инварианты. Рассмотрим пример такого контракта:
class ModuleA
{
protected value;
/**
* @param int value
* @throw RuntimeException
* @return boolean
*/
public function update(int value): bool
{
if (value.length < 40) {
throw new RuntimeException('...');
}
// Основной код ....
if (!result) {
throw new RuntimeException('...');
}
return result.response;
}
}
Клиент (тот, кто будет использовать наш код), посмотрев на наш класс, сможет понять какой контракт он должен соблюдать для метода, какие параметры он должен передавать, каких предусловии он должен придерживаться в методе и тем самым подкорректирует свой код, под нашу реализацию.
Из двух этих примеров, ясно, что первая реализация, через интерфейс, менее конкретна, чем вторая, в котором полностью видно, как нужно взаимодействовать с классом.
Офф-топ. Обычно используются оба вида контрактов вместе, сперва описывается контракт с помощью интерфейса, а потом в реализации, имплементится интерфейс и контракт конкретизируется. Другие примеры использования контрактов, будут представлены во втором части статьи.
Итог:
Контракты в программировании - это описание то, как будет ввести себя модуль (класс, метод и т.д), с помощью контрактов мы получаем удобства, такие как:
Понимание того, какого рода аргументы нужно передать в метод
Понимание того, что нас ожидает в случае ошибки (какого рода ошибки будут кидаться)
Обязывают разработчиков писать код в рамках контракта
Понимание того, как нужно взаимодействовать с нашим кодом
Их можно описать и с помощью сигнатуры методов (указание type hint, указание возвращаемых значении), и с помощью блока-комментария. Контракты имеют свойства, которые нужно соблюдать, чтобы контракт был целостным, это - предусловия, постусловия и инварианты.
Описание контрактов можно разделить на два вида: первый - описание контракта с помощью интерфейса, второй - описание контракта с помощью реализации класса.
В первом случае, мы не можем использовать свойства контракта (предусловия, постусловия и инварианты), т.к интерфейс не может иметь тело метода, этот тип контракта можно назвать - неполноценным или неконкретизированным
Втором случае, контракт более конкретизируется из-за возможности описать тело метода, тем самым соблюдаются все свойства контракта, этот тип контракта можно назвать - полноценным или расширенным.
Вот на этом все, если заметили ошибку или неточность пишите в комментариях. Можете также писать о вашем понимании контрактов.
Советую прочитать эти ресурсы для хорошего понимания материала:
Прочитать книгу “Адаптивный код”, глава по SOLID, там пролистать до третьего принципа, O - OCP и там описываются контракты
https://youtu.be/oMi2ReGtXrI - посмотреть это видео, более подробно описывает контракты и показывает пример на PHP.
https://habr.com/en/post/214371/ - другая статья, про контракты и как они реализованы в PHP, также даётся список библиотек которые позволяют описать свойства контракта в док-блоке
Прочитать про "Контрактное программирование" и как реализуется в других языках
Комментарии (7)
iStyx
24.03.2022 01:34+3В коде комментарии к пред- и постусловиям перепутаны местами. А ещё в PHP переменные начинаются с символа
$
.А ещё
throw Expection(...)
должно бытьthrow new Exception(...)
.
Octember
24.03.2022 06:44if (file.size < 5000) { throw Expection('File size is more than 5mb'); }
Многовато ошибок для трех строк. Переменные начинаются с $, не хватает new, не Expection, а Exception, логика неверная, if проверяет, что размер меньше 5000, а не больше
Пример проверки на состояние:
Пример куда-то потерялся.
В целом статья неплохая, но ошибки очень мешают воспринимать информациюobitel_016 Автор
24.03.2022 07:12Благодарю за замечания. Исправлю, допустил ошибки из-за невнимательности.
Maksclub
25.03.2022 08:55похоже на псевдокод, тк точка для обращения к переменной класса, видимо Джаву имитировали некоторым псевдокодом, это нормально
Steepik
24.03.2022 15:50Очень не привычно когда пишут не верный синтаксис :)
obitel_016 Автор
24.03.2022 15:53Хотел не привязываться к конкретному языку, но синтаксис все равно больше на PHP похож, только отсутствует всеми любимый - '$'
DVF
Мы не слышали об упоминаний контрактов. Аналогие в реальном мире нет. Продоёте?
Вычитывайте тексты, пожалуйста.