Lösung: 1

PHPUnit 10.2.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.14
Configuration: phpunit.xml

..                                                                  2 / 2 (100%)

Time: 00:00.001, Memory: 24.41 MB

Measurements
 ✔ Centimeters can be converted to int
 ✔ Meters can be converted to float

OK (2 tests, 2 assertions)
Ausgabe von phpunit --testdox
<?php declare(strict_types=1);

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

#[CoversClass(Meter::class)]
#[CoversClass(Centimeter::class)]
class MeasurementsTest extends TestCase
{
    #[Test]
    public function centimeters_can_be_converted_to_int(): void
    {
        $centimeters = 23;
        $measure = new Centimeter($centimeters);
        $this->assertEquals($centimeters, $measure->asInt());
    }

    #[Test]
    public function meters_can_be_converted_to_float(): void
    {
        $meters = 1.5;
        $measure = new Meter($meters);
        $this->assertEquals($meters, $measure->asFloat());
    }
}
MeasurementsTest.php

Die wichtigste Erkenntnis beim Lösen dieser Teilaufgabe ist, einzelne Klassen für Meter und Centimeter zu erstellen. Ein Zentimeter ist schließlich etwas ganz anderes als ein Meter. Du stimmst nicht unbedingt zu?

Würden wir einen Meter und ein Kilogramm gemeinsam in einer Klasse repräsentieren? Vermutlich nicht, das wäre wie Äpfel und Pilze in einer Schüssel.

Wie wäre es bei einem Meter und einem Yard? Obwohl beide Längenmaße sind, sind das metrische System und das imperiale System grundverschieden. Wir tun gut daran, das nicht in einer Klasse zu mischen.

Aber Zentimeter und Metern sind beide Bestandteil des metrischen Systems, können wir das also in eine einzige Klasse packen? Es ist relativ wahrscheinlich, dass wir unsere Lösung bald um Millimeter oder Kilometer erweitern müssen, oder je nach Anwendungsfall auch um Nanometer oder Astronomische Einheiten - und vermutlich jeweils allen Zwischenschritten. Wir sind uns wohl einig, dass das eine einzelne Klasse zu stark aufblähen würde. Das ist ein guter Indikator dafür, die beiden Einheiten schon jetzt in zwei Klassen aufzutrennen.

Das hat übrigens nichts mit spekulativer Generalisierung zu tun. Wir setzen lediglich konsequent das Single Responsibility-Prinzip um. Eine Klasse repräsentiert Zentimeter, eine Meter. Jede Klasse ist für eine Sache zuständig.

Keine Angst vor zu kleinen Klassen. Zusammenfassen ist leichter als auftrennen.

Falls wir in Zukunft feststellen, dass die beiden Klassen duplizierten Code enthalten, können wir sie jederzeit zusammenfassen. Das ist einfacher als eine komplexe Klasse aufzuteilen. Zu kleine Klassen sind immer weniger problematisch als zu große Klassen.

<?php declare(strict_types=1);

class Centimeter
{
    private readonly int $centimeters;

    public function __construct(int $centimeters)
    {
        $this->centimeters = $centimeters;
    }

    public function asInt(): int
    {
        return $this->centimeters;
    }
}
Centimeter.php

Diese beiden Klassen hier sind wirklich noch sehr klein und tun nicht wirklich etwas. Aber wir sind ja noch ganz am Anfang.

<?php declare(strict_types=1);

class Meter
{
    private readonly float $meters;

    public function __construct(float $meters)
    {
        $this->meters = $meters;
    }

    public function asFloat(): float
    {
        return $this->meters;
    }
}
Meter.php

Die gezeigten Tests sehen aus wie Konstruktor-Tests beziehungsweise Tests von Gettern. Es gibt Menschen, die sagen, solchen Code braucht man nicht zu testen, weil er trivial ist und ohnehin meist durch Autocomplete von der IDE oder eine AI erzeugt wird. Ich bin nicht dieser Meinung. Nach meiner Erfahrung ist gerade vermeintlich trivialer Code besonders häufig fehlerhaft, weil da nie jemand genau hinsieht.

Was hat uns dazu motiviert, diese Tests zu schreiben? Wir wollten nicht den Konstruktor oder einen Getter zu testen, sondern wir wollten skalare Werte durch Wertobjekte repräsentieren und brauchten dabei einen Weg, für die Weiterverarbeitung wieder an die skalaren Werte heranzukommen. In diesem Sinne sind die Methoden asInt() und asFloat() ein Ersatz für eine Typumwandlung wie (float) $meterValueObject, der so in PHP nicht möglich ist. Das ist, so finde ich, durchaus etwas anderes als ein Getter.

Das ist übrigens auch der Grund, warum ich asString() der eingebauten Interzeptorfunktion __toString() vorziehe. Ich möchte Objekte eben nicht immer nur in Strings umwandeln, sondern, gerade für Berechnungen, häufig eben auch in andere Datentypen. In der Tat war ja __toString() in PHP ursprünglich dafür gedacht, eine lesbare Repräsentation eines Objektes beispielsweise in Exception-Messages oder Protokolldateien zu erzeugen.