Что описывает понятие контракт в ооп
Проектирование по контракту
Два года назад мне посчастливилось побывать на лекции замечательного человека, одного из разработчиков языка Eiffel, Бертрана Мейера. Он читал в нашем университете (СПб ГУ ИТМО) лекцию о довольно интересной концепции проектирования ПО. Называется она «проектирование по контракту». Суть этой концепции я попытаюсь описать ниже.
Вот, например, когда вы с клиентом договариваетесь о совместной работе, то вы заключаете контракт. Т.е. вы описываете обязанности обоих сторон и возможные последствия в случае неожиданных ситуаций. Данный подход можно применить и к разработке ПО, где в качестве сторон выступают программные модули.
Проектирование по контракту является довольно простой, но, в то же время, мощной методикой, основанной на документировании прав и обязанностей программных модулей для обеспечения корректности программы. Я считаю, что корректная программа – это программа, которая выполняет не больше и не меньше того, на что она претендует.
Центральными фигурами этой концепции являются утверждения (assertions) – это булевы выражения, описывающие состояние программы. Можно выделить три основных типа утверждений: предусловия, постусловия и инварианты класса.
Предусловия.
Предусловия – это требования подпрограммы, т.е. то, что обязано быть истинным для выполнения подпрограммы. Если данные предусловия нарушены, то подпрограмма не должна вызываться ни в коем случае. Вся ответственность за передачу «правильных» данных лежит на вызывающей программе.
Этот подход противоречит многим концепция, которым учат в огромном количестве учебников. Там постоянная проверка вынесена во главу угла. Проектирование по контракту утверждает обратное – лишние проверки могут только навредить. Вообще, принцип проектирования по контракту «смотрит» на проектирования с позиции «Сложность – главный враг качества» (но об этом в следующий раз 😉 ).
Постусловия
Постусловия выражают состояния «окружающего мира» на момент выполнения подпрограммы. Т.е. это условия, которые гарантируются самой подпрограммой. Кроме того, наличие постусловия в подпрограмме гарантирует ее завершение (т.е. не будет бесконечного цикла, например).
Инварианты класса
Инварианты – это глобальные свойства класса. Они определяют более глубокие семантические свойства и ограничения целостности, характеризующие класс. Класс гарантирует, что данное условие всегда истинно с точки зрения вызывающей программы.
Попробую сформулировать основную идею, которой пользуюсь я:
«Если клиент, вызывающий подпрограмму, выполняет все предусловия, то вызываемая подпрограмма обязуется, что после ее выполнения все постусловия и инварианты будут истинными».
Как и в реальной жизни, при нарушении одного из пунктов контракта наступает заранее обговоренная и согласованная мера. В мире разработки ПО это может быть возбуждение исключения или завершение работы программы. В любом случае, вы будете точно знать, что нарушение условий контракта есть ошибка.
Хотелось бы сделать важное замечание. Необходимо понимать, что описанное чуть выше происходит не всегда. Поэтому, предусловия не должны использоваться для таких процедур, как, например, проверка вводимых пользователем данных.
Чтобы не быть голословным приведу пример (здесь и далее весь код написан на C#).
Давайте рассмотрим такую ситуацию: пользователь вводит код зап. части по каталогу и хочет получить информацию об этой детали. Известно, что код состоит из 9 символов. Вот классический пример реализации данной функции:
Во многих классических учебниках по созданию «качественного ПО» этот пример назвали бы отличным. Но вот с точки зрения принципа проектирования по контракту этот пример является ошибочным.
Начнем с того, что проверку на валидность значения атрибута id должна осуществлять вызывающая программа. Ведь именно она (вызывающая программа) может воспользоваться несколькими вариантами: завершить работу, выдать предупреждение и начать считывать новое число. А может существует возможность вводить только последние 4 цифры, а первые пять программа сформирует исходя из VIN-номера автомобиля. В любом случае, какой бы вариант не использовался, он ни как не связан с функцией GetComponentInfo().
Тогда исходный пример перепишем в следующем виде
public ComponentInfo GetComponentInfo( string id)
<
return componentProvider.GetComponent(id);
>
А вот дальше начинается самое интересное :). Если уж мы заявили, что данная функция возвращает объект типа ComponentInfo, то мы должны обеспечить это. Ведь метод GetComponent объекта componentProvider может вернуть значение null. И тогда уже вызывающей программе придется делать проверку на null-значение, иначе можем «нарваться» на «object reference» исключение. Т.е. пример стоит переписать так:
По крайней мере, так говорится во многих статьях и примерах. НО. Давайте рассуждать логически. Если уж мы используем принцип проектирования по контракту, то, опираясь на мое «золотое правило», мы можем быть уверены, что метод GetComponent() объекта componentProvider вернет нам истинное значение (т.к. его параметр по определению истинный). Поэтому, я не вижу смысла загромождать программу лишним кодом. Но с другой стороны, объект типа ComponentProvider может быть спроектирован сторонним разработчиком, который не придерживался принципа проектирования по контракту. Вот тут и встает дилемма. Вот мой совет для данной ситуации – если вы вызываете подпрограмму, которая была написана вами, то не пишите лишнего кода. Доверяйте себе. Но если вы вызываете подпрограмму, написанную сторонним разработчиком, и вы не уверены в ней, то произведите проверку. Самый наглядный пример – функция извлечения квадратного корня Math.Sqrt(). Понятно, что нельзя извлечь квадратный корень из отрицательного числа, но если в данную функцию передать отрицательное число, то никакого исключения сгенерировано не будет, а вернется значение типа NaN.
Все приведенные примеры основываются на некоторых ваших (команды разработчиков) соглашениях. Но существуют и специальные расширения для различных языков программирования. Например, препроцессор iContract для Java или расширение eXtensible C#.
Самое главное, что использование принципа проектирования по контракту поможет вам обеспечить автоматическое тестирование вашего кода.
Данную статью можно назвать введением в принцип проектирования по контракту. Если появится интерес со стороны пользователей, то я продолжу серию об этом принципе. Ведь все, что я описал – это лишь верхушка айсберга.
Контрактное программирование в PHP
Введение
Сама идея контрактного программирования возникла в 90-х годах у Бертрана Мейера при разработке объектно-ориентированного языка программирования Eiffel. Суть идеи Бертрана была в том, что нужно было иметь инструмент для описания формальной верификации и формальной спецификации кода. Такой инструмент давал бы конкретные ответы: «метод обязуется сделать свою работу, если вы выполните условия, необходимые для его вызова». И контракты как нельзя лучше подходили для данной роли, потому что позволяли описать что будет получено от системы (спецификация) в случае соблюдения предусловий (верификация). С тех пор появилось множество реализаций данной методики программирования как на уровне конкретного языка, так и в виде отдельных библиотек, позволяющих задавать контракты и проводить их верификацию с помощью внешнего кода. К сожалению, в PHP нет поддержки контрактного программирования на уровне самого языка, поэтому реализация может быть выполнена только с помощью сторонних библиотек.
Контракты в коде
Так как контрактное программирование было разработано для объектно-ориентированного языка, то не сложно догадаться, что основными рабочими элементами для контрактов являются классы, методы и свойства.
Предусловия
Самым простым вариантом контракта являются предусловия — требования, которые должны быть выполнены перед конкретным действием. В рамках ООП все действия описываются методами в классах, поэтому предусловия применяются к методами, а их проверка происходит в момент вызова метода, но до выполнения самого тела метода. Очевидное использование — проверка валидности переданных параметров в метод, их структуры и корректности. То есть с помощью предусловий мы описываем в контракте все то, с чем мы точно работать не будем. Это же здорово!
Чтобы не быть голословным, давайте рассмотрим пример:
Мы видим, что метод пополнения баланса в неявном виде требует числового значения величины суммы пополнения, которая также должна быть строго больше нуля, в противном случае будет выброшено исключение. Это типичный вариант предусловия в коде. Однако он имеет несколько минусов: мы вынуждены искать глазами эти проверки и, находясь в другом классе, не можем быстро оценить наличие/отсутствие таких проверок. Также, без наличия явного контракта, нам придется помнить о том, что в коде класса есть необходимые проверки входящих аргументов и нам не надо волноваться за них. Еще один фактор: эти проверки выполняются всегда, как в режиме разработки, так и боевом режиме работы приложения, что незначительно влияет в отрицательную сторону на скорость работы приложения.
Хочу обратить внимание на то, что предусловия в рамках контрактов служат для проверки логки работы программы и не отвечают за валидность параметров, переданных от клиента. Контракты отвечают только за взаимодействие внутри самой системы. Поэтому пользовательский ввод должен всегда фильтроваться с помощью фильтров, так как утверждения могут быть отключены.
Постусловия
Следующая категория контрактов — постусловия. Как можно догадаться из названия, данный тип проверки выполняется после того, как было выполнено тело метода, но до момента возврата управления в вызывающий код. Для нашего метода deposit из примера мы можем сформировать следующее постусловие: баланс счета после вызова метода должен равняться предыдущему значению баланса плюс величина пополнения. Осталось дело за малым — описать все это в виде утверждения в коде. Но вот здесь нас поджидает первое разочарование: как же сформировать это требование в коде, ведь мы сперва изменим баланс в теле самого метода, а потом попытаемся проверить утверждение, где нужно старое значение баланса. Здесь может помочь клонирование объекта перед выполнением кода и проверка пост-условий:
Еще одно разочарование поджидает нас при описании постусловий для методов, возвращающих значение:
И это для простого метода, не говоря уже о том случае, когда метод большой и в нем несколько точек возврата. Как вы уже догадались, на этом этапе идеи об использовании контрактного программирования в проекте на PHP быстро умирают, так как язык не поддерживает необходимых управляющих конструкций. Но есть решение! И о нем будет написано ниже, наберитесь немного терпения.
Инварианты
Новые возможности
Давайте посмотрим на то, как можно использовать контракты с помощью этого фреймворка:
Как вы заметили, все контракты описываются в виде аннотаций внутри док-блоков и содержат необходимые условия внутри самой аннотации. Не нужно менять оригинальный исполняемый код класса, он остается таким же чистым, как и код без контрактов.
Предусловия задаются с помощью аннотации Verify и определяют те проверки, которые будут выполнены в момент вызова метода, но до выполнения самого тела метода. Предусловия работают в области видимости метода класса, поэтому имеют доступ ко всем свойствам, включая приватные, а также имеют доступ к параметрам метода.
Благодаря использованию АОП стало возможным реализовать даже инварианты — они элегантно описываются в виде аннотаций Invariant в док-блоке класса и ведут себя аналогично постусловиям, но для всех методов.
Во время экспериментов с кодом я обнаружил удивительное сходство контрактов с интерфейсам в PHP. Если стандартный интерфейс определят требования к стандарту взаимодействия с классом, то контракты позволяют описывать требования к состоянию инстанса класса. Применяя описание контракта в интерфейсе, удается описывать требования как к взаимодействию с объектом, так и к состоянию объекта, которое будет потом реализовано в классе:
Дальше начинается самое интересное: при создании класса и определении нужного метода любая современная IDE переносит все аннотации из описания метода в интерфейсе в сам класс. А это позволяет движку PhpDeal их находить и обеспечивать автоматическую проверку контрактов в каждом конкретном классе, реализующем данный интерфейс. Для желающих пощупать все собственными руками — можно скачать проект с гитхаба, установить все зависимости с помощью композера, настроить локальный веб-сервер на эту папку и потом просто открыть в браузере код из папки demo
Заключение
Контрактное программирование в PHP — абсолютно новая парадигма, которая может использоваться для защитного программирования, для улучшения качества кода и обеспечения читаемости контрактов, определяемых в виде требований и спецификаций. Большой плюс данной реализации в том, что код классов остается читаемым, сами аннотации читаются как документация, а также то, что в боевом режиме проверка может быть полностью отключена и не требует абсолютно никакого времени на дополнительные ненужные проверки в коде. Интересный факт: сам фреймоврк содержит лишь пару аннотаций и один класс аспекта, который связывает эти аннотации с конкретной логикой.
ООП с примерами (часть 1)
Волею судьбы мне приходится читать спецкурс по паттернам проектирования в вузе. Спецкурс обязательный, поэтому, студенты попадают ко мне самые разные. Конечно, есть среди них и практикующие программисты. Но, к сожалению, большинство испытывают затруднения даже с пониманием основных терминов ООП.
Для этого я постарался на более-менее живых примерах объяснить базовые понятия ООП (класс, объект, интерфейс, абстракция, инкапсуляция, наследование и полиморфизм).
Первая часть, представленная ниже, посвящена классам, объектам и интерфейсам.
Вторая часть иллюстрирует инкапсуляцию, полиморфизм и наследование
Основные понятия ООП
Класс
Представьте себе, что вы проектируете автомобиль. Вы знаете, что автомобиль должен содержать двигатель, подвеску, две передних фары, 4 колеса, и т.д. Ещё вы знаете, что ваш автомобиль должен иметь возможность набирать и сбавлять скорость, совершать поворот и двигаться задним ходом. И, что самое главное, вы точно знаете, как взаимодействует двигатель и колёса, согласно каким законам движется распредвал и коленвал, а также как устроены дифференциалы. Вы уверены в своих знаниях и начинаете проектирование.
Вы описываете все запчасти, из которых состоит ваш автомобиль, а также то, каким образом эти запчасти взаимодействуют между собой. Кроме того, вы описываете, что должен сделать пользователь, чтобы машина затормозила, или включился дальний свет фар. Результатом вашей работы будет некоторый эскиз. Вы только что разработали то, что в ООП называется класс.
Класс – это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (контракт).
С точки зрения программирования класс можно рассматривать как набор данных (полей, атрибутов, членов класса) и функций для работы с ними (методов).
С точки зрения структуры программы, класс является сложным типом данных.
В нашем случае, класс будет отображать сущность – автомобиль. Атрибутами класса будут являться двигатель, подвеска, кузов, четыре колеса и т.д. Методами класса будет «открыть дверь», «нажать на педаль газа», а также «закачать порцию бензина из бензобака в двигатель». Первые два метода доступны для выполнения другим классам (в частности, классу «Водитель»). Последний описывает взаимодействия внутри класса и не доступен пользователю.
В дальнейшем, несмотря на то, что слово «пользователь» ассоциируется с пасьянсом «Косынка» и «Microsoft Word», мы будем называть пользователями тех программистов, которые используют ваш класс, включая вас самих. Человека, который является автором класса, мы будем называть разработчиком.
Объект
Вы отлично потрудились и машины, разработанные по вашим чертежам, сходят с конвейера. Вот они, стоят ровными рядами на заводском дворе. Каждая из них точно повторяет ваши чертежи. Все системы взаимодействуют именно так, как вы спроектировали. Но каждая машина уникальна. Они все имеют номер кузова и двигателя, но все эти номера разные, автомобили различаются цветом, а некоторые даже имеют литьё вместо штампованных дисков. Эти автомобили, по сути, являются объектами вашего класса.
Объект (экземпляр) – это отдельный представитель класса, имеющий конкретное состояние и поведение, полностью определяемое классом.
Говоря простым языком, объект имеет конкретные значения атрибутов и методы, работающие с этими значениями на основе правил, заданных в классе. В данном примере, если класс – это некоторый абстрактный автомобиль из «мира идей», то объект – это конкретный автомобиль, стоящий у вас под окнами.
Интерфейс
Когда мы подходим к автомату с кофе или садимся за руль, мы начинаем взаимодействие с ними. Обычно, взаимодействие происходит с помощью некоторого набора элементов: щель для приёмки монеток, кнопка выбора напитка и отсек выдачи стакана в кофейном автомате; руль, педали, рычаг коробки переключения передач в автомобиле. Всегда существует некоторый ограниченный набор элементов управления, с которыми мы можем взаимодействовать.
Интерфейс – это набор методов класса, доступных для использования другими классами.
Очевидно, что интерфейсом класса будет являться набор всех его публичных методов в совокупности с набором публичных атрибутов. По сути, интерфейс специфицирует класс, чётко определяя все возможные действия над ним.
Хорошим примером интерфейса может служить приборная панель автомобиля, которая позволяет вызвать такие методы, как увеличение скорости, торможение, поворот, переключение передач, включение фар, и т.п. То есть все действия, которые может осуществить другой класс (в нашем случае – водитель) при взаимодействии с автомобилем.
При описании интерфейса класса очень важно соблюсти баланс между гибкостью и простотой. Класс с простым интерфейсом будет легко использовать, но будут существовать задачи, которые с помощью него решить будет не под силу. В то же время, если интерфейс будет гибким, то, скорее всего, он будет состоять из достаточно сложных методов с большим количеством параметров, которые будут позволять делать очень многое, но использование его будет сопряжено с большими сложностями и риском совершить ошибку, что-то перепутав.
Примером простого интерфейса может служить машина с коробкой-автоматом. Освоить её управление очень быстро сможет любая блондинка, окончившая двухнедельные курсы вождения. С другой стороны, чтобы освоить управление современным пассажирским самолётом, необходимо несколько месяцев, а то и лет упорных тренировок. Не хотел бы я находиться на борту Боинга, которым управляет человек, имеющий двухнедельный лётный стаж. С другой стороны, вы никогда не заставите автомобиль подняться в воздух и перелететь из Москвы в Вашингтон.
Контрактное программирование
Контрактное программирование (designbycontract – DbC) — это метод проектирования ПО. Он предполагает, что проектировщик должен определить формальные, точные и верифицируемые спецификации интерфейсов для компонентов системы. При этом, кроме обычного определения абстрактных типов данных, также используются предусловия, постусловия и инварианты.
Основная идея контрактного программирования — это модель взаимодействия элементов программной системы, основывающаяся на идее взаимных обязательств и преимуществ. Контракт некоторого метода или функции может включать в себя (в списке ниже поставщик – это метод):
· конкретные обязательства, которые любой клиентский модуль должен выполнить перед вызовом метода — предусловия, которые дают преимущество для поставщика — он может не проверять выполнение предусловий;
· конкретные свойства, которые должны присутствовать после выполнения метода — постусловия, которые входят в обязательства поставщика;
· обязательства по выполнению конкретных свойств — инвариантов, которые должны выполняться при получении поставщиком сообщения, а также при выходе из метода.
Контрактное программирование подразумевает эти требования критическими для корректности программ, поэтому они должны быть утверждены при проектировании. Таким образом, контрактное программирование предписывает начинать писать код с написания формальных утверждений корректности (assertions).В ООП контракт метода обычно включает следующую информацию:
· возможные типы входных данных и их значение;
· типы возвращаемых данных и их значение;
· условия возникновения исключений, их типы и значения;
· присутствие побочного эффекта метода;
· предусловия, которые могут быть ослаблены (но не усилены) в подклассах;
· постусловия, которые могут быть усилены (но не ослаблены) в подклассах;
· инварианты, которые могут быть усилены (но не ослаблены) в подклассах;
· (иногда) гарантии производительности, например, временная сложность или сложность по памяти.
Таким образом, для выражения ожидаемого поведения используются утверждения(assertions), а для того, чтобы понять, кто в программе не прав, вызывающий или вызываемый код, то добавлено несколько видов утверждений:
1. Предусловия– нарушения которых говорит о баге в вызывающем коде («клиент не прав»).
2. Постусловия– нарушения которых говорят о баге в вызываемом коде («сервис не прав»).
3. Инварианты класса – нарушения которых также говорят о баге в вызываемом коде. Это ОО-специфическая штука, которая позволяет четко сказать, чем же является валидное состояние объекта, чтобы не множить предусловия и постусловия. Тут нужно помнить, что инвариант валиден от момента создания объекта до момента его уничтожения – вызова деструктора/метода Dispose, но может быть не валидным *внутри* вызова метода. Инвариант должен быть валидным в *видимых точках*.
4. Утверждения о корректности реализации – нарушения которых также говорят о баге в вызываемом коде. Это макросы Asserts, вставленные в разные точки приложения и упрощающие отлов багов.
5. Инвариантыцикла присутствуют в языке Eiffel, их можно игнорировать.
Тот факт, что нарушение утверждений – это баг, является очень важным, поскольку из этого следует, что в программе не должно быть возможности самовосстановления (это баг, его фиксация заключается в исправлении кода, в блоке catch с ним ничего сделать не удастся).
В чем же разница между контрактами и Asserts? Разница в том, что контрактное программирование подразумевает «интеграцию» утверждений в инструменты (среду разработки). Т.е. любаяполноценная реализация механизма «контрактов» должна предоставлять следующие возможности:
1. Возможность задавать уровень утверждений:
· оставить только предусловия;
· оставить предусловия и постусловия;
· оставить все утверждения.
2. Возможность генерации документации и удобный способ «обнаружения» контрактов в среде исполнения.
3. Возможность доказать корректность программы на этапе компиляции (статический верификатор).
Замечание о понятии корректности ПО. Дан простейший пример. Что вы можете сказать о следующей функции:
int DoSomething(int y) <
Сама по себе, эта функция не является ни корректной, ни не корректной. Понятие корректности приобретает смысл только в контексте ожидаемого результата. Эта функция является корректной к утверждению “Возвращаемое значение в два раза меньше аргумента”, но является некорректным к утверждению “Возвращаемое значение должно быть положительным”, поскольку нет никаких гарантий того, что в эту функцию не будет передано отрицательное значение. Таким образом, понятие корректности ПО можно рассматривать только по отношению к некоторой спецификации
Контракты C#. Класс Contractы (Пространство имен:System.Diagnostics.Contracts)
Класс Contractсодержит статические методы для представления контрактов программы, таких как предусловие, постусловие и инвариантность объектов.
2. Постусловия.Для задания постусловий используются Ensures() и EnsuresOnThrow (). Аргументы метода Ensures() определяются аналогично предыдущему случаю. EnsuresOnThrow () гарантирует, что разделенное состояние удовлетворит условию в случае генерации указанного исключения.
Метод Result () используется для указания гарантированного возвращаемого значения.
OldValue (T)Представляет значения, какими они были в начале метода или свойства. По умолчанию тип int.
3. Все требования контрактов, которые задаются в методе, независимо от того, являются они предусловиями или постусловиями, должны помещаться в начало метода.
Предусловия позволяют проверить условия на входе метода, постусловия – на выходе из метода. Реальный код, обслуживающий контракты, генерируется утилитой ccrewrite, находит все вызовы утверждений контрактов и вставляет в их место соответствующий код. В результате методContract.Ensures, который стоит в начале метода фактически происходит при выходе из метода.
4. Можно также назначить глобальный обработчик событияContractFailed,который будет вызываться при каждом нарушении контракта во время выполнения. При нарушении контракта возбуждается исключение ContractException. Но поставить обработчик на ContractException не получится, в пользовательском коде оно доступно как экземпляр Exception. Между концепцией исключения и нарушением контракта существует разница. Исключение возбуждается в случае возникновения ошибки, которая должна быть перехвачена и обработана где-то выше. Природа ошибки может быть различной: от получения ресурсов до ошибки кодирования. Нарушение контрактов всегда связано с ошибкой кодирования, поэтомудругая обработка кроме прерывания приложения является излишней. Такой подход не приемлем для приложений, которые не должны прерываться даже в случае критической ошибки, именно для таких случаев есть возможность установить обработчик нарушения контракта
Contract.ContractFailed += (sender, args) =>
args.SetHandled();
В данном примере при нарушении контракта будет выведено диагностическое сообщение. Вызов методе args.SetHandled() означает что исключение ContractException возбуждаться не будет.
5. Инварианты.Contract.Invariant() определяет условия, которые должны соблюдаться на протяжении времени жизни метода:
[ContractInvariantMethod] //обязательныйатрибут
privatevoidИмя_метода() //методбезаргументов, private – обязательно
Contract.Invariant(условие,необязательное_сообщение);
Тело метода – последовательность вызовов методов Contract.Invariant ().
usingSystem.Diagnostics.Contracts;
public Stack() //конструктор
publicintCount // свойство только для чтения
publicvoidPush( Tt ) // занесение новогоэлемента в объект Stack
Contract.Ensures(Count == Contract.OldValue(Count) + 1, «Число элементов должно увеличиться на 1»);
list.Add(t); // занесение новогоэлемента в list
publicTPop1() // удаление элемента изобъекта Stack
publicTPop2() // удаление элемента изобъекта Stack
publicTPop3() // удаление элемента изобъекта Stack
«Число элементов списка при извлечении должно уменьшиться на 1»);