Skip to content
This repository has been archived by the owner on May 4, 2024. It is now read-only.

Commit

Permalink
Add cohesion metric
Browse files Browse the repository at this point in the history
  • Loading branch information
floriandejonckheere committed Apr 14, 2024
1 parent e8ec436 commit 164bf64
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 12 deletions.
60 changes: 60 additions & 0 deletions lib/mosaik/metrics/cohesion.rb
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions spec/mosaik/metrics/cohesion_spec.rb
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions spec/support/contexts/directed_graph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions spec/support/contexts/undirected_graph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions spec/support/factories/metrics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 164bf64

Please sign in to comment.