From 164bf64096987da4043d24f11515b007fc30e8b3 Mon Sep 17 00:00:00 2001 From: Florian Dejonckheere Date: Sun, 14 Apr 2024 10:58:09 +0300 Subject: [PATCH] Add cohesion metric --- lib/mosaik/metrics/cohesion.rb | 60 +++++++++++++++++++++++ spec/mosaik/metrics/cohesion_spec.rb | 56 +++++++++++++++++++++ spec/support/contexts/directed_graph.rb | 12 ++--- spec/support/contexts/undirected_graph.rb | 12 ++--- spec/support/factories/metrics.rb | 2 + 5 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 lib/mosaik/metrics/cohesion.rb create mode 100644 spec/mosaik/metrics/cohesion_spec.rb diff --git a/lib/mosaik/metrics/cohesion.rb b/lib/mosaik/metrics/cohesion.rb new file mode 100644 index 0000000..d39cb57 --- /dev/null +++ b/lib/mosaik/metrics/cohesion.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module MOSAIK + module Metrics + ## + # Cohesion (S. Chidamber and C. Kemerer, 1994) + # + class Cohesion < Metric + def evaluate + # Total cohesion + cohesion = 0.0 + + # Iterate over each cluster + graph.clusters.each_value do |cluster| + # Find all vertices in the cluster + vertices_in_cluster = cluster.vertices + vertices_in_cluster_id = vertices_in_cluster.map(&:id) + + # Calculate the cardinality of the cluster + cardinality_c = vertices_in_cluster.sum do |v| + warn "No `methods` attribute found for #{v.id}, did you extract structural coupling information?" unless v.attributes.key?(:methods) + + v.attributes.fetch(:methods, 0.0) + end + + # Skip if the vertex has less than 2 methods (denominator would be 0) + if cardinality_c < 2 + warn "Cluster #{cluster.id} has less than 2 methods, skipping cohesion calculation" + + # Store cohesion value in the cluster + cluster.attributes[:cohesion] = 0.0 + + next + end + + # Calculate sum of edges between vertices in the cluster + sum = vertices_in_cluster + .flat_map { |v| v.edges.filter_map { |i, e| e if i.in?(vertices_in_cluster_id) } } + .uniq + .count + + # Calculate cohesion value for the cluster + cohesion_c = sum.to_f / (cardinality_c * (cardinality_c - 1) / 2) + + # Store cohesion value in the cluster + cluster.attributes[:cohesion] = cohesion_c + + # Calculate cohesion contribution from this cluster + cohesion += cohesion_c + end + + # Store cohesion value in the graph + graph.attributes[:cohesion] = cohesion + + # Return the total cohesion + cohesion + end + end + end +end diff --git a/spec/mosaik/metrics/cohesion_spec.rb b/spec/mosaik/metrics/cohesion_spec.rb new file mode 100644 index 0000000..c3708b5 --- /dev/null +++ b/spec/mosaik/metrics/cohesion_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +RSpec.describe MOSAIK::Metrics::Cohesion do + subject(:metric) { build(:cohesion_metric, graph:) } + + # Cohesion value calculated by hand: + # + # Directed: + # Cluster A: |A| = 11: (2 + 1 + 0) / (11 * 10 / 2) = 0.05454545454545454 + # Cluster B: |B| = 8: (1 + 0) / (8 * 7 / 2) = 0.03571428571428571 + # Cluster C: |C| = 1: 0 + # + # Cohesion = 0.05454545454545454 + 0.03571428571428571 + 0 = 0.09025974025974026 + # + # Undirected: + # Cluster A: |A| = 11: (2 + 1 + 0) / (11 * 10 / 2) = 0.05454545454545454 + # Cluster B: |B| = 8: (1 + 0) / (8 * 7 / 2) = 0.03571428571428571 + # Cluster C: |C| = 1: 0 + # + # Cohesion = 0.05454545454545454 + 0.03571428571428571 + 0 = 0.09025974025974026 + # + + context "when the graph is directed" do + include_context "with a simple directed graph" + + it "sets the cohesion values for each cluster, and for the graph" do + metric.evaluate + + expect(graph.find_cluster("A").attributes[:cohesion]).to eq 0.05454545454545454 + expect(graph.find_cluster("B").attributes[:cohesion]).to eq 0.03571428571428571 + expect(graph.find_cluster("C").attributes[:cohesion]).to eq 0.0 + expect(graph.attributes[:cohesion]).to eq 0.09025974025974026 + end + + it "returns the total cohesion" do + expect(metric.evaluate).to eq 0.09025974025974026 + end + end + + context "when the graph is undirected" do + include_context "with a simple undirected graph" + + it "sets the cohesion values for each cluster, and for the graph" do + metric.evaluate + + expect(graph.find_cluster("A").attributes[:cohesion]).to eq 0.05454545454545454 + expect(graph.find_cluster("B").attributes[:cohesion]).to eq 0.03571428571428571 + expect(graph.find_cluster("C").attributes[:cohesion]).to eq 0.0 + expect(graph.attributes[:cohesion]).to eq 0.09025974025974026 + end + + it "returns the total cohesion" do + expect(metric.evaluate).to eq 0.09025974025974026 + end + end +end diff --git a/spec/support/contexts/directed_graph.rb b/spec/support/contexts/directed_graph.rb index ef3baed..4e3d15c 100644 --- a/spec/support/contexts/directed_graph.rb +++ b/spec/support/contexts/directed_graph.rb @@ -4,12 +4,12 @@ let(:graph) do graph = build(:graph, directed: true) - a = graph.add_vertex("A") - b = graph.add_vertex("B") - c = graph.add_vertex("C") - d = graph.add_vertex("D") - e = graph.add_vertex("E") - f = graph.add_vertex("F") + a = graph.add_vertex("A", methods: 3) + b = graph.add_vertex("B", methods: 2) + c = graph.add_vertex("C", methods: 3) + d = graph.add_vertex("D", methods: 6) + e = graph.add_vertex("E", methods: 5) + f = graph.add_vertex("F", methods: 1) graph.add_edge("A", "B", weight: 3.0) graph.add_edge("A", "C", weight: 1.0) diff --git a/spec/support/contexts/undirected_graph.rb b/spec/support/contexts/undirected_graph.rb index 1a1fb89..458b593 100644 --- a/spec/support/contexts/undirected_graph.rb +++ b/spec/support/contexts/undirected_graph.rb @@ -4,12 +4,12 @@ let(:graph) do graph = build(:graph, directed: false) - a = graph.add_vertex("A") - b = graph.add_vertex("B") - c = graph.add_vertex("C") - d = graph.add_vertex("D") - e = graph.add_vertex("E") - f = graph.add_vertex("F") + a = graph.add_vertex("A", methods: 3) + b = graph.add_vertex("B", methods: 2) + c = graph.add_vertex("C", methods: 3) + d = graph.add_vertex("D", methods: 6) + e = graph.add_vertex("E", methods: 5) + f = graph.add_vertex("F", methods: 1) graph.add_edge("A", "B", weight: 3.0) graph.add_edge("A", "C", weight: 1.0) diff --git a/spec/support/factories/metrics.rb b/spec/support/factories/metrics.rb index c0b9e6f..d3ab03f 100644 --- a/spec/support/factories/metrics.rb +++ b/spec/support/factories/metrics.rb @@ -10,5 +10,7 @@ factory :coupling_metric, parent: :metric, class: "MOSAIK::Metrics::Coupling" + factory :cohesion_metric, parent: :metric, class: "MOSAIK::Metrics::Cohesion" + factory :modularity_metric, parent: :metric, class: "MOSAIK::Metrics::Modularity" end