- Decompose all 11 god-classes (>200 lines) into facade + collaborators (command/, schematic/, config/, generator/, mode/, papi/, adaptive/, storage/, player/, leaderboard/) — every file is now <=200 lines - Add PMD 7 (report-only) + stricter checkstyle rules (NPath, IllegalCatch, NestedDepth, Whitespace, MissingOverride); fix real bugs found (printStackTrace, resource leaks in ConfigLoader/SQLStatsStorage) - docs/ARCHITECTURE.md: target structure + 'one folder = one purpose' rule - Pilot restructure: command/core|util/, mode/impl/solo|multi|barrier/, hook/papi/resolver/; cross-package classes made public - TODO.md: track warning cleanup (132-138) + restructure progress
28 KiB
Целевая архитектура LoParkour
Руководство для последующих рефакторингов god-классов и добавления новых режимов (Duels, Elytra). Документ описывает целевое состояние; текущее зафиксировано в разделе «Текущее состояние».
Принципы проектирования
-
KISS — минимум слоёв и абстракций для решения задачи; каждый класс делает одну вещь.
-
DRY — одна реализация фичи в одном месте; дублирование выносится в общий переиспользуемый слой (напр. рендеринг/размещение блоков — в
block/). -
SOLID:
- SRP — у класса ровно одна ответственность (главный драйвер декомпозиции god-классов);
- OCP — новые режимы добавляются новыми подклассами/реализациями, без правки существующих;
- ISP — узкие интерфейсы (
Mode,MultiMode,GeneratorEventListener); - DIP — домен зависит от абстракций, инфраструктура зависит от домена.
-
Композиция вместо наследования — god-классы разбиваются на коллабораторов, инжектируемых в тонкий фасад (паттерн уже применён в
ParkourGenerator,BlockPlacer,StorageSQL,Leaderboard— развиваем его). -
Границы размера (жёсткие, зафиксированы в
config/checkstyle/checkstyle.xml, severity=error):- файл ≤ 200 строк (
FileLength), метод ≤ 80 строк (MethodLength), параметров ≤ 7 (ParameterNumber), линия ≤ 150 символов (LineLength),NPathComplexity≤ 50, цикломатическая сложность ≤ 20, вложенность if/for/try ≤ 3/2/2.
- файл ≤ 200 строк (
-
Явный порядок зависимостей — слои смотрят «вниз», циклы запрещены:
api/→core/(домен) →block/+ subsystems (generator/,schematic/,adaptive/,ghost/,style/,world/,leaderboard/,reward/,duels/,elytra/) → платформа (storage/,config/,hook/,listener/,command/,menu/,util/). -
Новые файлы обязаны соответствовать всем правилам — их нельзя добавлять в
suppressions.xml. Супрессии — только временный долг существующих legacy-файлов, удаляются по мере рефакторинга. -
Одна папка = одна цель. Каждый пакет содержит файлы ОДНОЙ ответственности (пример нормы:
command/schematic/— все 6 файлов про схематик-команды). Если в папку попадают разные по смыслу классы — режем на подпапки по под-целям (пример:util/gui— это часть подсистемы меню, а не общий util;mode/impl/— соли-режимы, мульти-режимы и барьеры — разные цели). Ориентир: ~3-5 файлов на папку; папка из 1 файла допустима, если это самостоятельная единица (util, model, msg), но НЕ ради искусственной нарезки.
Текущее состояние (кратко)
- 173 Java-файла в
src/main/java/dev/loki/loparkour/(база пакетаdev.loki.loparkour), Java 21, Paper 1.20.4 / Spigot API + Adventure, сборка shadow + checkstyle + PMD. - Декомпозиция по паттерну «фасад + коллабораторы» уже частично проведена (
ParkourGenerator,BlockPlacer,StorageSQL,Leaderboard,SessionStateManager), но фасады и часть режимов всё ещё превышают лимит 200 строк.
God-классы (>200 строк) и где они «протекают»:
| Класс | Строк | Протекающие обязанности |
|---|---|---|
command/LoParkourCommand |
302 | роутинг по аргументам (0..4) + join/leaderboard логика + весь tab-complete |
command/schematic/SchematicCommandHandler |
274 | сабкоманды схем + per-player кэш SELECTIONS |
config/core/ConfigUpdater |
229 | шаблон-merge, YAML-парсинг full-path, бэкап/восстановление |
generator/jump/placement/BlockPlacer |
227 | выбор BlockData, расчёт прыжка, запись блоков, эффекты, паста схем |
mode/impl/InvisibleBarrierMode |
222 | Mode-фасад + BarrierGenerator + отрисовка контуров частицами |
hook/papi/PAPIHook |
220 | метаданные expansion + роутинг + форматтеры рангов/сложности |
command/admin/AdminCommandHandler |
214 | forcejoin/forceleave/reset/recoverinventory + резолв игроков |
adaptive/core/MetricsCollector |
214 | метрики, near-miss-анализ, автосейв, кэш игроков |
storage/sql/StorageSQL |
209 | инициализация пула, DDL, read/write скоров, read/write игрока |
player/core/ParkourPlayer |
207 | Expose-настройки, скоринг-сет, статические реестры, сохранение, setup/hotbar |
leaderboard/core/Leaderboard |
203 | CRUD, сортировка, тай-брейк, снапшоты рангов |
mode/impl/CoopMode |
200 | на границе: MultiMode + CoopGenerator (contributions/милстоуны) |
Сопутствующие риски из TODO.md, влияющие на декомпозицию: статические mutable-поля
(GeneratorState — 15 публичных полей, ParkourGenerator.session/island/profile/state/generatorOptions —
п.107), рекурсия unregister/Coop (п.24), утечка SQL-пулов (п.27), per-player GUI-состояние (п.78),
неатомарный reload (п.70).
Целевая структура пакетов
Новые режимы выносятся в отдельные корневые пакеты duels/ и elytra/ (не в существующий mode/impl/).
Общий переиспользуемый рендеринг/размещение блоков — в block/. Ядро домена — в core/.
Существующие пакеты сохраняются и встраиваются в слои.
dev.loki.loparkour
│
├── api/ # Публичный контракт для внешних плагинов (единственный «экспортный» слой)
│ ├── core/ # ParkourAPI, Registry — регистрация режимов/стилей/событий
│ └── event/ # Доменные события (join/leave/score/fall/schematic/spectate/block-generate)
│
├── core/ # ЯДРО ДОМЕНА: агрегаты и доменные сервисы (слой ниже api/)
│ ├── model/ # Неизменяемые доменные модели (Score, Reward, GhostData и т.п.)
│ ├── player/ # Агрегат игрока: ParkourPlayer, ParkourUser, ParkourSpectator
│ ├── session/ # Session + жизненный цикл сессии (create/join/leave/removePlayers)
│ ├── mode/ # Контракты режимов: Mode, MultiMode, Modes
│ └── service/ # Доменные сервисы (UserRegistry, ParkourHotbar, ScoreboardManager)
│
├── block/ # ОБЩИЙ слой размещения/рендеринга блоков (для ВСЕХ генераторов и режимов)
│ ├── engine/ # BlockWriter: запись BlockData в мир + BlockEffectPlayer (particles/sound)
│ ├── selection/ # BlockSelector: выбор BlockData по стилям/весам (хук selectBlockData для режимов)
│ ├── style/ # BlockStyleProvider: материал/цвет/сдвиг блока по стилю
│ ├── jump/ # JumpCalculator, JumpValidator, JumpDirector, JumpOffsetGenerator, JumpType
│ ├── schematic/ # Паста схематик в мир (SchemPaster/StructurePaster, событие генерации)
│ ├── render/ # Частице-рендер: BlockOutlineRenderer, контуры/боксы (из InvisibleBarrierMode)
│ └── client/ # Клиент-сайд виртуальные блоки (без записи в мир) — использует Elytra
│
├── generator/ # SUBSYSTEM «бесконечная генерация» (сохраняется, использует block/)
│ ├── core/coordinator/ # ParkourGenerator (фасад), GeneratorProfileManager, GeneratorStatistics
│ ├── core/model/ # GeneratorState, Profile, Island, GeneratorOption
│ ├── jump/calculation/ # Расчёт прыжков (мигрирует в block/jump после стабилизации API)
│ ├── jump/placement/ # Оркестрация генерации блока (DEFAULT/SPECIAL/SCHEMATIC)
│ ├── lifecycle/loop/ # Тик/фолл/ресет/события генератора
│ ├── lifecycle/player/ # PlayerInteractionHandler, GeneratorCleanup
│ └── profile/ # GenerationProfileLoader (профили сложности)
│
├── schematic/ # SUBSYSTEM «схематики»: core/create/convert/nbt/schem/registry/legacy
├── adaptive/ # SUBSYSTEM «адаптивная сложность»: core, model, storage, bootstrap
├── ghost/ # SUBSYSTEM «ghost-реплей»: core (GhostManager/Recorder/Player), model
├── style/ # Визуальные темы: Style, RandomStyle
├── world/ # Зоны/острова: Divider, World
├── leaderboard/ # Лидерборды: core (Leaderboard/Sorter), model (Score), persistence
├── reward/ # Награды: Reward, Rewards
│
├── duels/ # НОВЫЙ РЕЖИМ «Duels» (корневой пакет, генератор на игрока)
│ ├── mode/ # DuelsMode implements MultiMode (create/join/leave, isAcceptingPlayers)
│ ├── generator/ # DuelsGenerator (owner) + SingleDuelsGenerator (генератор на игрока)
│ ├── match/ # goal, счёт, спавн-площадки, цвета игроков, initCountdown/win
│ └── menu/ # MultiplayerMenu / lobby invite (фильтр instanceof MultiMode)
│
├── elytra/ # НОВЫЙ РЕЖИМ «Elytra» (корневой пакет, клиент-сайд труба)
│ ├── mode/ # ElytraMode + вариации (Default/SpeedDemon/MinSpeed/TimeTrial/Close/Obstacle)
│ ├── section/ # ElytraSection, KnotDirector (сплайн/кольца), PointType
│ ├── render/ # Клиент-сайд рендер колец (sendBlockChanges)
│ └── generator/ # ElytraGenerator (свой tick()/fall-детект), фейерверк-буст
│
├── player/ # Существующий пакет (3-й слой, обслуживает core/player)
│ ├── core/ # ParkourPlayer, ParkourUser (агрегаты)
│ ├── data/ # PreviousData, InventoryData (save/restore состояния)
│ ├── service/ # ParkourHotbar, UserRegistry, ScoreboardManager, PlayerSettingsManager, BungeeUtil
│ └── spectator/ # ParkourSpectator
├── session/ # Session + Session*Manager (State/Player/User; тик пер-сессии)
├── mode/ # Существующие режимы: base/ (контракты), impl/solo|multi|barrier/
├── storage/ # Платформа хранения: Storage, disk/StorageDisk, sql/StorageSQL + SQL*
├── config/ # Платформа конфигурации: core, locale, options
├── hook/ # Платформенные интеграции: papi/ (+ resolver/), vault/, holo/, floodgate/
├── listener/ # Bukkit-листенеры: gameplay/, player/, schematic/
├── command/ # Команды: core/ (LoParkourCommand+Router+TabCompleter), util/, admin/, player/, schematic/
├── menu/ # GUI: core/, community/, lobby/, play/, settings/
└── util/ # Общие утилиты: gui/, item/, misc/, particle/, text/, world/
Текущее состояние (после пилотной реструктуризации)
Правило «одна папка = одна цель» применено к 3 пилотным областям (остальные — в работе):
command/—core/(LoParkourCommand, CommandRouter, CommandTabCompleter — вход/роутинг/табы),player/(PlayerCommandHandler, JoinCommandExecutor, LeaderboardCommandExecutor),util/(CommandUtil),admin/,schematic/.mode/impl/—solo/(DefaultMode, SpeedrunMode, GravityShiftMode),multi/(CoopMode, RaceMode, SpectatorMode),barrier/(InvisibleBarrierMode, BarrierGenerator, BarrierRenderer).hook/papi/—PAPIHook+PlaceholderDispatcher(вход/диспатч),resolver/(Global/Player/ScoreRank резолверы + PlaceholderFormatters).
Пересекающие пакеты классы стали public (JoinCommandExecutor, LeaderboardCommandExecutor, CommandUtil,
резолверы) — сигнатуры методов не менялись, только видимость.
Карта миграции
Таблица «старый класс → решение» для god-классов. split — разделить на новых коллабораторов,
keep — оставить как есть, move — перенести ответственность в другой пакет без изменения поведения.
| Старый класс | Решение | Новые классы (пакет · ответственность) |
|---|---|---|
command/LoParkourCommand |
split | command/CommandRouter (диспетчер args.length 0..4) · command/CommandContext (sender/player/args-обёртка, сокращает параметры) · command/JoinCommand (join: режим/игрок/сессия) · command/LeaderboardCommand · command/completion/CommandCompleter (весь onTabComplete) · command/completion/SchematicCompleter + ModeCompleter (подкомандные) · player/PlayerCommandHandler, admin/AdminCommandHandler, schematic/SchematicCommandHandler — keep (уже выделены) |
command/schematic/SchematicCommandHandler |
split | command/schematic/SchematicCommandRouter (роутинг) · .../SchematicCreateHandler · .../SchematicPasteHandler · .../SchematicWandSession (per-player SELECTIONS, cleanup на quit — чинит п.84) · .../SchematicConvertHandler |
command/admin/AdminCommandHandler |
split | command/admin/ForceJoinHandler · .../ForceLeaveHandler · .../ResetHandler · .../RecoverInventoryHandler (по сабкоманде) · command/admin/PlayerResolver (resolveUUID/resolvePlayerName/findNearest; поддержка BlockCommandSender — п.119) |
config/core/ConfigUpdater |
split | config/core/ConfigTemplate (чтение шаблона) · config/core/YamlPathParser (full-path-парсинг, из extractFullPathValues) · config/core/ConfigMerger (точечный merge отсутствующих ключей, сохранение листов/комментариев — фикс п.26) · config/core/ConfigBackup (бэкап/восстановление) · ConfigUpdater = тонкий фасад (keep) |
generator/jump/placement/BlockPlacer |
split+move | block/engine/BlockWriter (placeBlockData, setBlockData+phys) · block/engine/BlockEffectPlayer (particles/sound на всех игроков) · block/selection/BlockSelector (move, уже есть) · block/jump/JumpCalculator (move, уже есть) · generator/jump/placement/BlockGenerationOrchestrator (generateSingleBlock, switch DEFAULT/SPECIAL/SCHEMATIC) · generator/jump/placement/SchematicBlockGenerator (tryGenerateSchematic) · block/jump/TallMaterialResolver (isTallMaterial + форм.высота) |
mode/impl/InvisibleBarrierMode |
split+move | mode/impl/InvisibleBarrierMode (keep, тонкий фасад) · mode/impl/barrier/BarrierGenerator (keep как есть, переезжает в подпакет) · mode/impl/barrier/BarrierVisualState (activeBarriers + blockColors + self-clean) · block/render/BlockOutlineRenderer (drawEdge/drawBlockOutline) · block/render/ParticleBoxGeometry (record Point + рёбра) |
hook/papi/PAPIHook |
split | hook/papi/PAPIHook (keep, тонкий: метаданные expansion + роутинг) · hook/papi/PlaceholderRegistry (плейсхолдер → resolver) · hook/papi/PlayerPlaceholders (score/time/lead/style/difficulty…) · hook/papi/LeaderboardPlaceholders (rank_*, leader/record) · hook/papi/DifficultyFormatter (parseDifficulty) · опционально ScoreUntilPlaceholder (score_until_N) · переименовать id witp → loparkour (п.118) |
storage/sql/StorageSQL |
split | storage/sql/StorageSQL (keep, фасад) · storage/sql/SQLPool (один Hikari-пул на плагин — фикс п.27, setInitializationFailTimeout(-1) п.28) · storage/sql/SQLSchemaManager (единый мигратор DDL — фикс п.62) · storage/sql/SQLScoreRepository (readScores/writeScores) · storage/sql/SQLPlayerRepository (readPlayer/writePlayer) · storage/sql/SQLCallbackQueue (runWhenConnected, фикс п.29) · SQLQueryBuilder, SQLDataMapper, SQLMigrationManager — keep |
player/core/ParkourPlayer |
split | core/player/ParkourPlayer (keep агрегат, тонкий) · player/data/PlayerSettings (Expose-поля настроек + их serialize/restore) · player/service/ScoreTracker (scoredBlocks/hasScored/markScored/blockKey) · player/service/PlayerPersistence (save/read) · player/service/PlayerSetup (setup/teleport/hotbar) · player/service/GeneratorSettingsSync (updateGeneratorSettings) · player/core/ParkourPlayerRegistry (статические getPlayer/getPlayers/isPlayer — заодно фикс O(n), п.47) |
leaderboard/core/Leaderboard |
split | leaderboard/core/Leaderboard (keep, фасад) · leaderboard/core/ScoreComparator (isBetterThan/тай-брейк) · leaderboard/core/LeaderboardQueries (getRank/getScoreAtRank/sort-снапшоты) · LeaderboardStorage, LeaderboardSorter — keep · не split: model/Score.getTimeMillis — bugfix парсинга mm:ss.SSS/HH:mm:ss.SSS и long (п.25) |
adaptive/core/MetricsCollector |
split | adaptive/core/MetricsCollector (keep: сценарии GeneratorEventListener) · adaptive/core/JumpAnalyzer (near-miss, время на блок) · adaptive/core/MetricsAutoSave (таймер автосейва, фикс Folia п.21) · adaptive/core/PlayerMetricsCache (кэш/выгрузка) · adaptive/storage/StatsRepository — keep |
mode/impl/CoopMode |
split (на границе 200) | mode/impl/coop/CoopMode (keep: MultiMode-фасад) · mode/impl/coop/CoopSession (создание лобби/инвайтов, создание сессии в create — фикс п.23) · mode/impl/coop/CoopGenerator (contributions/милстоны) · guard в unregister (фикс рекурсии п.24) |
Новые режимы
Оба режима разрабатываются сразу в чистом виде (полное соответствие checkstyle, ноль PMD-нарушений) —
новая функциональность не может «перевозить» legacy-долг. Код режима НЕ добавляется в suppressions.xml.
Duels — duels/* (в терминах задачи mode/duels/*)
Модель «генератор на игрока»: одна сессия-владелец, у каждого игрока свой генератор.
- Пакеты:
duels/mode/DuelsMode(implements MultiMode),duels/generator/DuelsGenerator(owner,Map<ParkourPlayer, SingleDuelsGenerator>, allowJoining, goal, spawnData),duels/generator/SingleDuelsGenerator(extends ParkourGeneratorсо своимstate/history/цветом/островом),duels/match/(goal, счёт, initCountdown, win с guardstopped, идемпотентный синхронныйreset(false)),duels/menu/MultiplayerMenu+ кнопка вPlayMenu. - Переиспользуют ядро:
ParkourGenerator(база),BlockPlacer+block/*,PlayerInteractionHandler(скоринг по своей трассе),ParkourHotbar(баннер-старт),PreviousData(сейв/восстановление),Session,UserRegistry. Результаты НЕ пишутся в лидерборд (getLeaderboard() = null). - Требования к ядру: хук
selectBlockData()(фиксированный цвет на игрока), пер-игроковая спавн-площадка со сдвигом, ослабитьfinal class Island, leave() безParkourUser.leave-рекурсии (guard-флаг, п.24). - Не повторять баги Race/Coop: без TIME-лидерборда (время брать своим форматтером из
state.start), ресет только синхронно с null-guard, сессия создаётся вcreate(),join()черезisAcceptingPlayers.
Elytra — elytra/* (в терминах задачи mode/elytra/*)
Один генератор на игрока, трасса = секции узлов → сплайн → «кольца»; клиент-сайд рендер без записи в мир.
- Пакеты:
elytra/section/ElytraSection,elytra/section/ElytraKnotDirector(dxN(75,15), dy-bias, dzN(0,35)),elytra/section/ElytraPointType(CIRCLE/FLAT),elytra/generator/ElytraGenerator(extends ParkourGenerator, свойtick(), переопределённыйgetMode(), свой fall-детект — не полагаться наLifecycleTickManager.checkPlayerFall),elytra/mode/(ElytraMode + Default/SpeedDemon/MinSpeed/TimeTrial/Close/Obstacle — по подклассу на режим),elytra/render/(очередьMap<chunkX, Set<Vector>>→player.sendBlockChanges(states), лимит min(viewDistance, 8)). - Переиспользуют ядро:
ParkourGenerator(база + профили сложности),ParkourHotbar,PreviousData,block/client/(виртуальные блоки), зонуDivider; скор — double (добавить float/double-вариантScoreили отдельный счётчик). - Требования к ядру: пер-игроковый
state(не разделяемый на сессию), высота/порог ASCEND под наши zone-границы, раздача элитр в слот нагрудника черезParkourHotbar. - Зависимости: Apache Commons Math (
SplineInterpolator) или Catmull-Rom — без стороннего мира.
Тесты новых режимов (п.131): сплайн/секции, ElytraGenerator.tick (score по velocity.x, причины reset),
DuelsGenerator.win (guard/идемпотентность), SingleDuelsGenerator.score→win при goal, регрессия рекурсии unregister.
Политика линтеров
- Checkstyle — жёсткий гейт (
isIgnoreFailures=false,maxErrors=0вbuild.gradle.kts): правилоFileLength(200) и прочие жёсткие проверки падают сборку. Сейчас сборка проходит0 errors, но копит ~1429 warning. - Шумные правила — severity=
warning, не блокируют (задано вcheckstyle.xml):MagicNumber,FinalLocalVariable,EmptyLineSeparator,ClassDataAbstractionCoupling,ClassFanOutComplexity,MissingDeprecated. Они не требуют супрессий и чистятся отдельным проходом. - Legacy-нарушения — точечные супрессии в
config/checkstyle/suppressions.xml(FileLength,IllegalCatch,NPathComplexity,WhitespaceAround). Каждая запись привязана к конкретному файлу, удаляется сразу после декомпозиции/зачистки этого файла. Новые файлы в супрессии не добавляются. - PMD — report-only (
isIgnoreFailures=true), сейчас 45 нарушений:CognitiveComplexity(17)+CyclomaticComplexity(7)+GodClass(4)+TooManyMethods(8)— декомпозиция;AvoidReassigningParameters(7)— переименование параметров;EmptyCatchBlock(2)— комментарий/тип. После зачистки (п.137)isIgnoreFailuresубирается, PMD становится гейтом (п.138). - В
TODO.md«Зачистка варнингов линтеров»:FinalLocalVariable~1034 (чистить вместе с декомпозицией),MagicNumber~346,WhitespaceAround30,NPathComplexity29,IllegalCatch22.
Порядок работ
- Архитектура — утвердить этот документ, зафиксировать целевое дерево пакетов и карту миграции.
- Декомпозиция god-классов — по карте миграции (BlockPlacer →
block/*, LoParkourCommand → роутеры, PAPIHook → реестры плейсхолдеров, StorageSQL → репозитории + один пул, ParkourPlayer → сервисы, InvisibleBarrierMode →block/render, ConfigUpdater → мержер и т.д.). После каждого файла — удаление его супрессии изsuppressions.xml(сборка обязана оставаться зелёной). - Зачистка варнингов —
FinalLocalVariable/MagicNumber/WhitespaceAround/NPath/IllegalCatchпо отчётамbuild/reports/checkstyle/; закрыть оставшиеся PMD-нарушения. - Elytra — база + режимы + клиент-сайд рендер (меньше зависимостей, чем Duels; S/M-сложность).
- Duels —
duels/*с предварительными хуками ядра (selectBlockData, пер-игроковый state,final Island); требует предварительно закрытого Coop-рекурсивного цикла (п.24). - PMD-гейт — убрать
isIgnoreFailures=true, PMD в CI как жёсткий гейт; удалить пустые записи изsuppressions.xml.