Conversation
Общий вывод по проектуПроект представляет собой классическую реализацию веб-квеста на базе Java Servlets. Автор успешно применил Mockito для тестирования контроллеров и продемонстрировал понимание жизненного цикла HTTP-сессий. Структура проекта логична, а именование классов и переменных в целом соответствует стандартам Java. Однако, в архитектуре наблюдается сильная связанность (tight coupling) из-за использования статических методов и прямой работы с состоянием в сервлете. Внедрение паттерна "Состояние" и выделение сервисного слоя значительно повысят гибкость и расширяемость приложения. Также стоит уделить внимание безопасности (валидация ввода) и современным практикам логирования. Итоговая оценка: A (online review) |
|
|
||
| HttpSession session = req.getSession(); | ||
| GameState state = (GameState) session.getAttribute("state"); | ||
| if (state == null) state = GameState.START; |
There was a problem hiding this comment.
Использование жестко закодированного состояния по умолчанию напрямую в контроллере нарушает инкапсуляцию. Рекомендуется вынести логику инициализации в GameLogic.
| if (state == null) state = GameState.START; | ||
|
|
||
| String answer = req.getParameter("answer"); | ||
| GameResult result = GameLogic.next(state, answer); |
There was a problem hiding this comment.
Контроллер напрямую вызывает статический метод GameLogic.next. Это затрудняет тестирование и нарушает принцип инверсии зависимостей. Стоит использовать внедрение зависимостей.
|
|
||
| if (result.state() == GameState.WIN || result.state() == GameState.LOSE) { | ||
| Integer gamesPlayed = (Integer) session.getAttribute("gamesPlayed"); | ||
| session.setAttribute("gamesPlayed", gamesPlayed == null ? 1 : gamesPlayed + 1); |
There was a problem hiding this comment.
Логика инкремента счетчика игр смешана с логикой обработки HTTP-запроса. Данную операцию следует вынести в отдельный сервисный слой.
| if (result.state() == GameState.WIN || result.state() == GameState.LOSE) { | ||
| Integer gamesPlayed = (Integer) session.getAttribute("gamesPlayed"); | ||
| session.setAttribute("gamesPlayed", gamesPlayed == null ? 1 : gamesPlayed + 1); | ||
| req.getRequestDispatcher("/WEB-INF/result.jsp").forward(req, resp); |
There was a problem hiding this comment.
Пути к JSP ресурсам ('/WEB-INF/game.jsp') дублируются в коде. Целесообразно определить их как константы, чтобы избежать ошибок при опечатках.
|
|
||
| public class GameLogic { | ||
|
|
||
| public static GameResult next(GameState current, String answer) { |
There was a problem hiding this comment.
Метод next() содержит сложную структуру switch-case, управляющую переходами состояний. Это классическое нарушение принципа открытости/закрытости (OCP). Рекомендуется использовать паттерн State.
| case START -> new GameResult(GameState.UFO_CHALLENGE, null); | ||
|
|
||
| case UFO_CHALLENGE -> { | ||
| if ("accept".equals(answer)) { |
There was a problem hiding this comment.
Строковые литералы для ответов (например, 'accept', 'go') разбросаны по всему коду. Рекомендуется использовать перечисления (Enum) для возможных действий игрока.
| } else { | ||
| yield new GameResult(GameState.LOSE, "Ты отклонил вызов. Поражение."); | ||
| } | ||
| } |
There was a problem hiding this comment.
Текстовые сообщения для пользователя жестко прописаны в логике. Для поддержки интернационализации (i18n) их следует вынести в ResourceBundle.
| @@ -0,0 +1,10 @@ | |||
| package com.javarush.alimov.quest; | |||
There was a problem hiding this comment.
Перечисление GameState содержит как промежуточные состояния, так и терминальные (WIN, LOSE). Стоит рассмотреть разделение логики навигации и статуса завершения.
| class GameLogicTest { | ||
|
|
||
| @Test | ||
| void testVictoryPath() { |
There was a problem hiding this comment.
Тест 'testVictoryPath' проверяет сразу всю цепочку игры. В юнит-тестах предпочтительнее проверять каждый переход состояния изолированно.
| result = GameLogic.next(result.state(), "truth"); | ||
|
|
||
| assertEquals(GameState.WIN, result.state()); | ||
| assertEquals("Тебя вернули домой. Победа!", result.message()); |
There was a problem hiding this comment.
Проверка текстовых сообщений в тестах делает их хрупкими. При изменении формулировки сообщения тесты упадут, хотя логика останется верной.
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| MockitoAnnotations.openMocks(this); |
There was a problem hiding this comment.
Использование MockitoAnnotations.openMocks(this) считается устаревшим. Рекомендуется использовать аннотацию @ExtendWith(MockitoExtension.class).
|
|
||
| @Test | ||
| void doPost_StartToUfoChallenge() throws ServletException, IOException { | ||
| when(session.getAttribute("state")).thenReturn(null); |
There was a problem hiding this comment.
Тест завязан на внутреннее поведение сессии. Лучше тестировать контракт взаимодействия через сервис, скрывающий детали работы с HttpSession.
|
|
||
| import jakarta.servlet.http.*; | ||
| import jakarta.servlet.annotation.*; | ||
| import jakarta.servlet.*; |
There was a problem hiding this comment.
Использование wildcard import (jakarta.servlet.http.*) не рекомендуется, так как это затрудняет понимание используемых зависимостей.
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) | ||
| throws ServletException, IOException { | ||
|
|
||
| HttpSession session = req.getSession(); |
There was a problem hiding this comment.
Отсутствует логирование действий пользователя или ошибок. Использование SLF4J помогло бы в диагностике проблем на сервере.
| @@ -0,0 +1,37 @@ | |||
| package com.javarush.alimov.quest; | |||
|
|
|||
There was a problem hiding this comment.
Класс GameLogic не имеет приватного конструктора, хотя содержит только статические методы. Следует предотвратить создание экземпляров утилитного класса.
| GameState state = (GameState) session.getAttribute("state"); | ||
| if (state == null) state = GameState.START; | ||
|
|
||
| String answer = req.getParameter("answer"); |
There was a problem hiding this comment.
Параметр 'answer' получается напрямую из запроса без валидации. Это потенциальная уязвимость, если данные будут использоваться в выводе без экранирования.
| session.setAttribute("message", result.message()); | ||
|
|
||
| if (result.state() == GameState.WIN || result.state() == GameState.LOSE) { | ||
| Integer gamesPlayed = (Integer) session.getAttribute("gamesPlayed"); |
There was a problem hiding this comment.
Счетчик игр хранится в сессии как Integer. При конкурентных запросах (хотя здесь это маловероятно) возможны проблемы. Стоит рассмотреть атомарные типы или сервисный слой.
|
|
||
| import java.io.IOException; | ||
|
|
||
| import static org.mockito.Mockito.*; |
There was a problem hiding this comment.
Статические импорты Mockito (when, verify) — это хорошая практика, стоит продолжать их использовать для чистоты тестов.
| import java.io.IOException; | ||
|
|
||
| @WebServlet("/game") | ||
| public class GameServlet extends HttpServlet { |
There was a problem hiding this comment.
Методы doGet и doPost содержат дублирующийся код вызова Dispatcher. Общую логику перехода можно вынести в защищенный метод.
|
|
||
| public enum GameState { | ||
| START, | ||
| UFO_CHALLENGE, |
There was a problem hiding this comment.
Названия состояний (например, UFO_CHALLENGE) выбраны удачно и соответствуют предметной области квеста.
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| class GameLogicTest { |
There was a problem hiding this comment.
Класс теста не имеет модификатора доступа public, что хорошо (пакетная видимость достаточна для JUnit 5).
No description provided.