6.A Diseñar una jerarquía de clases¶
En la unidad 5 viste cómo se escribe la herencia, las clases abstractas y las interfaces. Aquí se trata de decidir bien: qué clases hacen falta, qué va en la base y qué en cada hija, y cómo se organizan los constructores para que el diseño aguante cambios.
Un ejemplo completo: un hotel¶
abstract class Habitacion(val numero: Int) {
var libre = true
abstract fun tipo(): String
abstract fun precio(): Int
fun describir() = "$numero ${tipo()}: ${precio()} €/noche${if (libre) "" else " (ocupada)"}"
}
class Individual(numero: Int) : Habitacion(numero) {
override fun tipo() = "Individual"
override fun precio() = 50
}
class Doble(numero: Int, private val desayuno: Boolean = false) : Habitacion(numero) {
override fun tipo() = if (desayuno) "Doble con desayuno" else "Doble"
override fun precio() = if (desayuno) 90 else 80
}
class Suite(numero: Int, private val extras: Int = 0) : Habitacion(numero) {
override fun tipo() = "Suite"
override fun precio() = 150 + 20 * extras
}
class Hotel {
private val habitaciones = mutableListOf<Habitacion>()
val todas: List<Habitacion>
get() = habitaciones.toList()
fun agregar(habitacion: Habitacion) {
habitaciones.add(habitacion)
}
fun reservar(numero: Int) {
val habitacion = habitaciones.firstOrNull { it.numero == numero }
?: throw IllegalArgumentException("la habitación $numero no existe")
check(habitacion.libre) { "la habitación $numero ya está ocupada" }
habitacion.libre = false
}
fun libres() = habitaciones.filter { it.libre }
fun precioMedio(): Int {
var suma = 0
for (h in habitaciones) {
suma += h.precio()
}
return suma / habitaciones.size
}
}
fun main() {
val hotel = Hotel()
hotel.agregar(Individual(101))
hotel.agregar(Doble(201, desayuno = true))
hotel.agregar(Suite(301, extras = 2))
for (h in hotel.todas) {
println(h.describir())
}
hotel.reservar(201)
println("tras reservar la 201: ${hotel.todas[1].describir()}")
try {
hotel.reservar(201)
} catch (e: IllegalStateException) {
println("Error: ${e.message}")
}
try {
hotel.reservar(999)
} catch (e: IllegalArgumentException) {
println("Error: ${e.message}")
}
println("libres: ${hotel.libres().joinToString(", ") { it.numero.toString() }}")
println("precio medio: ${hotel.precioMedio()} €")
}
Salida:
101 Individual: 50 €/noche
201 Doble con desayuno: 90 €/noche
301 Suite: 190 €/noche
tras reservar la 201: 201 Doble con desayuno: 90 €/noche (ocupada)
Error: la habitación 201 ya está ocupada
Error: la habitación 999 no existe
libres: 101, 301
precio medio: 110 €
Cómo se ha decidido el diseño:
- La base lleva lo que es común a todas.
Habitaciontiene elnumeroy si estálibre, que valen para cualquier habitación, y un métododescribir()ya escrito. - Cada hija define solo lo que cambia. El
tipo()y elprecio()son abstractos: cada habitación sabe el suyo.DobleySuiteañaden sus propios datos (desayuno,extras). describir()usa los métodos abstractos. Es el patrón del método plantilla: la base fija el esquema, las hijas rellenan los huecos.- El hotel solo conoce
Habitacion. Si se añade un tipo nuevo,Hotelno cambia: es polimorfismo y composición («un hotel tiene habitaciones»). - Las reglas, dentro de su sitio. Reservar comprueba que la habitación exista y esté libre, y lanza una excepción antes de modificar nada. Además,
todasdevuelvehabitaciones.toList(), una copia: quien la recibe no puede estropear la lista interna. - Los constructores reparten el trabajo. La base inicializa el número; cada hija pasa ese dato hacia arriba y se queda con el suyo.
Modificadores que ayudan a diseñar¶
| Necesito... | En Kotlin |
|---|---|
| Clase que no se puede instanciar | abstract class |
| Clase de la que no se puede heredar | Es lo normal: todas son cerradas salvo que pongas open |
| Solo contrato | interface |
| Jerarquía cerrada | sealed class / sealed interface |
| Visible para las subclases | protected |
| Valor que no cambia tras crear el objeto | val |
Preguntas antes de crear una jerarquía¶
| Pregunta | Si la respuesta es... |
|---|---|
| ¿La relación es «es un»? | No → composición o interfaz, no herencia |
| ¿Hay código o datos comunes? | Sí → clase base (abstracta si no tiene sentido por sí sola) |
| ¿Solo hay que fijar un contrato? | Sí → interfaz |
| ¿Lo que cambia es un cálculo o una regla? | Valorar pasar una función (ver 4.E) |
| ¿Las variantes son un conjunto cerrado con datos distintos? | Jerarquía cerrada (ver 5.D) |
| ¿Se podrá usar cualquier hija donde se pida la base sin sorpresas? | Si no → hay que rediseñar (ver 6.C, principio L) |
Buenas prácticas¶
- Jerarquías cortas y anchas (una base y varias hijas) mejor que largas y estrechas.
- La base, estable. Cuanto más se cambia, más hijas se rompen. Si una base cambia a menudo, probablemente tiene demasiadas responsabilidades.
- Cierra lo que no necesites abrir (
final,sealed, el comportamiento por defecto de Kotlin): permitir heredar es una decisión, no un valor por defecto. - No expongas tus colecciones internas: ofrece métodos o una vista de solo lectura.
- Prefiere la composición cuando dudes entre heredar o tener un atributo.
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
| Una clase base con atributos que a algunas hijas no les sirven | Mover esos atributos a las hijas que los usan |
| Hijas que sobrescriben un método solo para «no hacer nada» o lanzar un error | Señal de que la jerarquía está mal (ver 6.C) |
| Repetir el mismo código en varias hijas | Subirlo a la base |
| Una base con métodos que solo usa una hija | Bajarlos a esa hija |
Para practicar¶
Haz los ejercicios de U6.1 · Jerarquía de clases. Para ver cómo se escribe en otro lenguaje: Pasar de uno a otro.