Калькулятор 2.0 Школа 21

Пробую опубликовать эту статью второй раз, потому что доверился Хабру на счет хранения статей, но в итоге оказалось, что даже опубликованные статьи Хабр сначала модерирует, а после удаляет, если они не прошли модерацию… Придётся выделить ещё пару часов на статью. Сам калькулятор 2.0 — это учебный проект Школы 21 в Казани и это один из первых проектов на C++. Приступим

UI калькулятора
UI калькулятора

Зачем?

  • Узнать что такое паттерн MVC.
  • Углубиться в ООП
  • Усовершенствовать калькулятор 1.0

Как это было?

Взглянув на первый калькулятор на Си сразу отметил дизайн. Дизайн калькулятора менять не буду, основа точно на нём. Однако сам принцип работы точно пришлось поменять, он работает с ошибками и не так как нужно. Даже не знаю как его сдал впервые)
Нашел классный калькулятор на просторах интернета desmos некоторые фитчи заберу.

Понимание MVC

блок-схема из задания
блок-схема из задания

Долго спорили с пирами о пониманиии паттерна MVC и в итоге у каждого своё понимание данного паттерна. Кто-то считает, что во View необходимо создавать экземпляр класса Controller и обращаться к этим функциям, но мое мнение, исходя из блок схемы:

  1. Пользователь общается только с UI
  2. View обращается к Controller только через сигналы и приватные слоты, то есть не создает экземпляры классов внутри
  3. Controller по сигналам (нажатиям пользователя) вызывает методы Model и модифицирует View исходя из полученных результатов
  4. Model содержит в себе всю бизнес-логику, где она расчитывается и меняет состояния, опять же в Model нет обращений к объектам класса Controller

Поближе к коду

Изначально решил полностью переписать конвертер входящего выражения из инфиксной в постфиксную нотацию, но целый месяц потратил на поиск ошибок в коде. Основная ошибка — это довериться к чату GPT) Ошибки в коде не было. Просто напросто вычисления проверял на калькуляторе desmos, который вычисляет тригонометрические функции в градусах, а функции библиотеки в радианах))) Странно что несколько пиров, которых приглашал посмотреть на код и указать ошибку так и не заметили ничего...

всего то надо было поменять DEG на RAD

Парсер+валидатор

Попытка совместить парсер с валидатором провалилась. Изначально строка которая приходила от пользователя проставляла между операторами, функциями и числами пробелы, чтобы удобно было класть в контейнер уже отделенные токены:

std::list<std::string> stringToList(const std::string& newInfix) { // С помощью этой функции отделяем каждый токен в строке std::istringstream issInfix(apendDelimeter(newInfix)); std::list<std::string> listInfix; std::string element; // Записываем в лист каждый элемент while (issInfix >> element) { listInfix.push_back(element); } listInfix.unique(); return listInfix; }

далее с помощью регулярных выражений проверялся каждый элемент, но проблема в том, что возможны повторения элементов или неправильный порядок элементов.
Эта идея хорошая, имеет свои плюсы, но на данной стадии не подходит.

Инфикс ту постфикс посимвольно

В первой версии калькулятора конвертер был реализован посимвольно (char), поэтому решил вернуться к этому рабочему варианту, но переписать заново и без встроенной валидации. Валидацию выражения оставил на "десерт".

// CONVERT TO POSTFIX QQueue<QString> Model::infixToPostfix(QString& infix) { QStack<QString> operatorStack; QQueue<QString> outputQueue; QString currentToken = ""; // Main loop for chars for (int i = 0; i < infix.length(); i++) { QChar currentChar = infix[i]; // Check is Digital or exponential if (isDigital(currentChar, infix, i)) { if (currentChar == 'e') { currentToken += currentChar; currentChar = infix[++i]; } currentToken += currentChar; // Check is Letter } else if (QChar(currentChar).isLetter()) { QString function; while (QChar(currentChar).isLetter()) { function += currentChar; currentChar = infix[++i]; } function = keyFunctions.value(function); operatorStack.push(function); --i; } else { // Check brackets isEmptyBracket(currentToken, outputQueue); isBrackets(currentChar, outputQueue, operatorStack); } } // Checks is empty isEmptyToken(currentToken, outputQueue); isEmptyStack(outputQueue, operatorStack); return outputQueue; }

Главное - работает!

Рефакторинг в MVC

Всё готово (кроме валидатора) теперь пришло время подключить модуль построения графика, депозитный и кредитный калькуляторы. Но не так всё просто)

Ниже код main.cpp который отражает понимание паттерна MVC

// main.cpp #include "calcController.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); Model m; View v; Controller c(&m, &v); // SHOW && CONNECT VIEW CalcView w; w.show(); QObject::connect(&w, &View::uiEventEqual, &c, &Controller::calcEqual); ... return 0; }

Класс View выглядит так:

