Язык программирования Eiffel. Обзор, основные идеи и примеры программ. Проектирование по контракту Проектирование по контракту – это установление отношений между классом и его клиентами в виде формального соглашения, недвусмысленно устанавливающие права и обязанности сторон. Только через точное определение требований и ответственности для каждого модуля можно надеяться на достижение существенной степени доверия к большим программным системам. Корректность ПО Рассмотрим Код: int DoSomething(int y) { int x = y/2; return x; } Корректен ли он? Ответ: и Да и Нет. Эта функция является корректной для утверждения «Возвращаемое значение в два раза меньше аргумента», но является некорректной для утверждения «Возвращаемое значение должно быть положительным» . Ни Сигнатуры, ни имени метода, ни комментариев недостаточно, чтобы говорить о корректности. Корректность – это согласованность программных элементов с заданной спецификацией. Термин «корректность» не применим к программному элементу, он имеет смысл лишь для пары – «программный элемент и его спецификация» Утверждения Спецификация говорит о том, что должна делать система, а реализация определяет, как это будет достигнуто. В проектировании по контракту выражение спецификации осуществляется с помощью механизма утверждений (assertions). Утверждение – это логическое выражение, которое должно быть истинным на некотором участке программы и может включать сущности нашего ПО. Утверждениями могут служить обычные булевы выражения с некоторыми расширениями. Так, в утверждениях помимо булевых выражений могут применяться функции (но с некоторыми ограничениями: это должны быть функции без «побочных эффектов», т.е. не изменяющие состояние объекта). А для постусловий необходима возможность обратиться к результату выполнения функции, а также к значению, которое было до начала выполнения этой функции (определяется термином “old”). Формула корректности. Сильные и слабые условия Пусть A – это некоторая операция, тогда формула корректности – это выражение вида: {P} A {Q} Формула корректности, называемая также триадой Хоара, говорит следующее: любое выполнение А, начинающееся в состоянии, где Р истинно, обязательно завершится и в заключительном состоянии будет истинно Q, где P и Q – это утверждения, при этом P является предусловием, а Q – постусловием. Рассмотрим пример: {x = 5} x = x ^ 2 {x > 0} Говорят, что условие P1 сильнее, чем P2, а P2 слабее, чем P1, если выполнение условия P1 влечет за собой выполнение условия P2, но они не эквивалентны. Предусловия и постусловия Для четкого выражения задачи, выполняемой конкретной функцией, с ней связывают два утверждения – предусловие и постусловие. Предусловие определяет условия, которые должны выполняться всякий раз перед вызовом функции, а постусловие определяет свойства, гарантируемые после завершения ее выполнения. Предусловие и постусловие определяют контракт функции со всеми ее клиентами. Пусть предусловие определяется ключевым словом require, а постусловие – ключевым словом ensure. Тогда, если с некоторой функцией r связаны утверждения require A и ensure B, то класс говорит своим клиентам следующее: «Если вы обещаете вызвать r в состоянии, удовлетворяющем A, то я обещаю в заключительном состонии выполнить B» {A} R {B} Cуществует правило, согласно которому «каждый компонент, появляющийся в предусловии программы, должен быть доступен каждому клиенту, которому доступна сама программа». Пример Классическим примером предусловий и постусловий могут служить функция извлечения квадратного корня Sqrt или методы добавления и удаления элемента из стека: public class Math { public static double Sqrt(double x) { Contract.Requires(x >= 0, “Positive x"); // Реализация метода } } public class Stack<T> { public void Push(T t) { Contract.Requires(t != null, "t not null"); Contract.Ensures(Count == Contract.OldValue(Count) + 1, "One more item"); // Реализация метода } public T Pop() { Contract.Requires(Count != 0, "Not empty"); Contract.Ensures(Contract.Result<T>() != null, "Result not null"); Contract.Ensures(Count == Contract.OldValue(Count) - 1, "One fewer item"); // Реализация метода } public int Count {get;} } Предусловия и постусловия в Eiffel Предусловия записываются в специальной секции require реализации метода, постусловия – в секции ensure. Общий формат таков: [tag:] condition где tag – это необязательный идентификатор, а condition – это условие. Подобных утверждений в пред-, постусловиях и инвариантах может быть сколько угодно. Вот пример пред- и постусловий: string_to_binary (what: STRING): ARRAY [NATURAL_8] is -- Возвращает вектор ASCII кодов символов исходной строки. require valid_argument: what /= Void local i:INTEGER do … ensure valid_result: Result /= Void result_has_same_size: what.count = Result.count end Инварианты класса Предусловия и постусловия характеризуют конкретные функции, но не класс в целом. Экземпляры класса часто обладают некоторыми глобальными свойствами, которые должны выполняться после создания объекта и не нарушаться в течение всего времени жизни. Такие свойства носят название инвариантов класса. Они определяют более глубокие семантические свойства и ограничения, присущие объектам этого класса. Утверждение A является корректным инвариантом класса, если и только если оно удовлетворяет следующим двум условиям: 1) Каждый конструктор, применимый к аргументам, удовлетворяющим его предусловию в состоянии, в котором атрибуты имеют значения, установленные по умолчанию, вырабатывает заключительное состояние, гарантирующее выполнение A. 2) Каждый открытый метод класса, вызываемый с аргументами в состоянии, удовлетворяющем A и предусловию, вырабатывает заключительное состояние, гарантирующее выполнение A. Инварианты класса Первое условие определяет роль конструктора в жизненном цикле объекта класса и может быть выражено формально следующим образом: {Default and pre} body {post and Inv}, где Default – состояние объекта некоторого класса со значениями полей по умолчанию, pre – предусловие, body – тело конструктора, post – постусловие, Inv – инвариант класса. Второе условие позволяет неявно рассматривать инварианты как добавления к предусловиям и постусловиям, которые действуют на все контракты между классом и его клиентами. {Inv and pre} body {Inv and post}, где body – тело экспортируемого метода, pre – предусловие, post – постусловие, а Inv – инвариант класса. Любое выполнение метода (body), которое начинается в состоянии, удовлетворяющем инварианту (Inv) и предусловию (pre), завершится в состоянии, в котором выполняются инвариант (Inv) и постусловие (post). Инварианты класса в Eiffel Инварианты записываются в специальной секции invariant в описании класса: class OPTIONS_PARSING_RESULT create make_ok, make_failed feature -- Initialization make_ok (parsed_options: OPTIONS) is -- Creation when parsing successful. do is_ok := true options := parsed_options end make_failed( failure_reason: STRING ) is -- Creation when parsing failed. require failure_reason /= Void do Is_ok := false reason := failure_reason.twin end feature -- Access Is_ok: BOOLEAN options: OPTIONS reason: STRING invariant options_iff_ok: is_ok implies (options /=Void) reason_iff_failure: (not is_ok) implies ((reason /= Void) and (options = Void)) end Утверждения и проверка входных данных Главный принцип защищенного кода «все входные данные зловредны, пока не доказано противное» остается в силе, и о нем не следует забывать при разработке любых приложений, независимо от того, построены они по принципам проектирования по контракту или без него. Все данные, которые пересекают границу ненадежной и доверенной среды, требуют явной и тщательной проверки. Принципы проектирования по контракту вступают в силу только внутри доверенной среды в виде постусловий модулей ввода. Hello, world! class APPLICATION Create make feature -- Initialization make is -- Run application. do io.put_string ("Hello, world") io.put_new_line end end -- class APPLICATION Основные черты дизайна Eiffel программ При программировании на Eiffel необходимо придерживаться ряда принципов (т.н. design principles) методологии Eiffel. Поскольку Eiffel не мультипарадигменный язык, он не позволяет сочетать в одной программе несколько стилей. Поэтому важно с самого начала придерживаться стиля Eiffel (который приверженцы Eiffel явно считают единственным правильным), в противном случае есть риск вообще не получить хорошего результата Command/Query Separation Principle Из-за этого принципа программы на Eiffel выглядят как последовательность операций "сделал" и "спросил результат". Т.е. если в C++/D можно открыть файл и сразу получить результат операции: File file; If( -1 != file.open( file_name, file_mode ) ) ... то в Eiffel эта операция будет выглядеть несколько иначе: create file.make (file_name) file.open_read if file.is_open_read then ... end Information Hiding Principle Здесь ничего нового: разработчик класса должен определить, какая часть класса будет свободно доступной (публичной), а какая будет скрытой. Отличие состоит в том, что в Eiffel можно гибко управлять видимостью компонентов класса. Например: class A feature a is ... end b is ... end feature {B} c is ... end feature {C,D} d is ... end feature {NONE} e is ... end end Uniform Access Principle Компоненты (features в терминологии Eiffel) в классе могут быть двух видов: атрибуты (поля) и методы (подпрограммы). Принцип унифицированного доступа говорит о том, что по записи обращения к компоненту (например, такой: x.f) не должно быть понятно, является ли компонент атрибутом или же методом. Данный принцип начинает работать в полной мере, когда производный компонент переопределяет атрибут методом: class A feature name: STRING -- name является атрибутом. end class B inherit A redefine name end feature name: STRING is ... end -- name уже стало методом. end Мониторинг утверждений в период выполнения Как уже говорилось ранее, поведение системы при нарушении утверждений во время выполнения полностью зависит от разработчика. Механизмы проектирования по контракту предоставляют возможности управлять этим поведением путем задания соответствующих параметров компиляции, тем самым включая и отключая проверки времени выполнения по своему усмотрению. Некоторые уровни мониторинга: no – во время выполнения нет проверок никаких утверждений. В этом случае утверждения играют роль комментариев; require – проверка только предусловий на входе методов; ensure – проверка постусловий на выходе методов; invariant – проверка выполнимости инвариантов на входе и выходе всех экспортируемых методов; loop – проверка выполнимости инвариантов циклов; check – проверка инструкций утверждений; all – выполнение всех проверок;