The metrics family

This family counts rather than judging what depends on what. A rule says where it looks, which number it is about, and what that number has to be:

rule := archunit.Metrics(nil).
	InFolder("internal/api/**").
	Count().
	LinesOfCode().
	ShouldBeBelow(400)

archunit.Metrics is the entry point, and it has no second spelling: metrics is what the family calls itself in every port. A rule of this family ends in Check like every other rule in the library.

Left without a predicate, the same chain is a measurement rather than a rule — Measure hands back one Measurement per subject, carrying the metric’s own name, the subject it was read off and the value:

measurements, err := archunit.Metrics(nil).InFolder("internal/api/**").Count().LinesOfCode().Measure(nil)

Scope

Four scope verbs, chainable and combined with AND. Three of them describe files and ForClassesMatching describes declared types, which is the one thing about this family’s scope worth remembering: an exclusion is about the same population as the verb it qualifies.

Verb Selects by Excluded by
WithName the filename ExceptWithName
InFolder the folder ExceptInFolder
InPath the whole identifier ExceptInPath
ForClassesMatching the declared type’s own name ExceptClassesMatching

Except is the plain form, read against the same part as the selector in front of it.

Which number

The group a scope is followed by decides what there is to ask for, so a rule says which kind of number it means before it says which number — count, method count reads as one phrase.

Count is the eight numbers this library takes of a project as it is written. Six are about a file and two about a class:

Verb Counts Per
LinesOfCode the lines that carry something file
Statements the statements file
Imports the imports file
Functions the functions declared at package level file
Classes the declared types file
Interfaces the declared types that are interfaces file
MethodCount the methods a type has class
FieldCount the fields a type declares class

Distance is Robert C. Martin’s package metrics and the coupling factor beside them, each about a component — a folder of the project, with the types it declares and the packages it depends on:

Verb Is
Abstractness how much of the component is interfaces
Instability how much of its coupling points outward
DistanceFromMainSequence how far it sits from the line where the two balance
NormalizedDistance the same distance on a nought-to-one scale
CouplingFactor how much of the coupling it could have with the other selected components it has

The eight LCOM formulas are calculated and tested and have no fluent verb yet, which the README’s what is not implemented yet states as the one place it is stated.

The six thresholds

There are exactly six threshold predicates, and each spells its own mood, which is why this family has no mood stage. Five hold every number a rule measured to a figure:

Predicate Holds a measured value to
ShouldBeBelow less than the figure
ShouldBeAbove more than the figure
ShouldBe exactly the figure
ShouldBeBelowOrEqual at most the figure
ShouldBeAboveOrEqual at least the figure

There is no seventh. Should equal, should be at most and every other synonym of one of the five is deliberately absent, because two spellings of one comparison mean every reader of a suite has to learn which of them the author picked. A broken threshold is a MetricsThresholdViolation, one per subject, carrying the subject, the number and the comparison it failed.

The sixth is ShouldSatisfy, for the comparisons no threshold expresses — a question the user writes, and the words to report it by:

rule := archunit.Metrics(nil).
	ForClassesMatching("*Service").
	Count().
	MethodCount().
	ShouldSatisfy(func(measurement archunit.Measurement, class archunit.MetricsClassInfo) bool {
		return measurement.Value <= 20 || class.Interface
	}, "have at most 20 methods unless it is an interface")

The function is a MetricsSatisfaction and it is handed both halves of what the library knows: the Measurement, so a predicate can exempt one subject or read the figure, and the MetricsClassInfo the number was read off — the zero value for a metric that is not about a class, since a file’s lines of code and a package’s abstractness have no class to be about. It reports a MetricsSatisfactionViolation, and the requirement you wrote is the sentence it says.

The two zones

Abstractness and Instability are the two axes of a plane, and two of its corners are places a package should not be. Both checks are on the Distance group itself, because each is about both axes at once:

rule := archunit.Metrics(nil).InFolder("internal/**").Distance().ShouldNotBeInZoneOfPain()
Check Forbids the corner where a package is
ShouldNotBeInZoneOfPain concrete and depended upon — rigid, and offering no interface to depend on instead
ShouldNotBeInZoneOfUselessness abstract and depended on by nothing — an abstraction nobody uses

Each corner is a quarter-circle rather than the point itself, so a package that is nearly all concrete and nearly all depended upon is reported too. Both exist in the negative mood alone: the positive would demand that every selected package be badly designed. A failure is a MetricsZoneViolation, carrying the component, the zone and the two coordinates that put it there.

A metric of your own

CustomMetric is the family’s escape hatch, and the reason it does not have to be exhaustive: a name, the words saying what the number means, and your own function for reading it off one class.

rule := archunit.Metrics(nil).
	ForClassesMatching("*Service").
	CustomMetric("public surface", "how many methods and fields a type exposes",
		func(class archunit.MetricsClassInfo) float64 {
			return float64(class.MethodCount + class.FieldCount)
		}).
	ShouldBeBelow(20)

The function is a MetricsClassMeasure and it is handed an archunit.MetricsClassInfo: the class’s name, its identifier, the file it was declared in, whether it is an interface, how many fields and methods it has, and — through MetricsFieldInfo and MetricsMethodInfo — which of its fields each of its methods reaches. Everything after the verb is unchanged, because a custom metric is a metric and not a second kind of rule.

The numbers as a page

Either group also closes without naming a metric at all, and then the chain is a report rather than a rule. ExportAsHTML measures every number of the group over the one scope and writes them as one self-contained page — the form to reach for when the numbers are for a person rather than for a threshold:

err := archunit.Metrics(nil).InFolder("internal/**").Count().ExportAsHTML("build/metrics.html", nil)

For measurements you have already taken and grouped your own way, archunit.NewMetricsExporter is the same page under your own title, timestamp and stylesheet:

exporter := archunit.NewMetricsExporter(&archunit.MetricsReportOptions{Title: "the numbers of this project"})
err := exporter.ExportAsHTML(archunit.MetricsReportData{"lines of code": measurements}, "build/metrics.html")

MetricsReportData is a map from a heading to the measurements under it, and MetricsReportOptions is the page’s own Title, Timestamp and Style. The timestamp is a field rather than a clock the library reads: a page that stamped itself would render different bytes on every run, so a report committed beside the code would show up in every diff. A scope that selected nothing to measure is ErrEmptyReport.

Next

Dependency-graph reports are the other half of reporting rather than judging: the project’s own files, drawn.