Files
werkcrappit/docs/features/08-cohesion-coupling.feature
miandmhoennig 8a4568e34d
werkator build succeeded after 02:15
werkator/publish build succeeded after 00:17
werkator/mutation build succeeded after 02:54
Cohesion and coupling judged, RFC 0002 step 3 (0.20.0) (#38)
## Related Links

- [RFC 0002](../rfcs/0002-cohesion-and-coupling.md), step 3; [PR#36](2026-10-04-PR%2336-cohesion-coupling-measured.md), [PR#37](2026-10-04-PR%2337-cohesion-coupling-city.md)

## The Problem

Since PR#36 cohesion and coupling are measured and since PR#37 shown, but nothing kept a class from falling further apart or depending on ever more classes.
The first measurement of hsadmin-ng also showed that LCOM4 as published counts every method without state as a group of its own: of 66 classes with LCOM4 of 2 or more, most owed it to a `toString` through a static helper, a bean factory or the methods of an anonymous class.

## The Scenarios

- `@MC-CK-010` a group of methods that uses no instance field at all does not count; a class with only such methods has no LCOM4.
- `@MC-CK-011` the gate judges LCOM4 and outgoing coupling against `maxLcom4` and `maxEfferentCoupling`; a violation names class and dimension; a class exactly at a limit passes.
- `@MC-CK-012` a dimension without a limit is reported, not judged; without any limit the gate is inactive, as before.

##### Verified by

- [DesignAnalysisTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/design/DesignAnalysisTest.java) `methodsWithoutStateDoNotCount`
- [GateTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/gate/GateTest.java) `cohesionAndCouplingHaveLimitsOfTheirOwn`, `aDimensionWithoutALimitIsOnlyReported`, `classLimitsNeedTheSectionClasses`
- [GateFunctionalTest](../../werkcrappit-gradle-plugin/src/functionalTest/java/de/javagil/werkcrappit/gradle/GateFunctionalTest.java) `cohesionAndCouplingLimitsReachTheGate`

## The Solution

- `Cohesion` keeps only the groups that use an instance field.
- `GateConfig` has the optional limits `maxLcom4` and `maxEfferentCoupling`; the gate judges the methods only with `maxMutationCrap` and the classes only with one of the new limits.
  The failure line counts "limit(s) exceeded", as it now covers classes too.
- The Gradle DSL has `maxLcom4` and `maxEfferentCoupling`; a limit below its minimum fails the check with a message.
- werkcrappit-pit applies the Gradle plugin 0.19.0, so its own city shows the metrics of step 2.
- Version 0.20.0.

## Follow-up PRs

- Ratchets for werkcrappit itself, once 0.20.0 is released: `maxLcom4 = 3`, `maxEfferentCoupling = 21`.
- hsadmin-ng, in its pilot branch: `maxLcom4 = 4`, `maxEfferentCoupling = 26`.

## Attachments

### LCOM4 and Ce, measured from the class files on 2026-10-04

| | LCOM4 max, as published → without stateless groups | Classes with LCOM4 of 2 or more | Ce max |
|---|---|---|---|
| hsadmin-ng (267 classes, generated code left out) | 12 → 4 | 66 → 19 | 26 (`HsAccountController`) |
| werkcrappit-pit (58 classes) | 3 → 3 | 9 → 4 | 21 (`MutationCrapListener`) |

The top of hsadmin-ng now shows real candidates: `HsOfficeContact` (4) with its independent e-mail, phone and postal address, `HsOfficeRelationSearchCriteria` (4) with four independent search fields.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #38
2026-10-04 12:25:17 +02:00

138 lines
7.3 KiB
Gherkin

# language: de
Funktionalität: Kohäsion und Kopplung je Klasse neben Mutation-CRAP
Als Entwickler möchte ich sehen, wie gut eine Klasse zusammenhält und wie stark sie von anderen abhängt,
ohne dass diese Werte die Mutation-CRAP-Metrik verändern.
@MC-CK-001
Szenario: Eine Klasse aus zwei unabhängigen Teilen hat LCOM4 2
… damit man sieht, welche Klasse sich entlang ihrer Teile aufteilen ließe.
Angenommen eine Klasse Konto mit den Feldern saldo und inhaber
Und den Methoden einzahlen und abheben, die nur saldo nutzen
Und der Methode umbenennen, die nur inhaber nutzt
Wenn der Report geschrieben wird
Dann hat Konto im Abschnitt "classes" lcom4 2
Und die Methodengruppen [abheben, einzahlen] und [umbenennen]
@MC-CK-002
Szenario: Konstruktoren, generierte und statische Methoden verbinden nichts
… damit LCOM4 nicht schon deshalb 1 ist, weil der Konstruktor jedes Feld setzt.
Angenommen die Klasse Konto aus @MC-CK-001
Und ein Konstruktor, der saldo und inhaber setzt
Und eine von Lombok generierte Methode toString, die beide Felder liest
Und eine statische Methode, die beide Felder einer Instanz liest
Wenn der Report geschrieben wird
Dann hat Konto weiterhin lcom4 2
@MC-CK-003
Szenario: Aufrufe, Getter und Lambdas verbinden Methoden
… damit LCOM4 die Zusammenarbeit der Methoden zählt, nicht nur gemeinsame Felder.
Angenommen eine Klasse Bestellung mit den Feldern positionen und status
Und einer Methode summe, die positionen nur über einen Getter getPositionen liest
Und einer Methode abschliessen, die status setzt und summe aufruft
Und einer Methode drucken, deren Lambda status liest
Wenn der Report geschrieben wird
Dann hat Bestellung lcom4 1
@MC-CK-004
Szenario: Verschachtelte Klassen zählen für ihre Top-Level-Klasse, eine Klasse ohne Instanzmethoden hat kein LCOM4
… damit jedes Gebäude der Code-City genau einen Wert hat und eine reine Konstantenklasse nicht als zusammenhanglos gilt.
Angenommen eine Klasse Lager mit einer verschachtelten Klasse Lager.Fach
Und Lager.Fach hat zwei Methoden, die nichts gemeinsam nutzen
Und eine Klasse Konstanten mit nur statischen Feldern und Methoden
Wenn der Report geschrieben wird
Dann gibt es im Abschnitt "classes" einen Eintrag Lager, keinen für Lager$Fach
Und Lager.Fach geht mit seinen Methoden in das LCOM4 von Lager ein
Und Konstanten hat lcom4 null, nicht 0
@MC-CK-005
Szenario: Ce und Ca zählen die anderen Top-Level-Klassen des Projekts
… damit man sieht, welche Klasse von vielen anderen abhängt und von welcher viele abhängen.
Angenommen BestellService nutzt Bestellung, Bestellung.Position, Kunde und java.util.List
Und Rechnung nutzt Bestellung
Wenn der Report geschrieben wird
Dann hat BestellService efferentCoupling 2: Bestellung und Kunde
Und Bestellung hat afferentCoupling 2: BestellService und Rechnung
Und java.util.List zählt nicht, Bibliotheken und das JDK gehören nicht zum Projekt
@MC-CK-006
Szenario: Der Report führt Kohäsion und Kopplung neben Mutation-CRAP, ohne diese zu verändern
… damit Gate, Code-City und Leser alle drei Dimensionen aus einer Quelle lesen.
Angenommen ein PIT-Lauf über ein Projekt, dessen Code-Roots auch Klassen ohne Mutanten enthalten
Wenn der Report geschrieben wird
Dann hat mutation-crap.json die schemaVersion 2 und ist gültig gegen das mitgelieferte Schema
Und der Abschnitt "classes" führt jede Top-Level-Klasse der Code-Roots mit lcom4, Methodengruppen, efferentCoupling und afferentCoupling, nach Namen sortiert
Und die Werte der Methoden, ihre Mutation-CRAP und ihre Zustände sind dieselben wie mit schemaVersion 1
Und mutation-crap.md zeigt die zehn Klassen mit dem höchsten LCOM4 und die zehn mit dem höchsten efferentCoupling
Und mutationCrapCheck liest den Report der Version 2
@MC-CK-007
Szenario: Die Zugriffsmethoden eines Records zählen wie generierte Methoden nicht
… damit ein Record nicht schon wegen seiner Komponenten als zusammenhanglos gilt, Lombok-Klassen gleich.
Angenommen ein Record Buchung mit den Komponenten betrag, waehrung und text
Und den eigenen Methoden istSoll, die betrag nutzt, und inEuro, die betrag und waehrung nutzt
Und ein Record Adresse mit drei Komponenten und ohne eigene Methoden
Wenn der Report geschrieben wird
Dann hat Buchung lcom4 1 mit der Methodengruppe [inEuro, istSoll]
Und Adresse hat lcom4 null
@MC-CK-008
Szenario: Die Code-City zeigt Kohäsion und Kopplung als eigene Metriken
… damit man im Viewer die Farbe von mutation_crap auf lcom4 oder efferent_coupling umstellen kann.
Angenommen ein Report mit dem Abschnitt "classes"
Wenn der CodeCharta-Export ausgeführt wird
Dann hat jedes Gebäude die Metriken lcom4, efferent_coupling und afferent_coupling
Und lcom4 ist "relative": ein Package zeigt den Median seiner Klassen
Und efferent_coupling und afferent_coupling sind "absolute": ein Package zeigt ihre Summe
Und ein Gebäude ohne LCOM4 hat keinen Wert lcom4, nie 0, und erscheint in der Farbe lcom4 grau
@MC-CK-009
Szenario: Die Report-Seite öffnet die Stadt auch mit Kohäsion oder Kopplung als Farbe
… damit man die drei Dimensionen mit einem Klick nacheinander ansehen kann.
Angenommen die Report-Seite wird über http(s) ausgeliefert
Wenn sie die Links zum Viewer bildet
Dann gibt es neben "Mutation-CRAP" die Links "Kohäsion" und "Kopplung"
Und "Kohäsion" färbt nach lcom4: grün bei 1, gelb bei 2, rot ab 3
Und "Kopplung" färbt nach efferent_coupling, mit den Farbgrenzen des Viewers
Und die Tabelle der Seite erklärt lcom4, efferent_coupling und afferent_coupling in einem Satz je Metrik
@MC-CK-010
Szenario: Methoden ohne jeden Zustand zählen nicht für LCOM4
… damit toString über einen statischen Helfer, Bean-Fabriken und Konstanten-Getter keine Aufteilung vortäuschen.
Angenommen die Klasse Konto aus @MC-CK-001
Und eine Methode toString, die nur einen statischen Helfer aufruft
Und eine Methode waehrung, die eine Konstante zurückgibt
Wenn der Report geschrieben wird
Dann hat Konto weiterhin lcom4 2 mit den Gruppen [abheben, einzahlen] und [umbenennen]
Und eine Klasse, deren Methoden alle keinen Zustand nutzen, hat lcom4 null
@MC-CK-011
Szenario: Das Gate prüft LCOM4 und die ausgehende Kopplung gegen eigene Grenzwerte
… damit eine Klasse nicht unbemerkt weiter zerfällt oder an immer mehr Klassen hängt.
Angenommen werkcrappit.maxLcom4 ist 3 und werkcrappit.maxEfferentCoupling ist 20
Und eine Klasse Lager hat lcom4 4, eine Klasse Service hat efferentCoupling 21
Wenn mutationCrapCheck läuft
Dann schlägt der Build fehl
Und die Meldung nennt "Lager has lcom4 4 > 3" und "Service has efferentCoupling 21 > 20"
Und eine Klasse genau auf dem Grenzwert besteht, wie bei maxMutationCrap
@MC-CK-012
Szenario: Ohne Grenzwert wird eine Dimension nur berichtet
… damit ein Projekt Kohäsion und Kopplung erst ansehen kann, bevor es sie begrenzt.
Angenommen werkcrappit.maxMutationCrap ist gesetzt, maxLcom4 und maxEfferentCoupling sind es nicht
Und eine Klasse hat lcom4 12 und efferentCoupling 40
Wenn mutationCrapCheck läuft
Dann urteilt das Gate nur über Mutation-CRAP
Und ohne irgendeinen Grenzwert ist das Gate inaktiv, wie bisher