TL;DR
- El Problema: Optimización de complejidad temporal y espacial para estructuras de datos clave.
- El Enfoque: Problema estilo CTCI 7.2 para principiantes: respondents, managers y directors. Enruta cada llamada al rango libre más bajo que pueda atenderla, escala cuando haga falta e implementa dispatchCall.
- Complejidad: Relación óptima de tiempo y espacio con gestión de casos límite.
Llega una llamada a recepción. Debe ir primero a un respondent libre. Si esa persona no puede cerrarla, la llamada sube a un manager. Si los managers tampoco pueden, va a un director. La gente ocupada no roba trabajo a gente libre de rango más bajo. Ese es todo el diseño: rangos, listas de libres y un método que asigna trabajo.
Este post es enseñanza original para principiantes en Java. Misma familia de diseño orientado a objetos en entrevista, no una copia de libro. Parte de la serie CTCI en Java. Capítulo 7, object-oriented design, problema 7.2.
1. Analogía cotidiana
Piensa en un help desk de tres pisos:
- Piso 1: muchos respondents con auriculares. La mayoría de restablecimientos de contraseña y "cómo inicio sesión" mueren aquí.
- Piso 2: unos pocos managers. Disputas de facturación, excepciones de política, clientes enfadados que ya probaron el piso 1.
- Piso 3: uno o dos directors. Llamadas raras legales o de caídas del servicio.
Una llamada empieza en el piso 1 salvo que ya sepas que necesita un rango más alto. Si el piso 1 está lleno, la llamada espera o subes de piso solo cuando el problema de verdad lo exige. Escalar no es "saltar al jefe." Es "este piso no puede terminar, prueba el de arriba."
Tu trabajo en la entrevista no es simular audio de teléfono. Es nombrar tipos, guardar empleados libres por rango y escribir dispatchCall para que la persona libre correcta tome la siguiente llamada.
2. Problema en palabras simples
Objetivo: diseño de clases para un call center con tres rangos de empleado y enrutado de llamadas con escalado.
Rangos (de menor a mayor):
| Rango | Rol típico |
|---|---|
| Respondent | primera línea; resuelve la mayoría |
| Manager | segunda línea; casos difíciles |
| Director | línea superior; escalados raros |
Comportamientos clave:
- Una llamada entrante se asigna a un empleado libre que pueda manejar su rango (empieza en respondent salvo que ya pida más alto).
- Si el asignado no puede resolverla, la llamada escala un rango y se reasigna.
- Si nadie libre puede tomarla, va a una cola de espera de ese rango (o se retiene hasta que alguien se libere).
- Cuando un empleado termina una llamada, queda libre y puede sacar la siguiente en espera.
Método principal a implementar:
void dispatchCall(Call call);
También útil:
void callCompleted(Employee emp); // liberar empleado, asignar siguiente en espera si hay
void escalate(Call call); // subir rango y despachar otra vez
Aclara antes de codificar:
- ¿Cuánta gente por rango? (Listas fijas valen en la entrevista.)
- ¿Puede un director atender una llamada de respondent si todos los respondents están ocupados? (A menudo sí: un rango más alto cubre trabajo inferior cuando está libre.)
- ¿Y si todos están ocupados? (Encola por rango; no la tires en silencio.)
- ¿Seguridad entre hilos? (Modelo monohilo primero; habla de locks solo si te lo piden.)
3. Piensa primero
Tipos que casi siempre necesitas
Rankenum:RESPONDENT,MANAGER,DIRECTORcon un entero de nivel para comparar y escalar.Call: quién llamó (string opcional), rango actual requerido, empleado que la atiende (o null).Employee: base abstracta con nombre, rango, flag libre/ocupado, llamada actual. Métodos:receiveCall,callCompleted,escalateAndReassign(o similar).Respondent/Manager/Director: subclases finas que fijan el rango en el constructor.CallCenter(oCallHandler): posee las listas de empleados y colas de espera; implementadispatchCall.
¿Por qué no una sola lista plana de empleados?
Puedes recorrer a todos buscando "libre y rango suficiente." Sirve para demos pequeñas y como primer boceto. En entrevista suele gustar más empleados agrupados por rango para despachar "mira rango R, luego R+1, luego R+2" sin barrer a gente demasiado junior para esa llamada.
Política de despacho (el núcleo)
Para una llamada que ahora necesita rango r:
- Busca un empleado libre en rango
r. - Si no hay, prueba libres en
r+1, luegor+2(rangos más altos pueden cubrir trabajo inferior). - Si aún no hay nadie, encola la llamada en su rango actual y déjala esperar.
- Si encontraste a alguien, márcalo ocupado, enlaza la llamada con él y a él con la llamada.
Política de escalado
Cuando un empleado no puede terminar:
- Desconecta al empleado de la llamada (queda libre; elige una historia y cúmplela).
- Sube el rango requerido de la llamada en uno (respondent → manager → director).
- Si ya es director y sigue atascada, déjala con el director o falla de forma explícita. Di la regla.
- Llama otra vez a
dispatchCallpara que alguien libre de rango más alto la tome (o se encole).
Herencia vs composición
Employee como base con tres subclases es la respuesta clásica. Las subclases casi no difieren: solo el valor de rango. Está bien. El punto de la jerarquía es:
CallCenterpuede guardarList<Employee>por rango.- Polimorfismo: todo empleado tiene
receiveCallygetRank.
Si odias subclases vacías, una sola clase Employee con un campo Rank también es honesta. Dilo en voz alta. A muchos entrevistadores aún les gustan los tres tipos con nombre porque el enunciado los nombró.
4. Solución en Java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
enum Rank {
RESPONDENT(0),
MANAGER(1),
DIRECTOR(2);
private final int value;
Rank(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public Rank next() {
if (this == DIRECTOR) {
return DIRECTOR; // already top
}
return Rank.values()[value + 1];
}
}
class Call {
private Rank rank;
private final String callerId;
private Employee handler;
public Call(String callerId) {
this.callerId = callerId;
this.rank = Rank.RESPONDENT; // start at first line
}
public Call(String callerId, Rank minRank) {
this.callerId = callerId;
this.rank = minRank;
}
public Rank getRank() {
return rank;
}
public void setRank(Rank rank) {
this.rank = rank;
}
public void escalateRank() {
this.rank = rank.next();
}
public String getCallerId() {
return callerId;
}
public Employee getHandler() {
return handler;
}
public void setHandler(Employee handler) {
this.handler = handler;
}
}
abstract class Employee {
private final String name;
private final Rank rank;
private boolean free = true;
private Call currentCall;
protected CallCenter center;
protected Employee(String name, Rank rank) {
this.name = name;
this.rank = rank;
}
public void setCallCenter(CallCenter center) {
this.center = center;
}
public String getName() {
return name;
}
public Rank getRank() {
return rank;
}
public boolean isFree() {
return free;
}
public void receiveCall(Call call) {
free = false;
currentCall = call;
call.setHandler(this);
}
/** Employee finished work on this call successfully. */
public void callCompleted() {
if (currentCall != null) {
currentCall.setHandler(null);
currentCall = null;
}
free = true;
// ask center to give me the next waiting call I can take
if (center != null) {
center.assignCall(this);
}
}
/**
* Cannot finish. Free self, raise call rank, redispatch.
* Returns true if escalation happened.
*/
public boolean escalateAndReassign() {
if (currentCall == null) {
return false;
}
Call call = currentCall;
currentCall = null;
free = true;
call.setHandler(null);
if (call.getRank() == Rank.DIRECTOR) {
// nowhere higher; put back in director wait queue
center.enqueueCall(call);
center.assignCall(this);
return false;
}
call.escalateRank();
center.dispatchCall(call);
center.assignCall(this); // this person may take another waiting call
return true;
}
}
class Respondent extends Employee {
public Respondent(String name) {
super(name, Rank.RESPONDENT);
}
}
class Manager extends Employee {
public Manager(String name) {
super(name, Rank.MANAGER);
}
}
class Director extends Employee {
public Director(String name) {
super(name, Rank.DIRECTOR);
}
}
class CallCenter {
// employees[0] = respondents, [1] = managers, [2] = directors
private final List<List<Employee>> employees = new ArrayList<>();
// wait queues per rank
private final List<Queue<Call>> callQueues = new ArrayList<>();
public CallCenter(int numRespondents, int numManagers, int numDirectors) {
employees.add(new ArrayList<>());
employees.add(new ArrayList<>());
employees.add(new ArrayList<>());
callQueues.add(new LinkedList<>());
callQueues.add(new LinkedList<>());
callQueues.add(new LinkedList<>());
for (int i = 0; i < numRespondents; i++) {
addEmployee(new Respondent("R" + i));
}
for (int i = 0; i < numManagers; i++) {
addEmployee(new Manager("M" + i));
}
for (int i = 0; i < numDirectors; i++) {
addEmployee(new Director("D" + i));
}
}
private void addEmployee(Employee e) {
e.setCallCenter(this);
employees.get(e.getRank().getValue()).add(e);
}
/** Assign call to first free employee at call.rank or higher. */
public void dispatchCall(Call call) {
Employee emp = getCallHandler(call);
if (emp != null) {
emp.receiveCall(call);
} else {
enqueueCall(call);
}
}
void enqueueCall(Call call) {
callQueues.get(call.getRank().getValue()).offer(call);
}
/**
* Free employee looks for a waiting call they can handle:
* any queue at their rank or lower (they can cover junior work).
*/
public void assignCall(Employee emp) {
for (int r = emp.getRank().getValue(); r >= 0; r--) {
Queue<Call> q = callQueues.get(r);
if (!q.isEmpty()) {
Call call = q.poll();
emp.receiveCall(call);
return;
}
}
}
/** First free employee with rank >= call.getRank(). */
private Employee getCallHandler(Call call) {
for (int r = call.getRank().getValue(); r <= Rank.DIRECTOR.getValue(); r++) {
for (Employee e : employees.get(r)) {
if (e.isFree()) {
return e;
}
}
}
return null;
}
}
Recorrido:
| Paso | Acción | Resultado |
|---|---|---|
| 1 | Centro con 2 respondents, 1 manager, 1 director | todos libres |
| 2 | dispatchCall(Call("A")) |
R0 toma A (rango respondent) |
| 3 | dispatchCall(Call("B")) |
R1 toma B |
| 4 | dispatchCall(Call("C")) |
no hay respondent libre; el manager toma C (el superior cubre lo inferior) |
| 5 | R0 no puede terminar A → escala | A pasa a rango manager; si el manager está ocupado, A espera o la toma el director |
| 6 | R0 hace callCompleted tras otra llamada |
R0 libre; puede sacar una llamada en espera de nivel respondent |
Puedes cambiar el paso 4 para que rangos bajos ocupados nunca suban solos a managers, y solo escalar cuando un empleado falla. Ambas políticas valen si las nombras. El código de arriba prefiere "rangos libres más altos cubren trabajo inferior cuando la primera línea está llena," lo que encaja con muchos centros reales y evita la cola de espera si un manager está ocioso.
5. Tabla de complejidad
| Operación | Tiempo | Notas |
|---|---|---|
dispatchCall |
O(E) peor caso | recorre libres desde el rango requerido hacia arriba; E = plantilla (pequeña en entrevista) |
getCallHandler |
O(E) | bucles anidados por rangos y personas |
enqueueCall |
O(1) | offer en una cola |
assignCall |
O(R + 1) | R es el índice de rango; mira colas desde el rango del emp hacia abajo |
escalateAndReassign |
O(E) | escala y luego dispatchCall |
Espacio O(E + W) por empleados más llamadas en espera. En la entrevista la charla de complejidad es secundaria frente a tipos claros y propiedad: quién tiene el flag free, quién las colas de espera, quién muta el rango de la llamada.
6. Casos límite y errores frecuentes
Los entrevistadores pinchan esto:
- Todos ocupados: la llamada debe entrar en cola de espera, no desaparecer.
- La llamada ya pide manager: no empieces en respondent; respeta el rango de
Call. - Escalar en director: no hay rango superior; reencola o marca fallo. No hagas bucle infinito en
next(). - Empleado termina con colas de trabajo inferior: el libre debe sacar trabajo que pueda manejar.
- Doble free / doble assign: tras
receiveCall,freees false; tras completar o escalar, limpiacurrentCallantes de quedar free. - Center null en el empleado: hace falta una referencia de vuelta (o pasar
CallCentera los métodos) para que el escalado pueda redespachar.
Errores comunes:
- Sin rango en la llamada. Entonces no puedes escalar; solo sabes quién está libre.
- Una lista global de libres sin filtro de rango. Un director siempre roba trabajo de respondent aunque haya respondents libres (malo para coste y para la historia de diseño). Prefiere "el rango libre más bajo suficiente primero."
- Olvidar colas de espera. Un
dispatchCallque solo devuelve cuando hay alguien libre falla bajo carga. - Escalado que mantiene al mismo empleado ocupado. Escalar debe liberar al handler actual y buscar a otra persona (o encolar).
- Árboles de herencia enormes. Tres rangos y un center bastan. No inventes
TeamLead,ShiftSupervisoryRegionVPsi no te lo piden. - Hablar de hilos antes de la estructura. Los locks importan en producción; la entrevista primero quiere propiedad correcta monohilo.
Idea mínima de prueba:
CallCenter center = new CallCenter(2, 1, 1);
Call a = new Call("alice");
Call b = new Call("bob");
Call c = new Call("cara");
center.dispatchCall(a);
center.dispatchCall(b);
center.dispatchCall(c); // may land on manager if both respondents busy
// simulate first-line cannot finish
Employee handler = a.getHandler();
if (handler != null) {
handler.escalateAndReassign();
}
7. Resumen para contárselo a un amigo
Call Center es un problema de diseño de objetos sobre quién puede tomar qué llamada:
- Tres rangos: respondent, manager, director. Codifícalos como enum con orden.
- Un
Callempieza en respondent (o un min rank dado) y puede subir un peldaño cada vez. Employeesabe free/busy, rango y la llamada actual. Las subclases solo fijan el rango.CallCenter.dispatchCallencuentra la primera persona libre en el rango de la llamada o superior; si no, encola.- Escalar libera al handler actual, sube el rango de la llamada y despacha otra vez.
- Cuando alguien termina, queda libre y puede sacar una llamada en espera que pueda cubrir.
Si puedes dibujar los tres pisos, señalar listas de libres y colas de espera, y recorrer una llamada de respondent a manager sin perderla, dominas el 7.2. Lo siguiente del capítulo 7 es un jukebox: otro diseño con "nombres" más claros y menos escalados en vivo.
Serie
- Guía: guía de la serie CTCI
- Anterior: Deck of Cards
- Siguiente: Jukebox
