Café y Código
⚡ C++ POO· Lección 9.1

9.1 Asociación de Clases: Relación "Tiene-Un" (1 a 1) ⚔️

Aprende cómo conectar dos entidades independientes en C++: haz que tu clase Jugador posea una Espada como atributo, delegue el combate e intercambie armas en caliente.

En el mundo real y en la arquitectura de software, los objetos rara vez existen de forma aislada. Un auto tiene un motor, una computadora tiene un procesador, y en un videojuego, un Jugador tiene una Espada.

1. ¿Qué es la Asociación y la Composición?

En POO existen dos formas principales de relacionar clases:

🔗 Composición / Asociación ("Tiene-Un")

Una clase almacena una instancia de otra clase dentro de sus atributos privados.
Ejemplo: Jugador tiene una Espada. Es la técnica más flexible y recomendada para ensamblar sistemas modulares.

🏛️ Herencia ("Es-Un")

Una clase deriva de otra más genérica.
Ejemplo: Guerrero es un Personaje. Se utiliza cuando hay una jerarquía de tipos (lo veremos en la lección 10).

El lema clásico de la ingeniería de software es: "Favorece la composición sobre la herencia". Al componer objetos, tus clases se mantienen desacopladas, testeables y fáciles de modificar.

2. Paso 1: Definir la Clase Componente (Espada)

Antes de que el Jugador pueda tener una espada, necesitamos que la clase Espada esté claramente modelada con sus propios atributos y encapsulamiento:

Espada como clase independiente
CPP
1class Espada {
2private:
3 string nombre;
4 int danio;
5 int durabilidad; // 0 a 100
6
7public:
8 Espada() : nombre("Desarmado"), danio(2), durabilidad(100) {}
9
10 Espada(string nombre, int danio, int durabilidad)
11 : nombre(nombre), danio(danio), durabilidad(durabilidad) {}
12
13 // Getters
14 string getNombre() const { return nombre; }
15 int getDurabilidad() const { return durabilidad; }
16
17 // El daño efectivo depende del estado del arma
18 int getDanio() const {
19 return (durabilidad > 0) ? danio : 1; // Un arma rota apenas hace 1 de daño
20 }
21
22 // Comportamiento del arma
23 void desgastar() {
24 if (durabilidad > 0) {
25 durabilidad -= 10;
26 if (durabilidad < 0) durabilidad = 0;
27 }
28 }
29};

3. Paso 2: La Clase Contenedora (Jugador)

Ahora declaramos el atributo Espada espadaEquipada; dentro de la clase Jugador. Fíjate cómo la inicializamos en la lista de inicialización del constructor:

Jugador integrando Espada como atributo de instancia
CPP
1class Jugador {
2private:
3 string nombre;
4 int salud;
5 int saludMaxima;
6 Espada espadaEquipada; // <- ASOCIACIÓN: Un objeto dentro de otro objeto
7
8public:
9 // El constructor recibe una Espada y la asigna a su atributo interno
10 Jugador(string nombre, int saludMax, Espada espadaInicial)
11 : nombre(nombre), salud(saludMax), saludMaxima(saludMax), espadaEquipada(espadaInicial) {
12 }
13
14 // Getter del arma asociada
15 Espada getEspada() const { return espadaEquipada; }
16
17 // Cambiar de arma dinámicamente en caliente
18 void equiparEspada(Espada nuevaEspada) {
19 espadaEquipada = nuevaEspada;
20 cout << "⚔️ " << nombre << " ahora empuña: " << espadaEquipada.getNombre() << "!\n";
21 }
22};

4. Delegación y Paso por Referencia en Métodos

Cuando el jugador ejecuta la acción atacar(Jugador &objetivo):

  1. Paso por referencia (Jugador &objetivo): Usamos el símbolo & para recibir la referencia directa del enemigo en memoria. Si no pusiéramos &, C++ crearía una copia efímera y los puntos de vida del enemigo original nunca bajarían.
  2. Delegación de responsabilidades: El jugador no inventa el cálculo de daño; le pregunta a su arma: int danio = espadaEquipada.getDanio(); y luego la desgasta con espadaEquipada.desgastar();.
Método de combate delegando en el objeto asociado
CPP
1void atacar(Jugador &objetivo) {
2 if (salud <= 0) {
3 cout << "💀 " << nombre << " no puede atacar porque está fuera de combate.\n";
4 return;
5 }
6
7 cout << "\n🗡️ " << nombre << " ataca a " << objetivo.getNombre()
8 << " blandiendo su " << espadaEquipada.getNombre() << "!\n";
9
10 // 1. Delegamos el daño en la espada
11 int danioInfligido = espadaEquipada.getDanio();
12
13 // 2. Aplicamos el daño al objetivo real mediante su referencia
14 objetivo.recibirDanio(danioInfligido);
15
16 // 3. Desgastamos el arma utilizada
17 espadaEquipada.desgastar();
18
19 if (espadaEquipada.getDurabilidad() == 0) {
20 cout << "⚠️ ¡CRAC! La " << espadaEquipada.getNombre() << " se ha roto por completo.\n";
21 }
22}

5. Ejemplo Completo y Ejecutable

Copia este código en tu entorno de desarrollo para ver en acción el combate completo, la durabilidad y el cambio dinámico de arma:

