Проектирование по контракту

advertisement
Язык программирования 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 – выполнение всех проверок;
Download