16. ВОТ И УКРОЩАЕМ
Очевидно, мы имеем Противоречие 6. И это противоречие типовое ("Много - мало"). Например, такое же точно, как Противоречие 3.
В предыдущей главке была фраза: "Когда внутри алгоритма интерпретации была лишь сущность "операнд" и "четыре действия арифметики", он был невелик". Между прочим, в этой фразе и кроется причина "пухлости". Следует вынести из алгоритма во внешние справочные таблицы "все разные токены" и методы их обработки. Сам алгоритм должен быть чист.
В этом и заключается типовой прием устранения Противоречия "Много - мало", который мы уже не раз применили.
Иными словами, поступим так же, как мы поступили с алгоритмом преобразования выражения в инфиксной нотации в выражение в нотации ПОЛИЗ.
1. ОБЪЯВИМ ТИПЫ ТОКЕНОВ
У нас уже есть справочник типов токенов - им и воспользуемся:
enum TTokenType //Это наш справочник типов токенов
{
eTokenOperand = 0, // операнд
eTokenPlus, // операция сложение
eTokenMinus, // операция вычитание
eTokenMultiply, // операция умножение
eTokenDivide, // операция деление
eTokenOpeningBracket, // открывающая скобка
eTokenClosingBracket, // закрывающая скобка
eTokenEndOfSequence, // конец последовательности
eTokenFunction, // функция
eTokenDelimiter, // разделитель
eTokenTypeCount // количество значений токенов
};
Все или почти все типы токенов предположительно исчерпываются данным списком. Два типа "операнд" и "функция" - универсальные токены, покрывающие любые операнды и функции, 4 действия арифметики вряд ли расширятся; то же касается скобок, разделителя и числа значений. В любом, случае, если это предположение неверно, несложно будет вставить строчку.
Поэтому токен "Функция" и методы его обработки мы опишем отдельно. Это нам позволит для удобства создать 1 Пользовательский файл, где Пользователь будет регистрировать новые конкретные функции, которых нет в общем списке. Мы хотим добиться того, чтобы он их просто туда вписывал, и калькулятор после перекомпиляции программы сразу начинал их обрабатывать - т.е., алгоритму было бы "все равно", а точность его не менялась.
2. ТОГДА ОПИШЕМ МЕТОДЫ ОБРАБОТКИ ТОКЕНОВ, КРОМЕ "ФУНКЦИИ"
Вот так обработаем операнд:
TErrorType my_operand (const TToken & rToken, std::vector<double> & rStack)
{
rStack.push_back(rToken.GetValue());
return eNoError;
}
Вот так обработаем операцию "плюс":
TErrorType my_plus (const TToken & rToken, std::vector<double> & rStack);
{
if (rStack.size() < 2)
return eStackUnderflow;
double fltArg2 = rStack.back();
rStack.pop_back();
double fltArg1 = rStack.back();
rStack.pop_back();
rStack.push_back(fltArg1 + fltArg2);
return eNoError;
}
Вот так обработаем операцию "минус":
TErrorType my_minus (const TToken & rToken, std::vector<double> & rStack)
{
if (rStack.size() < 2)
return eStackUnderflow;
double fltArg2 = rStack.back();
rStack.pop_back();
double fltArg1 = rStack.back();
rStack.pop_back();
rStack.push_back(fltArg1 - fltArg2);
return eNoError;
}
Вот так обработаем операцию "умножить":
TErrorType my_multiply (const TToken & rToken, std::vector<double> & rStack)
{
if (rStack.size() < 2)
return eStackUnderflow;
double fltArg2 = rStack.back();
rStack.pop_back();
double fltArg1 = rStack.back();
rStack.pop_back();
rStack.push_back(fltArg1 * fltArg2);
return eNoError;
}
Вот так обработаем операцию "разделить":
TErrorType my_divide (const TToken & rToken, std::vector<double> & rStack)
{
if (rStack.size() < 2)
return eStackUnderflow;
double fltArg2 = rStack.back();
rStack.pop_back();
double fltArg1 = rStack.back();
rStack.pop_back();
rStack.push_back(fltArg1 / fltArg2);
return eNoError;
}
Вот так обработаем скобку:
TErrorType my_bracket (const TToken & rToken, std::vector<double> & rStack)
{
return eBracketMismatch;
}
Вот так обработаем разделитель:
TErrorType my_delimiter (const TToken & rToken, std::vector<double> & rStack)
{
return eWrongSymbol;
}
И этот перечень, предположительно, уже не расширится.
3. УКАЖЕМ КАКОЙ МЕТОД КАКОЙ ТОКЕН ОБРАБАТЫВАЕТ
std::map<TTokenType, TFunction> handlers; |
handlers[eTokenOperand | ] = my_operand; |
handlers[eTokenPlus | ] = my_plus; |
handlers[eTokenMinus | ] = my_minus; |
handlers[eTokenMultiply | ] = my_multiply; |
handlers[eTokenDivide | ] = my_divide; |
handlers[eTokenOpeningBracket | ] = my_bracket; |
handlers[eTokenClosingBracket | ] = my_bracket; |
handlers[eTokenEndOfSequence | ] = my_delimiter; |
handlers[eTokenFunction | ] = my_function; |
handlers[eTokenDelimiter | ] = my_delimiter; |
И этот перечень уже не расширится.
4. И, НАКОНЕЦ, СДЕЛАЕМ РЕАЛИЗАЦИЮ МЕТОДА ВЫЗОВА ТОКЕНОВ ПРИ ПОМОЩИ ОБРАЩЕНИЯ К СПРАВОЧНОЙ ТАБЛИЦЕ
Т.о., основной алгоритм во время вычисления не использует методы обработки конкретных токенов напрямую - так что мы нигде в алгоритме не увидим вызовов, например, my_plus и т.д. А пользуется только абстрактным вызовом my_token_handler
TErrorType my_token_handler (const TToken & rToken, std::vector<double> & rStack)
{
//Ищем ссылку на метод для обработки конкретного токена
if (handlers.find(rToken.GetType()) == handlers.end())
return eWrongSymbol;
//Получаем ссылку на метод для обработки конкретного токена
TFunction pfn = handlers[rToken.GetType()];
//Выполняем полученный метод и возвращаем результат вычисления
return (*pfn)(rToken, rStack);
}
И эти строки уже никогда не поменяются, как бы ни расширялись возможности программы.
5. ТЕПЕРЬ АНАЛОГИЧНЫМ ОБРАЗОМ ПОСТУПИМ И С "ФУНКЦИЯМИ"
Создадим "место", где будем регистрировать новые Пользовательские функции:
// Новая функция должна быть зарегистрирована здесь
std::map<std::string, TFunction> g_functions;
g_functions["sin"] = my_sin;
g_functions["cos"] = my_cos;
g_functions["sqrt"] = my_sqrt;
Опишем методы обработки конкретных функций:
// Алгоритм вычисления синуса
TErrorType my_sin (const TToken & rToken, std::vector<double> & rStack)
{
if (rStack.size() < 1)
return eStackUnderflow;
double fltValue = rStack.back();
rStack.pop_back();
rStack.push_back(sin(DegreeToRadian(fltValue)));
return eNoError;
}
// Алгоритм вычисления косинуса
TErrorType my_cos (const TToken & rToken, std::vector<double> & rStack)
{
if (rStack.size() < 1)
return eStackUnderflow;
double fltValue = rStack.back();
rStack.pop_back();
rStack.push_back(cos(DegreeToRadian(fltValue)));
return eNoError;
}
// Алгоритм вычисления квадратного корня
TErrorType my_sqrt (const TToken & rToken, std::vector<double> & rStack)
{
if (rStack.size() < 1)
return eStackUnderflow;
double fltValue = rStack.back();
rStack.pop_back();
rStack.push_back(sqrt(fltValue));
return eNoError;
}
//Напишите алгоритм вычисления Вашей функции
......................................................
......................................................
Для удобства сохраним обработки функций в отдельном файле, куда Пользователь будет вносить новые функции и/или вписывать их алгоритмы.
6. И, НАКОНЕЦ, ДЕЛАЕМ РЕАЛИЗАЦИЮ МЕТОДА ВЫЗОВА ПОЛЬЗОВАТЕЛЬСКОЙ ФУНКЦИИ ПРИ ПОМОЩИ ОБРАЩЕНИЯ К СПРАВОЧНОЙ ТАБЛИЦЕ
Т.о. основной алгоритм во время вычисления не использует напрямую методы обработки вызовов конкретных функций - так что мы нигде в алгоритме не увидим вызовов, например, my_cos и т.д. А пользуется только абстрактным my_function
TErrorType my_function (const TToken & rToken, std::vector<double> & rStack)
{
// Ищем зарегистрированную функцию
if (g_functions.find(rToken.GetText()) == g_functions.end())
return eUndefinedFunction;
// Получаем зарегистрированную функцию
TFunction pfn = g_functions[rToken.GetText()];
// Выполняем найденную функцию и возвращаем результат.
return (*pfn)(rToken, rStack);
}
Итак, мы видим:
1. Алгоритм интерпретации "со свитчами" теперь не нужен (и как это поначалу он показался нам простым?). Разные сущности вновь ушли во внешние таблицы.
Вот весь новый алгоритм интерпретации:
while (GetToken(&token))//Получаем очередной токен из ПОЛИЗа
{
//Выполняем вычисление "чего бы то ни было" (если оно зарегистрировано)
my_token_handler(token.GetType(), stack);
}
Как мы видим вновь получилась функция, приближенная к идеальной: "Получи, что велено, откуда прикажут, и отправь куда следует" (в данном случае, получаем токен из ПОЛИЗа, который является внешним по отношению к алгоритму интерпретации).
Весь калькулятор состоит из трех таких функций.
2. "Распухания" кода не происходит, т.к.:
a. при добавлении нового типа токена нам всего лишь нужно добавить новый обработчик токена и зарегистрировать его в справочной таблице обработчиков токенов;
b. при добавлении новой функции нам всего лишь нужно добавить новый обработчик функции и зарегистрировать его в справочной таблице функций.
Далее...