Tabla de contenido
Ver contenido
Introducción
Hablemos ahora del acoplamiento en relación a la calidad del software, en una entrega pasada hablamos de la cohesión y una forma de medirla mediante LCOM4. Mediremos ahora la inestabilidad de nuestro sistema mediante las métricas de clases acopladas dentro de un paquete.
Medición de Acoplamiento
El acoplamiento representa la dependencia entre clases. Idealmente, una clase debe:
- Encapsular información y responsabilidades relacionadas.
- Interactuar con otras clases solo cuando sea necesario.
Inestabilidad
Una medida del acoplamiento es la inestabilidad, que representa la fragilidad de una clase a cambios externos.
Cálculo de Inestabilidad
Inestabilidad = Acoplamiento Eferente / (Acoplamiento Aferente + Acoplamiento Eferente)
Por ejemplo, si una clase Bar
tiene un acoplamiento aferente de 1 (una clase depende de ella) y un acoplamiento eferente de 5 (ella depende de cinco clases), su inestabilidad será:
5 / (1 + 5) = 0.83
, una inestabilidad bastante alta.
Esto significa que la clase Bar
es fácil de cambiar, pues tiene poca dependencia de otras clases, mientras que por otro lado tiene muchas dependencias externas.
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.