Tabla de contenido
Ver contenido
Introducción
¿La calidad depende del observador? En el software, la calidad es un concepto amplio y multifacético: no todos la perciben de la misma forma. Lo que para algunos usuarios representa una mejora, para otros puede ser una pérdida.
En términos generales, los usuarios esperan un producto que consideren justo en relación a su inversión. Para los ingenieros, en cambio, la calidad implica que el software sea robusto, flexible y fácil de mantener.
Por tanto, la calidad no se evalúa únicamente por su funcionalidad exterior, sino también por la forma en que está construido. Medir la calidad estructural permite tomar decisiones fundamentadas sobre el diseño.
¿Por qué cuantificar la calidad del diseño de software?
Si bien los ingenieros tienen criterio para identificar problemas de diseño, ese juicio puede ser subjetivo en muchos casos. Las métricas, por otro lado, permiten detectar problemas tangibles y justificar decisiones arquitectónicas con datos.
Métricas de Cohesión
LCOM (Lack of Cohesion of Methods)
Existen varias versiones de esta métrica. Nos centraremos en LCOM4, la más moderna y útil para evaluar la cohesión de clases.
LCOM4 evalúa qué tan cohesiva es una clase, basándose en cuántos grupos de responsabilidades (métodos) acceden a datos comunes de una instancia.
Cálculo de LCOM4
- Los constructores y destructores se excluyen del cálculo.
- Si todos los métodos acceden a los mismos atributos:
LCOM4 = 1
(clase cohesiva, responsabilidad definida). - Si hay grupos de métodos aislados entre sí:
LCOM4 > 1
(clase con múltiples responsabilidades). - Si ningún método accede a atributos:
LCOM4 = 0
(probablemente no es una clase valiosa).
Ejemplo en Ruby
Revisemos este caso en Ruby (considere que está hecho así por fines didácticos):
La clase Route modela una ruta de transporte en un sistema simulado. Su objetivo es representar una secuencia de estaciones por las que pasa un vehículo (como un autobús), y gestionar tanto el avance en la ruta como la generación de pasajeros.
Y tiene como responsabilidades:
- Saber si se llegó al final de la ruta (is_at_end?)
- Avanzar a la siguiente estación (next_stop)
- Obtener la estación de destino actual (get_destination_stop)
- Calcular distancias (get_total_route_distance, get_next_stop_distance)
- Generar pasajeros en cada estación (generate_new_passengers)
- Ejecutar la generación de nuevos pasajeros y actualizar las estaciones (update)
class Route
def initialize(name, stops, distances, num_stops, generator)
@name = name
@stops = stops
@distances_between = distances
@num_stops = num_stops
@generator = generator
@destination_stop_index = 0
@destination_stop = nil
end
def update
generate_new_passengers
@stops.each(&:update)
end
def is_at_end?
@destination_stop_index >= @num_stops
end
def next_stop
@destination_stop_index += 1
if @destination_stop_index < @num_stops
@destination_stop = @stops[@destination_stop_index]
else
@destination_stop = @stops.last
end
end
def get_destination_stop
@destination_stop
end
def get_total_route_distance
@distances_between.sum
end
def get_next_stop_distance
@distances_between[@destination_stop_index - 1]
end
def generate_new_passengers
@generator.generate_passengers
end
end
Al analizar la clase, podemos notar que tiene dos grupos de responsabilidades (métodos):
- Gestión de la trayectoria (Grupo 1):
is_at_end?
,next_stop
,get_destination_stop
,get_total_route_distance
,get_next_stop_distance
. Acceden a:@destination_stop_index
,@num_stops
,@stops
,@distances_between
,@destination_stop
- Gestión de pasajeros en relación a las estaciones (Grupo 2):
generate_new_passengers
,update
. Acceden a:@generator
,@stops
Si vamos a la definición, podemos notar que el uso de los atributos resaltan bastante bien esta división de responsabilidades.
El resultado de LCOM4 es igual a 2, lo que nos indica que la clase tiene 2 responsabilidades, lo que sugiere que podría refactorizarse.
Propuesta de división en clases
A continuación, refactorizaremos la lógica de la clase Route
en dos componentes:
- RouteTracker: se encarga de todo lo relacionado con la ruta, destino y el cálculo de distancias.
- PassengerManager: se encarga de la generación de pasajeros y la actualización de estado de las estaciones.
Es clave reconocer que la lógica de update
implica modificaciones dinámicas en el estado del atributo @stops (estaciones). Mientras que RouteTracker se enfoca en el seguimiento estructural de la ruta —como el avance y el cálculo de distancias—, es PassengerManager quien asume la responsabilidad de manipular el estado de los pasajeros y en relación a las estaciones.
class RouteTracker
def initialize(stops, distances, num_stops)
@stops = stops
@distances_between = distances
@num_stops = num_stops
@destination_stop_index = 0
@destination_stop = nil
end
def is_at_end?
@destination_stop_index >= @num_stops
end
def next_stop
@destination_stop_index += 1
@destination_stop = if @destination_stop_index < @num_stops
@stops[@destination_stop_index]
else
@stops.last
end
end
def get_destination_stop
@destination_stop
end
def get_total_route_distance
@distances_between.sum
end
def get_next_stop_distance
@distances_between[@destination_stop_index - 1]
end
end
class PassengerManager
def initialize(generator, stops)
@generator = generator
@stops = stops
end
def generate_new_passengers
@generator.generate_passengers
end
def update
generate_new_passengers
@stops.each(&:update)
end
end
class Route
def initialize(name, stops, distances, num_stops, generator)
@name = name
@tracker = RouteTracker.new(stops, distances, num_stops)
@passenger_manager = PassengerManager.new(generator, stops)
end
def update
@passenger_manager.update
end
def is_at_end?
@tracker.is_at_end?
end
def next_stop
@tracker.next_stop
end
def get_destination_stop
@tracker.get_destination_stop
end
def get_total_route_distance
@tracker.get_total_route_distance
end
def get_next_stop_distance
@tracker.get_next_stop_distance
end
def generate_new_passengers
@passenger_manager.generate_new_passengers
end
end
Como puedes observar, ahora la clase Route actúa como una API que delega responsabilidades a RouteTracker y PassengerManager, mejorando la testabilidad y la separación de responsabilidades.
Conclusión
El cálculo de métricas asociadas a la cohesión es una herramienta más para tomar decisiones de diseño basadas en datos. No debemos depender exclusivamente de una sola métrica, sino utilizarla como guía complementaria a nuestro criterio.
LCOM4 nos ayuda a identificar una posible violación del principio de responsabilidad única (SRP). En este ejemplo, propusimos una refactorización que separa las responsabilidades de manera más clara. Como toda métrica, su valor está en cómo se interpreta y aplica.