Patrones de Diseno y SOLID - vykymoon/rpg-character-creator GitHub Wiki
| Patrón | Categoría | Clase(s) | Problema que resuelve |
|---|---|---|---|
| Builder | Creacional | builder/CharacterBuilder |
Construir un Character paso a paso (una llamada por pantalla del wizard) y validar antes de build()
|
| Factory Method | Creacional |
RaceFactory, CharacterClassFactory, SkillFactory, OutfitFactory
|
Crear los objetos del catálogo sin acoplar al cliente, dejando que el catálogo crezca vía JSON |
| Prototype | Creacional | prototype/CharacterPrototype |
Clonar plantillas de NPC sin repetir el wizard |
| Singleton | Creacional | singleton/CatalogManager |
Una única copia cacheada del catálogo compartida por todas las pantallas |
| DAO | Estructural |
dao/CharacterDAO + CharacterDAOJson
|
Aislar la persistencia del resto del sistema |
| Command | Comportamiento |
game/command/Command, MoveCommand, game/InputHandler
|
Traducir teclas a acciones de movimiento en el modo un jugador |
Los primeros cinco son los documentados en el README del Corte 1. Command se añadió después, en el modo de juego.
Character c = new CharacterBuilder()
.withName("Aria")
.withRace(RaceFactory.createRace("elfo"))
.withClass(CharacterClassFactory.createClass("mago"))
.addSkill(skill) // valida contra raza y clase
.addOutfit(outfit) // ídem
.build(); // valida obligatorios y calcula stats-
Orden obligatorio:
withRaceywithClassdeben llamarse antes deaddSkill/addOutfit, o la validación no tiene contra qué comparar. -
addSkill/addOutfitlanzanIllegalStateExceptionsi el ítem no aplica;Step5SummaryControllerla captura y la muestra conDialogUtils.warning. - Alternativa descartada: constructor telescópico o muchos setters públicos; con 5+ campos opcionales serían ilegibles y no validarían el orden.
Cada factory es el único punto que instancia su tipo. Internamente:
-
JsonCatalogLoader.load(recurso, tipo)lee el JSON del classpath (congetResourceAsStream, por lo que funciona igual desde el IDE que desde un.jar). - Gson rellena un DTO privado (
RaceDefinition, etc.), separado del modelo para no acoplarlo al formato del archivo. - Se valida (id y nombre obligatorios, ids duplicados) y se cachea en un
LinkedHashMapque respeta el orden del archivo. -
createRace(id)/createAll()construyen instancias nuevas del modelo.
API: createRace("elfo"), createAll(), availableIds(), reload().
El enum
RaceType/ClassTypeya no existe: se usa elidcomoString.
CharacterPrototype guarda un Map<String, Character> de plantillas. withDefaultTemplates() registra tres NPC construidos con el propio Builder:
| Clave | Nombre | Raza / Clase |
|---|---|---|
bandido |
Bandido Base | Humano / Pícaro |
guardia |
Guardia Real | Enano / Guerrero |
mago |
Mago Errante | Elfo / Mago |
clone(key) devuelve una copia con nuevo UUID. Se usa desde la pantalla de plantillas (ver Galería y Persistencia).
CatalogManager.getInstance() (con synchronized) carga las cuatro listas al crearse. No contiene ningún dato del juego: solo cachea lo que producen las factories.
- Expone listas no modificables (
Collections.unmodifiableList) y búsquedasfindRaceById,findClassById,findSkillById,findOutfitById. - Devuelve las listas completas; filtrar por raza/clase es responsabilidad de los pasos 3 y 4.
-
reload()vuelve a leer los cuatro JSON sin reiniciar la app, útil al probar cambios en caliente.
public interface CharacterDAO {
void save(Character character); // crea o reemplaza por id
Optional<Character> findById(String id);
List<Character> findAll();
void delete(String id);
boolean existsByName(String name); // sin distinguir mayúsculas
}Los controladores declaran private final CharacterDAO characterDAO = new CharacterDAOJson();: dependen de la interfaz y solo el new conoce la implementación.
InputHandler mapea W/A/S/D a MoveCommand(dx, dy) y, en cada frame, ejecuta los comandos de las teclas pulsadas. La tecla E no es un comando: dispara un Runnable para abrir el inventario. Detalle en Modo Un Jugador.
Un controlador por pantalla del wizard (Step1NameRaceController … Step5SummaryController) en vez de un WizardController gigante. Step2ClassController solo llena el combo de clases, valida y avanza. CatalogRestriction concentra la regla de restricciones en un único lugar.
Antes, agregar una raza obligaba a editar un switch. Ahora se agrega un objeto a races.json y RaceFactory no cambia.
// Antes (hipotético) // Ahora
switch (id) { RaceDefinition d = definitions().get(normalize(id));
case "elfo": return new Race(...); if (d == null) throw new IllegalArgumentException(...);
... return build(d);
}La UI depende de CharacterDAO (abstracción), nunca de CharacterDAOJson como tipo declarado. Aplica en GalleryController, Step5SummaryController y TemplateGalleryController. En LAN, GameServer depende de GameSessionService, que depende del dominio y no de WebSocket.
No hay jerarquías de herencia propias que lo exijan, y las interfaces (CharacterDAO, Command) son pequeñas y cohesivas.
| Atributo | Cómo se sostiene | Qué se sacrificó |
|---|---|---|
| Mantenibilidad |
CharacterDAO separa negocio de persistencia |
Una capa más de indirección |
| Extensibilidad | Catálogos en JSON | Se pierde el chequeo de tipos del compilador: un error de tipeo en el JSON se detecta solo al ejecutar |
| Consistencia de datos | Singleton compartido | Estado global: hay que evitar mutar las listas por accidente |
| Corrección de reglas |
CatalogRestriction único |
Un bug ahí afecta habilidades y vestuario |