2. ПЕРЕБИРАЯ ВАРИАНТЫ
Предположим, необходимо написать несложный интерпретатор арифметических выражений. Выражения могут содержать 4 арифметические операции: +, -, *, /
Например:
3 + 5
12 - 4
3 * 2
6 / 2
Кроме того, допустимы выражения, которые содержат несколько операций, и выражения со скобками.
Например:
5 * 2 - 3
(3 + 2) * 3
Казалось бы, "тоже мне задача". Но предлагая ее даже "продвинутым студентам" (сказать по правде, даже и еще более продвинутым Коллегам), мы столкнулись со следующим: редко кто пытается представить даже маленький проект сразу в целом. Наоборот, большинство ответов представляют собой попытки сходу написать "работающий частный случай", с тем чтобы уже затем "по мере поступления" поддерживать сложные выражения.
Хорошо ли это? Может быть, так и надо? Давайте посмотрим.
Вот пример очень характерного ответа (далее цитирование):
"Если выражение состоит из одной операции и двух операндов, то алгоритм интерпретации такого выражения выглядит достаточно просто:
- Считать первый операнд и поместить его в массив операндов.
- Считать знак операции и занести его во внутреннюю переменную.
- Считать второй операнд и поместить его в массив операндов.
- Посчитать результат выражения.
Если бы передо мной стояла задача сделать систему побыстрее, я бы для начала ограничился поддержкой простых арифметических выражений, т.е. выражений с двумя операндами и единственным знаком операции между ними. Для многих задач такое решение может оказаться достаточным, а более сложные выражения можно будет поддержать после того, как будут загружаться и интерпретироваться простые.
На языке программирования C++ алгоритм интерпретатора арифметических выражений выглядел бы таким образом (без кода обработки ошибок):
double fltResult;
double Operands[2];
int iOperand = 0;
char chOp = '0';
TToken token;
while (GetToken(&token))
{
switch (token.GetType())
{
case eTokenOperand: //Операнд
Operands[iOperand++] = token.GetOperand();
break;
case eTokenOperation: //Операция
chOp = token.GetOperation();
break;
case eTokenCalculation:
{
switch (chOp)
{
case '+': //Плюс
fltResult = Operands[0] + Operands[1];
break;
case '-': //Минус
fltResult = Operands[0] - Operands[1];
break;
case '*': //Умножить
fltResult = Operands[0] * Operands[1];
break;
case '/': //Разделить
fltResult = Operands[0] / Operands[1];
break;
}
}
break;
}
}
После сдачи первой версии я бы переписал алгоритм - использовал бы деревья или какие-нибудь другие хитроумные структуры данных". Конец цитаты.
Сравним приведённое решение с первоначальными требованиями. Можно ли считать, что решение им удовлетворяет? На первый взгляд, "и нет, и да".
- Нет. Т.к. в требованиях говорится о поддержке выражений с несколькими операциями, а предложенное решение поддерживает выражения только с одной операцией.
- Может быть, и да. Т.к. видно из решения, что иные операции будут поддерживаться по аналогии. Кроме того, программист, предложивший решение, верит в то, что найдет позже приемлемые варианты для конструирования более сложных комбинаций.
- Нет. Т.к. в требованиях подчёркивается необходимость поддержки выражений со скобками, а предложенное решение поддерживает выражения без скобок.
- Может быть, и да. Т.к. программист, похоже, не видит противоречий, которые возникнут при развитии проекта предложенным способом.
Фраза "может быть" здесь ключевая. Т.к., волей-неволей, предложенным решением заложена определенная (назовем ее "портяночная") идеология развития проекта. Между тем, для целей качественного проектирования было бы здорово сразу попытаться увидеть "идеальный образ" - т.е. полное и компактное решение задачи (не требующее дальнейших дописок, комбинаций, поправок, деревьев и хитроумных структур).
Однако, возможно ли это?
Далее...