Ayao "Alqualos" Kuroyuki (ayao) wrote,
Ayao "Alqualos" Kuroyuki
ayao

Задача о чайнике

Благодаря упоминанию процедуры "забить сваю" у Аднаки, наконец вспомнил, про что же я хотел написать последние несколько дней.

Итак, широко известная в определённых кругах задача о чайнике. Есть чайник, пустой. Есть дрова. Есть спички. Есть источник воды. Задача - вскипятить чайник. Решаем задачу: вода наливается в чайник, огонь разжигается, чайник ставится на огонь. Задача решена. Теперь рассмотрим другую задачу, отличающуюся от первой тем, что огонь уже горит, а вода уже в чайнике. Самый простой способ решить эту задачу: выливаем воду на огонь, после чего оказываемся в условиях предыдущей задачи, которую мы решать уже научились. Мокрыми дровами пренебречь ^_^

Анекдот, по-моему, математический. Но ничто не мешает его рассмотреть с точки зрения программиста.

В программировании приходится исходить из практических требований. Соображения здравого смысла говорят нам, что выливать воду на огонь не стоит, значит этого делать не надо. Но на здравом смысле тут полёт мысли не заканчивается. Во всяком случае, у хорошего программиста не должен заканчиваться. Итак, сформулируем задачу более точно. Итак, у нас есть набор данных нам свыше примитивов:
kettle_fill() - налить воду в чайник
kettle_empty() - вылить воду из чайника
fire_light() - зажечь огонь
fire_douse() - потушить огонь
kettle_put_on_fire() - поставить чайник на огонь
kettle_put_off_fire() - снять чайник с огня # грамотность ещё та, наверное...
Очевидно, что если мы хотим вскипятить чайник, то вылить/потушить/снять нам вряд ли понадобятся. Но они у нас есть ^_^

Как же решать наши две задачи? "Тупой" способ:

task1() {
kettle_fill();
fire_light();
kettle_put_on_fire();
}

task2() {
kettle_put_on_fire();
}

Логично? Вроде да. Какой-нибудь умник может решить, что на самом деле первая задача сводится ко второй (а не наоборот, как в анекдоте) и решить вот так:

task1() {
kettle_fill();
fire_light();
task2();
}

Мотивировать он может это так: дескать, а вдруг решение второй задачи усложнится (например, надо будет чайник ещё крышкой накрывать), так нам надо будет только в одном месте подправить, а иначе - в двух. Типа, избегаем дублирования кода. А кто-то может решить, что вообще один чёрт.

Так вот, не один чёрт. В первом случае мы имеем два независимых решения, во втором - два зависимых. Это принципиально разные вещи!!! В первом случае имеет место быть дублирование кода. Одна строчка, да, но правы те, кто скажут "сегодня одна строчка - завтра целая процедура". Неправы, они, однако, в том, что из этого пока ещё не следует, что второе решение лучше. У него тоже есть свой минус и заключается он в самой зависимости: если мы что-то изменим в решении второй задачи, нам надо не забыть, что это может повлиять на решение первой задачи! Увеличивается вероятность что-то напортить.

Что же делать? Какое решение правильнее? Как быть в аналогичных ситуациях? На уровне "так или сяк - как лучше?" эта проблема не имеет решения вовсе. Чтобы её решить, нужно подняться на уровень выше и перейти от реализации к проектированию.

Что у нас есть? Набор из шести примитивов. Две типовых задачи. Требуется спроектировать высокоуровневый интерфейс для решения этих задач. Теперь возвращаемся к реализации. Одна задача сводится к другой: вопрос, сводить или нет? Вопрос закономерный: а нельзя ли спроектировать интерфейс так, чтобы этот вопрос вообще не возникал? В каком случае он возникать не будет? Ну, например, если мы сведём две задачи к одной. То есть будем решать более общую задачу. Только чтобы не делать лишней работы, нам надо сделать это решение максимально простым. Возможно ли это? Да. Наше входное условие состоит из трёх величин: налита ли вода в чайник? Зажжён ли огонь? Стоит ли чайник на огне? Значит, последовательность действий для нашей задачи будет определяться трёхмерной матрицей 2x2x2. Каждым элементом будет являться последовательность действий, которую можно задать, например, строкой, в которой нашим примитивам будут соответствовать буквы w/W/f/F/k/K. Матрица будет иметь вид (псевдокод):
actions[2][2][2] = {
{// огонь не зажжён} {//огонь зажжён}
{ {"wfk", NULL}, {"wk", NULL} }, // воды нет
{ {"fk", "f"}, {"k", ""} }, // вода есть
};
Далее мы находим в этом массиве нужный нам элемент и, идя посимвольно по строке, выполняем нужные действия. В C++ вместо буковок wWfFkK я бы использовал бы байты \x01, \x02, \x03, \x04, \x05, \x06 - тогда бы можно было завести ещё массив указателей на функции и тупо вызывать нужную. В питоне - организовал бы хэш функций. В перле - тоже. Но это уже детали реализации. NULL в матрице - непредусмотренные ситуации: воды нет, чайник на огне. Можно ли налить воды, не снимая его с огня? Можно ли зажечь огонь, не снимая чайник? Это нам не дано, поэтому эти случаи нереализованы. Затем, если кому-нибудь понадобятся функции task1() и task2(), то выглядеть они будут вот так:

task1() {
task(false, false, false); // нет воды, нет огня, не стоит на огне: actions[0][0][0]
}

task2() {
task(true, true, false); // есть вода, есть огонь, не стоит на огне: actions[1][1][0]
}

Таким образом, мы свели две задачи не одну к другой, а обе к третьей. Казалось бы, какая разница? А разница в том, что по проектной задумке мы теперь имеем вообще одно-единственное место, где сосредоточена вся логика задачи. Нам больше не надо думать, "а что если мы поменяем тут?", "а не испортит ли это нам что-нибудь там?", потому что больше нет "тут" и "там". Кажущаяся сложность на самом деле является высшей простотой: логика в одной матрице, остальное - её оформление.

Последняя мысль, которая должна возникнуть у опытного человека: а нужны ли нам вообще функции task1() и task2()? Конечно, если задача изначально стояла написать эти функции, то от неё никуда не денешься. Но если стояла более общая задача - разработать интерфейс, пригодный для решения этих задач, то встаёт выбор: будет ли наш интерфейс состоять из функций task, task1 и task2, или из функций task1 и task2 (task - внутренняя) или же из функции task только? Разумный довод номер один: не стоит прятать функцию task, так как она обеспечивает решение более широкого круга задач, что есть хорошо. Может пригодиться. А вот нужны ли нам функции task1 и task2 - вопрос более сложный. Казалось бы - а чем они плохи? А тем, что они усложняют интерфейс. Вместо двух слоёв "примитивы - реализация" мы получаем три: "примитивы - реализация - интерфейс". Функции, подобные task1 и task2 называются convenience functions, функции для удобства. Их существование оправдано лишь в том случае, когда это действительно даёт выигрыш в удобстве. Например, функции используются по несколько раз в строчке, а обобщённый интерфейс слишком громоздок для этого (например, там штук 10 параметров, а не 3, как тут). Но вообще-то начинать следует с максимально простого интерфейса и функции удобства добавлять по необходимости, а не от всей души. Иначе интерфейс усложняется, становится менее гибок, тяжелее поддерживать его неизменным при меняющейся логике.

Вот сколько полезного можно выжать из анекдота о чайнике ^_^
Tags: devel, fun
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 0 comments