#ifndef VIEW_H #define VIEW_H #include "ui_View.h" QT_BEGIN_NAMESPACE namespace Ui { class View; } QT_END_NAMESPACE class View : public QMainWindow { Q_OBJECT public: View(QWidget* parent = nullptr); ~View(); void setLineEdit(QLineEdit* ActiveLineEdit) { lastActiveLineEdit_ = ActiveLineEdit; }; signals: void uiEventEqual(QString& equalResult, QString& equalLabel); ... private slots: void equalClick(); ... private: Ui::View* ui = nullptr; ... }; #endif // VIEW_H

Класс Controller:

#ifndef CONTROLLER_H #define CONTROLLER_H #include "Model.h" #include "View.h" class Controller : public QObject { Q_OBJECT public slots: void calcEqual(QString& equalResult, QString& equalLabel); ... public: Controller(Model* m, Ui::View* v) : m_(m), v_(v){}; ... private: Model* m_ = nullptr; Ui::View* v_ = nullptr; }; #endif // CONTROLLER_H

Класс Model обычный независимый класс, который содержит все функции в теле, есть недостаток этого класса - антипаттерн класс-бог. Класс содержит в себе функции для расчета графика, депозитного и кредитного калькулятора. Хотя для масштабирования проекта необходимо разделить на несколько классов.

Проблемы

1. Большой проблемой с которой столкнулся - это передача параметров UI в класс Model для расчетов. У моих функций много аргументов и не соблюдается принцип один вход - один выход. По сути void функции меняют значения в структурах данных. Это выглядит некрасиво и пока не хватает опыта как сделать так чтобы функции либо наследовались от базового класса либо виртуально перегружались.

struct Credit { double summ; double interestRate; unsigned short caseIndex; QDate currentDate; unsigned short months; }; void Model::setCreditStructureValues(double summ, double interestRate, unsigned short caseIndex, QDate currentDate, unsigned short months) { CreditStruct.summ = summ; CreditStruct.interestRate = interestRate; CreditStruct.caseIndex = caseIndex; CreditStruct.currentDate = currentDate; CreditStruct.months = months; }

Оставим эту проблему на следующую версию калькулятора.

2. Обнаружен баг - депозитный калькулятор не считает при добавлении внесений или снятий.

void Model::reDepositWithdrawCalculate(QDate& currentDate, QDate& pasteDate, QDate& itterator, double& finalAmount, QString& anuInfo, bool flag) { QString summ = ReDepositStruct.summDep; int caseIndex = ReDepositStruct.caseIndexDep; pasteDate = ReDepositStruct.depositDate; if (flag) { summ = WithdrawStruct.summWithdraw; caseIndex = WithdrawStruct.caseIndexWithdraw; pasteDate = WithdrawStruct.withdrawDate; } auto startDay = pasteDate.day(); QString infoDepWithdraw = "Deposite for "; if (flag) infoDepWithdraw = "Withdraw for "; if (!summ.isEmpty()) { if (caseIndex == 0 || caseIndex == 1) { if (currentDate <= pasteDate && itterator >= pasteDate && DepStruct.endDate >= pasteDate) { if (flag) { finalAmount -= summ.toDouble(); } else { finalAmount += summ.toDouble(); } anuInfo += infoDepWithdraw + QString::number(pasteDate.day()) + " " + QLocale().monthName(pasteDate.month()) + " " + QString::number(pasteDate.year()) + ": " + QString::number(finalAmount, 'f', 2) + " amount: " + QString::number(summ.toDouble(), 'f', 2) + "\n"; if (caseIndex == 1) { pasteDate = pasteDate.addMonths(1); fixDate(pasteDate, startDay); } } } } }

Думаю это происходит из-за этой неуклюжей функции, которую решил совместить для снятий и внесений. Надо будет разобраться почему так происходит.

3. Калькулятор и тесты медленно собираются, потому что внутри всех файлов используется массивная библиотека QT. Причем для сборки тестов использую Cmake а для сборки самого калькулятора Qmake

о том как некрасиво собирается qmake)
о том как некрасиво собирается qmake)

4. Большая ошибка с графиком функций. Неверно расчитывается область определения! Эту ошибку исправил, но произошел сбой в компьютерах школы и пришлось откатить версию коммита, так что в конечный результат это исправление не попало... И ещё, если изменение по оси Х встроено в интерфейс, то по оси Y нет изменения, хотя на одной из проверок узнал что qcustomplot поддерживает масштабирование колесиком мышки.

виджет с графиком
виджет с графиком

5. Валидация - огромная проблема, не смогу написать правильный валидатор, поэтому peer to peer предложил использовать готовую библиотеку exprtk.hpp с встроенной валидацией, осторожно, это

6. Code Review и Google Style - ошибки стиля кода и логика кода.

деструкторы создаются в qt по умолчанию - они не нужны!
деструкторы создаются в qt по умолчанию - они не нужны!
возможен баг в валидации!
возможен баг в валидации!
// не правильно: #ifndef CALCCONTROLLER_H #define CALCCONTROLLER_H // правильно: #ifndef SMARTCALC_SRC_CALCCONTROLLER_H_ #define CALCCONTROLLER_H
Калькулятор 2.0 Школа 21
11
Начать дискуссию