Asociación 1 a 1 entre Jugador y Espada en C++
CPP
1#include <iostream>
2#include <string>
3
4using namespace std;
5
6// --- 1. CLASE ASOCIADA: ESPADA ---
7class Espada {
8private:
9 string nombre;
10 int danio;
11 int durabilidad;
12
13public:
14 Espada() : nombre("Puños"), danio(5), durabilidad(100) {}
15
16 Espada(string nombre, int danio, int durabilidad)
17 : nombre(nombre), danio(danio), durabilidad(durabilidad) {}
18
19 string getNombre() const { return nombre; }
20 int getDurabilidad() const { return durabilidad; }
21
22 int getDanio() const {
23 return (durabilidad > 0) ? danio : 2; // Daño residual si está rota
24 }
25
26 void desgastar() {
27 if (durabilidad > 0) {
28 durabilidad -= 20;
29 if (durabilidad < 0) durabilidad = 0;
30 }
31 }
32};
33
34// --- 2. CLASE CONTENEDORA: JUGADOR ---
35class Jugador {
36private:
37 string nombre;
38 int salud;
39 int saludMaxima;
40 Espada espadaEquipada; // Atributo compuesto
41
42public:
43 Jugador(string nombre, int saludMax, Espada espadaInicial)
44 : nombre(nombre), salud(saludMax), saludMaxima(saludMax), espadaEquipada(espadaInicial) {}
45
46 string getNombre() const { return nombre; }
47 int getSalud() const { return salud; }
48 bool estaVivo() const { return salud > 0; }
49
50 void equiparEspada(Espada nuevaEspada) {
51 espadaEquipada = nuevaEspada;
52 cout << "✨ " << nombre << " ha equipado una nueva arma: " << espadaEquipada.getNombre() << "!\n";
53 }
54
55 void recibirDanio(int cantidad) {
56 salud -= cantidad;
57 if (salud < 0) salud = 0;
58 cout << " 💥 " << nombre << " recibe " << cantidad << " de daño. (Vida restante: " << salud << "/" << saludMaxima << ")\n";
59 }
60
61 void atacar(Jugador &objetivo) {
62 if (!estaVivo()) {
63 cout << "💀 " << nombre << " está inconsciente y no puede atacar.\n";
64 return;
65 }
66
67 cout << "🗡️ " << nombre << " ataca a " << objetivo.getNombre()
68 << " con " << espadaEquipada.getNombre() << "!\n";
69
70 objetivo.recibirDanio(espadaEquipada.getDanio());
71 espadaEquipada.desgastar();
72
73 if (espadaEquipada.getDurabilidad() == 0) {
74 cout << " ⚠️ ¡La " << espadaEquipada.getNombre() << " se ha roto!\n";
75 }
76 }
77
78 void mostrarEstado() const {
79 cout << "┌──────────────────────────────────────────────┐\n";
80 cout << " Héroe: " << nombre << " | HP: " << salud << "/" << saludMaxima << "\n";
81 cout << " Arma: " << espadaEquipada.getNombre()
82 << " (Daño: " << espadaEquipada.getDanio()
83 << ", Durabilidad: " << espadaEquipada.getDurabilidad() << "%)\n";
84 cout << "└──────────────────────────────────────────────┘\n";
85 }
86};
87
88// --- 3. SIMULACIÓN EN MAIN ---
89int main() {
90 // Fabricamos espadas
91 Espada espadaOxidada("Espada Oxidada", 18, 40);
92 Espada mandobleFuego("Mandoble de Fuego", 45, 100);
93
94 // Creamos jugadores vinculando sus espadas iniciales
95 Jugador heroe("Sir Arthur", 100, espadaOxidada);
96 Jugador monstruo("Gólem de Roca", 90, Espada("Golpe Rocoso", 14, 100));
97
98 heroe.mostrarEstado();
99 monstruo.mostrarEstado();
100
101 cout << "\n=== ⚔️ INICIO DEL COMBATE ===\n\n";
102
103 // Ronda 1
104 heroe.atacar(monstruo);
105 monstruo.atacar(heroe);
106
107 // Ronda 2: El arma del héroe se desgasta
108 heroe.atacar(monstruo);
109
110 // El héroe abre un cofre y equipa un arma superior en plena batalla
111 cout << "\n🎁 ¡Sir Arthur encuentra un tesoro en el campo de batalla!\n";
112 heroe.equiparEspada(mandobleFuego);
113
114 // Ronda 3 con nueva arma
115 heroe.atacar(monstruo);
116
117 cout << "\n=== ESTADO FINAL TRAS LA BATALLA ===\n";
118 heroe.mostrarEstado();
119 monstruo.mostrarEstado();
120
121 return 0;
122}
123
El Siguiente Paso Natural

9.2 Inventario de Objetos con std::vector (1 a N) 🎒

Un guerrero no solo lleva una espada: ¡también lleva una mochila con pociones que suben vida, venenos y elixires de poder!

Ir a Inventario de Objetos →

Ejercicio de Práctica

Crea una clase Escudo con atributos nombre, defensa y durabilidad. Luego agrégale al Jugador un atributo Escudo escudoEquipado;. En el método recibirDanio(), calcula el daño real restando la defensa del escudo antes de descontar vida.

Autoevaluación Formativa

Comprueba lo que Has Aprendido 🧠

Valida tu comprensión de composición, paso por referencia y delegación entre objetos en C++.

Dato curioso: C++ apareció en 1985; Bjarne Stroustrup en Bell Labs. Wikipedia

Ko-fi
Donaciones
Apoyá cafeycodigo con un café en Ko-fi. Colaboradores: insignia, muro y zona exclusiva.