Patrones de Diseno y SOLID - vykymoon/rpg-character-creator GitHub Wiki

Resumen de patrones

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.

Builder

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: withRace y withClass deben llamarse antes de addSkill/addOutfit, o la validación no tiene contra qué comparar.
  • addSkill/addOutfit lanzan IllegalStateException si el ítem no aplica; Step5SummaryController la captura y la muestra con DialogUtils.warning.
  • Alternativa descartada: constructor telescópico o muchos setters públicos; con 5+ campos opcionales serían ilegibles y no validarían el orden.

Factory Method (data-driven)

Cada factory es el único punto que instancia su tipo. Internamente:

  1. JsonCatalogLoader.load(recurso, tipo) lee el JSON del classpath (con getResourceAsStream, por lo que funciona igual desde el IDE que desde un .jar).
  2. Gson rellena un DTO privado (RaceDefinition, etc.), separado del modelo para no acoplarlo al formato del archivo.
  3. Se valida (id y nombre obligatorios, ids duplicados) y se cachea en un LinkedHashMap que respeta el orden del archivo.
  4. createRace(id)/createAll() construyen instancias nuevas del modelo.

API: createRace("elfo"), createAll(), availableIds(), reload().

El enum RaceType/ClassType ya no existe: se usa el id como String.

Prototype

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).

Singleton

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úsquedas findRaceById, 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.

DAO

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.

Command

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.


Principios SOLID

S — Responsabilidad única

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.

O — Abierto/Cerrado

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);
}

D — Inversión de dependencias

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.

L e I

No hay jerarquías de herencia propias que lo exijan, y las interfaces (CharacterDAO, Command) son pequeñas y cohesivas.

Trade-offs asumidos

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
⚠️ **GitHub.com Fallback** ⚠️