Наткнулся на заметку про верификацию автоматных программ. В ней рассматривается сложность верификации кода на примере "простой программы со сложным поведением", созданного для книги по автоматному программированию. В логике кода изначально была заложена ошибка, но она не была замечена ни после тестов, ни после формальной верификации ряда темпоральных свойств. И только после расширения списка проверяемых свойств ошибка была выявлена. Из этого делается вывод, что даже простая программа требует сложной проверки.
А на мой взгляд куда важней, что код из статьи является наглядным примером того, что для того, чтобы проверка работала, форма кода должна быть максимально понятна для разработчиков, и если это не так, то даже самое строгое доказательство корректности не поможет, потому что соответствие непонятной спецификации к непонятному коду только и подтвердит, что программа действительно в точности выполняет что-то непонятное. И в этом отношении автоматное программирование является классическим антипаттерном, преподносимом как паттерн.
Больше всего меня впечатлил сам код из исходной книги. Пример сложной логики для программного воплощения будильника выглядел так
class Alarm_Clock {
public:
void h_button() // Нажатие кнопки H
{
if (is_in_alarm_time_mode) {
alarm_hours = (alarm_hours + 1) % 24;
} else {
hours = (hours + 1) % 24;
}
}
void m_button() // Нажатие кнопки M
{
if (is_in_alarm_time_mode) {
alarm_minutes = (alarm_minutes + 1) % 60;
} else {
minutes = (minutes + 1) % 60;
}
}
void a_button() // Нажатие кнопки A
{
if (is_alarm_on) {
if (is_in_alarm_time_mode) {
is_in_alarm_time_mode = false;
} else {
bell_off();
is_alarm_on = false;
}
} else {
is_alarm_on = true;
is_in_alarm_time_mode = true;
}
}
void tick() // Срабатывание минутного таймера
{
if (is_alarm_on && !is_in_alarm_time_mode) {
if ((minutes == alarm_minutes - 1) && (hours == alarm_hours) ||
(alarm_minutes == 0) && (minutes == 59) && (hours == alarm_hours - 1))
bell_on();
else if ((minutes == alarm_minutes) && (hours == alarm_hours))
bell_off();
}
minutes = (minutes + 1) % 60;
if (minutes == 0) hours = (hours + 1) % 24;
}
private:
int hours; // Часы текущего времени
int minutes; // Минуты текущего времени
int alarm_hours; // Часы срабатывания будильника
int alarm_minutes; // Минуты срабатывания будильника
bool is_alarm_on; // Включен ли будильник?
bool is_in_alarm_time_mode;
// Активен ли режим установки времени будильника?
void bell_on() {...} // Включить звонок
void bell_off() {...} // Выключить звонок
};
Приведение к автоматному коду для улучшения понятности выглядел даже более впечатляюще.
bool x1() {
if ((minutes == alarm_minutes - 1) && (hours == alarm_hours) ||
(alarm_minutes == 0) && (minutes == 59) && (hours == alarm_hours - 1))
return true;
else
return false;
}
bool x2() {
if ((minutes == alarm_minutes) && (hours == alarm_hours))
return true;
else
return false;
}
void z1() {
hours = (hours + 1) % 24;
}
void z2() {
minutes = (minutes + 1) % 60;
}
void z3() {
alarm_hours = (alarm_hours + 1) % 24;
}
void z4() {
alarm_minutes = (alarm_minutes + 1) % 60;
}
void z5() {
minutes = (minutes + 1) % 60;
if (minutes == 0) hours = (hours + 1) % 24;
}
void z6() {
... // Включить звонок
}
void z7() {
... // Выключить звонок
}
// Реализация управляющего автомата
void A1() {
switch (y) {
case 1: // Будильник выключен
if (e == h) { z1(); }
else if (e == m) { z2(); }
else if (e == a) { y = 2; }
else if (e == t) { z5(); }
break;
case 2: // Установка времени будильника
if (e == h) { z3(); }
else if (e == m) { z4(); }
else if (e == a) { y = 3; }
else if (e == t) { z5(); }
break;
case 3: // Будильник включен
if (e == h) { z1(); }
else if (e == m) { z2(); }
else if (e == a) { z7(); y = 1; }
else if ((e == t) && x1()) { z5(); z6(); }
else if ((e == t) && x2()) { z5(); z7(); }
else if (e == t) { z5(); }
break;
}
}
Он сопровождался картинкой с очевидными связями между состояниями, но для меня и она была не особо полезна. Мне ещё первый код показался подозрительным и переусложнённым, поэтому захотелось сначала воспроизвести его суть, но без лишних ветвлений. Получилось так:
MODULE AlarmClock;
CONST
AlarmOff = 0; AlarmSet = 1; AlarmWait = 2;
VAR
time: ARRAY 2 OF RECORD hours, minutes: INTEGER END;
state: INTEGER;
PROCEDURE Bell(on: BOOLEAN); END Bell;
PROCEDURE Next(VAR var: INTEGER; mod: INTEGER); BEGIN
var := (var + 1) MOD mod
END Next;
PROCEDURE PushHour*; BEGIN
Next(time[ORD(state = AlarmSet)].hours, 24)
END PushHour;
PROCEDURE PushMinute*; BEGIN
Next(time[ORD(state = AlarmSet)].minutes, 60)
END PushMinute;
PROCEDURE PushAlarm*; BEGIN
Bell(FALSE);
Next(state, 3) (* Off, Set, Wait *)
END PushAlarm;
PROCEDURE MinuteTick*; BEGIN
Next(time[0].minutes, 60);
IF time[0].minutes = 0 THEN Next(time[0].hours, 24) END;
Bell((state # AlarmOff)
& (time[1].hours = time[0].hours)
& (time[1].minutes = time[0].minutes))
END MinuteTick;
END AlarmClock.
Вот и всё — этот код оказался слишком простым, чтобы как-то дополнительно его разбивать по режимам работы будильника. Похоже, авторы неосознанно подогнали начальный пример, названный ими громоздким и непонятным, под решение с автоматом. Хотя даже в таком виде выгода предложенного взгляда на разработку не выглядит очевидной.
Понятно также, что никто не будет специально лепить в один набор обработчиков ветвления по чётко отделяемым режимам работы, а, скорее, сделает разделённые по режимам разные наборы обработчиков. В общем же случае выбор между параметризацией поведения через ветвление и декомпозицией по разным подпрограммам определяется историей кода и близостью к решаемой задаче, которая влияет на ясность результирующего кода. В ещё более общем случае проблема разбиения задачи на подзадачи, которое наилучшим образом помогает решению, требует творческого подхода и тем более не может сводиться к приведению всего к автоматному подходу.
Комментариев нет:
Отправить комментарий