Skip to content

Commit 251f522

Browse files
Merge pull request #189 from refactorfirst/improve-class-relationship-removal-calculation
Improve class relationship removal calculation
2 parents 8833c1b + 852ec59 commit 251f522

5 files changed

Lines changed: 267 additions & 83 deletions

File tree

codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.hjug.graphbuilder;
22

3-
import java.util.HashMap;
43
import java.util.List;
54
import java.util.Map;
65
import java.util.Set;
@@ -26,7 +25,6 @@ public class CodebaseGraphDTO {
2625

2726
private final List<ClassDisharmony> classDisharmonies;
2827
private final List<MethodDisharmony> methodDisharmonies;
29-
private final Map<String, Long> disharmonyCountByClass;
3028

3129
public CodebaseGraphDTO(
3230
Graph<String, DefaultWeightedEdge> classReferencesGraph,
@@ -41,15 +39,6 @@ public CodebaseGraphDTO(
4139
this.classToSourceFilePathMapping = classToSourceFilePathMapping;
4240
this.classDisharmonies = classDisharmonies;
4341
this.methodDisharmonies = methodDisharmonies;
44-
this.disharmonyCountByClass = buildDisharmonyIndex(classDisharmonies, methodDisharmonies);
45-
}
46-
47-
private static Map<String, Long> buildDisharmonyIndex(
48-
List<ClassDisharmony> classDisharmonies, List<MethodDisharmony> methodDisharmonies) {
49-
Map<String, Long> counts = new HashMap<>();
50-
classDisharmonies.forEach(d -> counts.merge(d.getMetrics().getClassName(), 1L, Long::sum));
51-
methodDisharmonies.forEach(m -> counts.merge(m.getClassName(), 1L, Long::sum));
52-
return counts;
5342
}
5443

5544
public List<ClassDisharmony> getClassDisharmoniesOfType(String disharmonyType) {
@@ -63,8 +52,4 @@ public List<MethodDisharmony> getMethodDisharmoniesOfType(String disharmonyType)
6352
.filter(d -> disharmonyType.equals(d.getDisharmonyType()))
6453
.collect(Collectors.toList());
6554
}
66-
67-
public long getClassDisharmonyCountForClass(String classFqn) {
68-
return disharmonyCountByClass.getOrDefault(classFqn, 0L);
69-
}
7055
}

cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.hjug.metrics.DisharmonyRanker;
2828
import org.hjug.metrics.rules.CBORule;
2929
import org.jgrapht.Graph;
30+
import org.jgrapht.graph.AsSubgraph;
3031
import org.jgrapht.graph.DefaultWeightedEdge;
3132

3233
@Slf4j
@@ -330,9 +331,15 @@ public List<RankedDisharmony> calculateRelationshipCostBenefitValues(
330331
Graph<String, DefaultWeightedEdge> classGraph,
331332
Map<DefaultWeightedEdge, Integer> edgeToRemoveCycleCounts,
332333
CodebaseGraphDTO dto,
333-
Set<String> vertexesToRemove) {
334+
Set<String> vertexesToRemove,
335+
Map<String, AsSubgraph<String, DefaultWeightedEdge>> packageCycles,
336+
List<RankedDisharmony> packageRelationshipDisharmonies) {
334337
List<RankedDisharmony> edgesThatNeedToBeRemoved = new ArrayList<>();
335338

339+
Set<DefaultWeightedEdge> packageEdgesToRemove = packageRelationshipDisharmonies.stream()
340+
.map(RankedDisharmony::getEdge)
341+
.collect(Collectors.toSet());
342+
336343
for (DefaultWeightedEdge edge : classGraph.edgeSet()) {
337344
// shouldn't have to check for null edges & counts :-(
338345
if (null == edge || null == edgeToRemoveCycleCounts.get(edge)) continue;
@@ -350,8 +357,8 @@ public List<RankedDisharmony> calculateRelationshipCostBenefitValues(
350357
(int) classGraph.getEdgeWeight(edge),
351358
sourceNodeShouldBeRemoved,
352359
targetNodeShouldBeRemoved,
353-
dto.getClassDisharmonyCountForClass(edgeSource),
354-
dto.getClassDisharmonyCountForClass(edgeTarget));
360+
getPackageCycleCount(edgeSource, edgeTarget, dto, packageCycles),
361+
packageRelationshipShouldBeRemoved(edgeSource, edgeTarget, dto, packageEdgesToRemove));
355362

356363
edgesThatNeedToBeRemoved.add(edgeThatNeedsToBeRemoved);
357364
}
@@ -376,18 +383,69 @@ public List<RankedDisharmony> calculateRelationshipCostBenefitValues(
376383
return edgesThatNeedToBeRemoved;
377384
}
378385

386+
/**
387+
* Counts how many package cycles contain the package-level relationship corresponding to the given class (or
388+
* package) edge - i.e. cycles where the edge between the source's and target's packages is itself part of the
389+
* cycle, not merely cycles that happen to contain one of the endpoints.
390+
*/
391+
private static int getPackageCycleCount(
392+
String edgeSource,
393+
String edgeTarget,
394+
CodebaseGraphDTO dto,
395+
Map<String, AsSubgraph<String, DefaultWeightedEdge>> packageCycles) {
396+
String sourcePackage = toPackageName(edgeSource, dto);
397+
String targetPackage = toPackageName(edgeTarget, dto);
398+
399+
int packageCycleCount = 0;
400+
for (AsSubgraph<String, DefaultWeightedEdge> packageCycle : packageCycles.values()) {
401+
if (packageCycle.containsEdge(sourcePackage, targetPackage)) {
402+
packageCycleCount++;
403+
}
404+
}
405+
return packageCycleCount;
406+
}
407+
408+
/**
409+
* Determines whether the package-level relationship corresponding to the given class (or package) edge is
410+
* itself one of the package edges selected for removal, based on membership in the already-computed package
411+
* relationship disharmonies.
412+
*/
413+
private static boolean packageRelationshipShouldBeRemoved(
414+
String edgeSource, String edgeTarget, CodebaseGraphDTO dto, Set<DefaultWeightedEdge> packageEdgesToRemove) {
415+
String sourcePackage = toPackageName(edgeSource, dto);
416+
String targetPackage = toPackageName(edgeTarget, dto);
417+
DefaultWeightedEdge packageEdge = dto.getPackageReferencesGraph().getEdge(sourcePackage, targetPackage);
418+
return packageEdge != null && packageEdgesToRemove.contains(packageEdge);
419+
}
420+
421+
/**
422+
* The vertex may already be a package name (when classGraph is actually a package graph) or a fully-qualified
423+
* class name, in which case the containing package is derived from it.
424+
*/
425+
private static String toPackageName(String vertex, CodebaseGraphDTO dto) {
426+
if (dto.getPackageReferencesGraph().containsVertex(vertex)) {
427+
return vertex;
428+
} else if (vertex.contains(".")) {
429+
return vertex.substring(0, vertex.lastIndexOf('.'));
430+
} else {
431+
return "";
432+
}
433+
}
434+
379435
static void sortEdgesThatNeedToBeRemoved(List<RankedDisharmony> rankedDisharmonies) {
380436
// Sort by impact value
381437
// Order by cycle count reversed (highest count bubbles to the top)
382438
rankedDisharmonies.sort(Comparator.comparingInt(RankedDisharmony::getCycleCount)
383439
.reversed()
384440
// then by weight, with lowest weight edges bubbling to the top
385441
.thenComparingInt(RankedDisharmony::getEffortRank)
386-
// then by disharmony count
387-
.thenComparingInt(RankedDisharmony::getEdgeSourceDisharmonyCount)
388-
.thenComparingInt(RankedDisharmony::getEdgeTargetDisharmonyCount)
389-
// then if the source node is in the list of nodes to be removed
442+
// then by whether the underlying package relationship should also be removed, true before false
390443
// multiplying by -1 reverses the sort order (reverse doesn't work in chained comparators)
444+
.thenComparingInt(
445+
rankedDisharmony -> -1 * (rankedDisharmony.isPackageRelationshipShouldBeRemoved() ? 1 : 0))
446+
// then by package cycle count, with classes in more package cycles bubbling to the top
447+
.thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getPackageCycleCount())
448+
// then if the source node is in the list of nodes to be removed
391449
.thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getSourceNodeShouldBeRemoved())
392450
// then if the target node is in the list of nodes to be removed
393451
.thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getTargetNodeShouldBeRemoved()));

cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedDisharmony.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public class RankedDisharmony {
4444
private int sourceNodeShouldBeRemoved;
4545
private int targetNodeShouldBeRemoved;
4646
private String edgeTargetClass;
47-
private Integer edgeSourceDisharmonyCount;
48-
private Integer edgeTargetDisharmonyCount;
47+
private Integer packageCycleCount;
48+
private boolean packageRelationshipShouldBeRemoved;
4949

5050
public RankedDisharmony(GodClass godClass, ScmLogInfo scmLogInfo) {
5151
path = scmLogInfo.getPath();
@@ -109,14 +109,14 @@ public RankedDisharmony(
109109
int weight,
110110
boolean sourceNodeShouldBeRemoved,
111111
boolean targetNodeShouldBeRemoved,
112-
long sourceDisharmonyCount,
113-
long targetDisharmonyCount) {
112+
int packageCycleCount,
113+
boolean packageRelationshipShouldBeRemoved) {
114114

115115
className = edgeSource;
116116
this.edge = edge;
117117
this.cycleCount = cycleCount;
118-
edgeSourceDisharmonyCount = Math.toIntExact(sourceDisharmonyCount);
119-
edgeTargetDisharmonyCount = Math.toIntExact(targetDisharmonyCount);
118+
this.packageCycleCount = packageCycleCount;
119+
this.packageRelationshipShouldBeRemoved = packageRelationshipShouldBeRemoved;
120120
effortRank = weight;
121121
this.sourceNodeShouldBeRemoved = sourceNodeShouldBeRemoved ? 1 : 0;
122122
this.targetNodeShouldBeRemoved = targetNodeShouldBeRemoved ? 1 : 0;

0 commit comments

Comments
 (0)