Введение
Многие из вас, вероятно, слышали об упоминании контрактов во время обсуждения кода. Фразы наподобие «Код должен соблюдать контракт интерфейса», «Юнит-тестами тестируется не код, а контракт класса», «Тестируйте не код, а контракты» и т.п. Сегодня постараемся понять, что такое контракты и что они дают. Статья будет состоят из двух частей:
-
Введение в контракты, что такое контракты, свойство контрактов и т.д
-
Примеры использование контрактов в коде и объяснение о том, что определенные выражения об контрактах (пример: «Тестировать нужно контракты, а не реализацию» и т.п).
В статье не будет упоминания о том, кем был выведен этот принцип и т.д, эта информация все равно забывается, если вам нужно об этом знать, можно прочитать об этом в википедии
Содержание:
-
Что такое контракты, аналогие в реальном мире
-
Что такое контракт в коде
-
Из чего состоит контракт: постусловия, преудсловия и инварианты
-
Виды контрактов
Что такое контракт?
Если быть честным, нету определения прочитав, который можно сразу понять, что такое контракты, а с определения из википедии, ничего мало что понятно. Походу статьи постараемся понять, как все это устроено. Начало будет с аналогии из реального мира.
Контракты в реальном мире, это когда у нас есть некое соглашение в котором прописываются пункты которые должны соблюдать стороны и то, как будут реагировать если условия не выполнены. Обычно контракт состоит из примерно таких пунктов:
-
Что мы ожидаем от того, с кем мы заключили контракт, т.е то что он должен выполнить
-
Какие последствия будут если наш контракт будет нарушен
-
И т.д
То есть, контракт — это когда мы описываем ряд условии, которые должны придерживаться обе стороны, если что-то будет идти не так по соглашению, то соответствующим образом среагировать на это.
И это понятие контракт из реального мира, попытались внедрить в код. Теперь объясним что такое контракт в рамках кода.
Что такое контракт в коде?
Контракт — это описание “правил” взаимодействия сущностей друг с другом в рамках кода. Примеры «правил» для класса бывают примерно такими: “метод этого класса обязуется предоставить результат, если будут выполнены все условия”. Если привести более понятный пример с кодом:
Допустим, у нас есть класс ProductService и у него метод uploadImage, и если попытаться составить контракт к этому методу, то он будет выглядит таким:
Метод выполнится если:
-
Передан аргумент file — который является типом N-класса
-
В случае передачи неправильного типа — выходит ошибка
-
-
Переданный аргумент file имеет вес меньше 5мб
-
В случае передачи неправильного типа — выходит ошибка (исключение)
-
-
И т.д
Т.е мы описали контракт для этого метода, в котором указали, что методу нужно передать определенное количество аргументов и они должны иметь определенные характеристики, в случае не соблюдения «правил», то программа завершится с ошибкой.
В настоящее время, прописать контракт для метода становится легче, потому что в языки введены, type hint (указание типов передаваемых аргументов), возможность указать тип возвращаемого результата и т.д.
В последствии, писать проверки в теле метода на подобии: аргумент “a” соответствует типу “б” теряет актуальность, все это теперь работает на уровне ядра языка. Но из-за этого не снизилась значимость контрактов, потому что не все условия можно указать в аргументах.
Свойства контракта:
Обычно контракт состоит из некоторых свойств, с помощью которого описывается контракт:
-
Предусловия
-
Постусловия
-
Инварианты
Предусловия — это условие которое, мы выполняем в теле метода, до выполнение основного действия. Все это описывается именно в методе, с помощью простых if и т.д. Пример (будет псевдокод, укороченный PHP):
class ProductService { public function uploadImage(FileUpload file): bool { // Постусловие if (file.size < 5000) { throw Expection('File size is more than 5mb'); } // Выполнение основного кода // .... } }
Думаю из примера понятно, что тут ничего сложного нету, предусловия — это проверки перед выполнение основного кода метода.
Постусловия — это такие же условия как и предусловия только наоборот, под “наоборот”, я имею ввиду, что они выполняются после выполнение основного кода метода, перед возвращением результата. Тот же пример:
class ProductService { public function uploadImage(FileUpload file): bool { // Постусловие if (file.size < 5000) { throw Expection('File size is more than 5mb'); } // Выполнение основного кода result = ......; // Предусловия if (!result) { throw Expection('Fail during upload file'); } return true; } }
Инварианты — это проверки на то, что состояние объекта является валидным после изменения. Допустим пример, у нас есть класс Balance (Баланс) и метод который обновляет значение баланса. Инвариант в этом случае будет проверка состояния объекта (в нашем случае баланса), находится ли он в довзоленном нашей системой состоянии.
Пример проверки на состояние:
Мы обновили баланс, тем самым у нас состояние объекта изменилось и инвариант в этом случае будет проверка на то, что наш баланс не меньше 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 RuntimeException('...'); } // Основной код .... if (!result) { throw RuntimeException('...'); } return result.response; } }
Клиент (тот, кто будет использовать наш код), посмотрев на наш класс, сможет понять какой контракт он должен соблюдать для метода, какие параметры он должен передавать, каких преудсловии он должен придерживаться в методе и тем самым подкорректирует свой код, под нашу реализацию.
Из двух этих примеров, ясно, что первая реализация, через интерфейс, менее конкретна, чем вторая, в котором полностью видно, как нужно взаимодействовать с классом.
Офф-топ. Обычно используются оба вида контрактов вместе, сперва описывается контракт с помощью интерфейса, а потом в реализации, имплементится интерфейс и контракт конкретизируется. Другие примеры использования контрактов, будут представлены во втором части статьи.
Итог:
Контракты в программировании — это описание то, как будет ввести себя модуль (класс, метод и т.д), с помощью контрактов мы получаем удобства, такие как:
-
Понимание того, какого рода аргументы нужно передать в метод
-
Понимание того, что нас ожидает в случае ошибки (какого рода ошибки будут кидаться)
-
Обязывают разработчиков писать код в рамках контракта
-
Понимание того, как нужно взаимодействовать с нашим кодом
Их можно описать и с помощью сигнатуры методов (указание type hint, указание возвращаемых значении), и с помощью блока-комментария. Контракты имеют свойства, которые нужно соблюдать, чтобы контракт был целостным, это — предусловия, постусловия и инварианты.
Описание контрактов можно разделить на два вида: первый — описание контракта с помощью интерфейса, второе — описание контракта с помощью реализации класса.
В первом случае, мы не можем использовать свойства контракта (предусловия, постусловия и инварианты), т.к интерфейс не может иметь тело метода, этот тип контракта можно назвать — неполноценным или неконкретизированным
Втором случае, контракт более конкретизируется из-за возможности описать тело метода, тем самым соблюдаются все свойства контракта, этот тип контракта можно назвать — полноценным или расширенным.
Вот на этом все, если заметили ошибку или неточность пишите в комментариях. Можете также писать о вашем понимании контрактов.
Советую прочитать эти ресурсы для хорошего понимания материала:
-
Прочитать книгу “Адаптивный код”, глава по SOLID, там пролистать до третьего принципа, O — OCP и там описываются контракты
-
https://youtu.be/oMi2ReGtXrI — посмотреть это видео, более подробно описывает контракты и показывает пример на PHP.
-
https://habr.com/en/post/214371/ — другая статья, про контракты и как они реализованы в PHP, также даётся список библиотек которые позволяют описать свойства контракта в док-блоке
-
Прочитать про «Контрактное программирование» и как реализуется в других языках
ссылка на оригинал статьи https://habr.com/ru/post/657131/
Добавить комментарий