"Zero" Hölle und wie man da rauskommt

Nullwerte können, wenn sie gedankenlos verwendet werden, Ihr Leben unerträglich machen und Sie können möglicherweise nicht einmal verstehen, was genau diese Schmerzen verursacht. Lass es mich erklären.


Standardwerte


Wir alle haben eine Methode gesehen, die viele Argumente akzeptiert, von denen jedoch mehr als die Hälfte optional ist. Das Ergebnis ist ungefähr so:

public function insertDiscount(   string $name,   int $amountInCents,   bool $isActive = true,   string $description = '',   int $productIdConstraint = null,   DateTimeImmutable $startDateConstraint = null,   DateTimeImmutable $endDateConstraint = null,   int $paymentMethodConstraint = null ): int 

Im obigen Beispiel möchten wir einen Rabatt erstellen, der standardmäßig überall gilt. Er kann jedoch beim Erstellen inaktiv sein, nur für bestimmte Produkte gelten, nur zu bestimmten Zeiten handeln oder angewendet werden, wenn der Benutzer eine bestimmte Zahlungsmethode auswählt.

Wenn Sie einen Rabatt für eine bestimmte Zahlungsmethode erstellen möchten, müssen Sie die Methode wie folgt aufrufen:

 insertDiscount('Discount name', 100, true, '', null, null, null, 5); 

Dieser Code wird funktionieren, ist aber für die Person, die ihn liest, völlig unverständlich. Es wird extrem schwierig, es zu analysieren, sodass wir die Anwendung nicht einfach unterstützen können.

Nehmen wir dieses Beispiel Argument für Argument.

Was ist ein gültiger Rabatt?


Wir haben bereits herausgefunden, dass der unbegrenzte Rabatt überall gilt. Ein gültiger Rabatt enthält also alles außer den Einschränkungen, die wir später hinzufügen können. Das Argument isActive hat den Standardwert true. Daher kann die Methode wie folgt aufgerufen werden:

 insertDiscount('Discount name', 100); 

Wenn ich nur den Code lese, weiß ich nicht, dass der Rabatt sofort aktiviert wird. Um herauszufinden, müsste ich prüfen, ob die Methodensignatur Standardwerte hat.

Stellen Sie sich nun vor, Sie müssen 200 Codezeilen lesen. Möchten Sie wirklich jede Signatur der aufgerufenen Methode auf versteckte Informationen überprüfen? Ich würde den Code lieber einfach lesen, ohne nach irgendetwas suchen zu müssen.

Gleiches gilt für das für die Beschreibung verantwortliche Argument. Standardmäßig handelt es sich um eine leere Zeichenfolge. Dies kann an der Stelle, an der Sie eine Beschreibung erwarten, zu zahlreichen Problemen führen. Beispielsweise kann es auf dem Scheck gedruckt werden, aber da es leer ist, sieht der Benutzer einfach eine leere Zeile neben der Zeile mit dem Betrag. Das System sollte dies nicht zulassen.

Ich würde diese Methode folgendermaßen umschreiben:

 public function insertDiscount(  string $name,  string $description,  int $amountInCents,  bool $isActive ): int 

Ich habe die Einschränkungen vollständig entfernt, da wir beschlossen haben, sie später mithilfe separater Methoden hinzuzufügen. Da nun alle Parameter benötigt werden, können sie in beliebiger Reihenfolge angeordnet werden. Ich habe die Beschreibung direkt nach dem Namen eingefügt, da der Code besser lesbar ist, wenn sie in der Nähe sind.

 insertDiscount(  'Discount name',  'Discount description',  100,  Discount::STATUS_ACTIVE ); 

Ich habe auch Konstanten für den Rabattaktivitätsstatus verwendet. Sie müssen sich jetzt nicht die Signatur der Methode ansehen, um herauszufinden, was für dieses Argument richtig ist: Es wird offensichtlich, dass wir einen aktiven Rabatt erstellen. In Zukunft können wir diese Methode weiter verbessern (Spoiler: Verwendung von Wertobjekten).

Einschränkungen hinzufügen


Jetzt können Sie verschiedene Einschränkungen hinzufügen. Um Null, Null, Null Hölle zu vermeiden, werden wir separate Methoden erstellen.

 public function addProductConstraint(  Discount $discount,  int $productId ): Discount; public function addDateConstraint(  Discount $discount,  DateTimeImmutable $startDate,  DateTimeImmutable $endDate ): Discount; public function addPaymentMethodConstraint(  Discount $discount,  int $paymentMethod ): Discount; 

Wenn wir also einen neuen Rabatt mit bestimmten Einschränkungen erstellen möchten, gehen wir folgendermaßen vor:

 $discountId = insertDiscount(  'Discount name',  'Discount description',  100,  Discount::STATUS_ACTIVE ); addPaymentMethodConstraint(  $discountId,  PaymentMethod::CREDIT_CARD ); 

Vergleichen Sie dies nun mit dem ursprünglichen Aufruf. Sie werden sehen, wie viel bequemer das Lesen geworden ist.

Null in Objekteigenschaften


Das Auflösen von Nullen in Objekteigenschaften verursacht ebenfalls Probleme. Ich kann nicht sagen, wie oft ich solche Dinge sehe:

 $currencyCode = strtolower(  $record->currencyCode ); 

Buum! Msgstr "Kann nicht null an strtolower übergeben." Dies geschah, weil der Entwickler vergessen hat, dass currencyCode möglicherweise null ist. Da viele Entwickler die IDE immer noch nicht verwenden oder Warnungen unterdrücken, kann dies viele Jahre lang unbemerkt bleiben. Der Fehler wird in einigen ungelesenen Protokollen weiterhin auftreten, und Clients werden regelmäßig auftretende Probleme melden, die anscheinend nicht damit zusammenhängen, sodass sich niemand die Mühe macht, diese Codezeile zu lesen.

Wir können natürlich überall dort, wo wir auf currencyCode zugreifen, Null-Checks hinzufügen. Aber dann werden wir in einer anderen Art von Hölle enden:

 if ($record->currencyCode === null) {  throw new \RuntimeException('Currency code cannot be null'); } if ($record->amount === null) {  throw new \RuntimeException('Amount cannot be null'); } if ($record->amount > 0) {  throw new \RuntimeException('Amount must be a positive value'); } 

Aber wie Sie bereits verstanden haben, ist dies nicht die beste Lösung. Sie sollten jetzt nicht nur Ihre Methode überladen, sondern diesen Test überall wiederholen. Und vergessen Sie nicht, jedes Mal, wenn Sie eine weitere Null-Eigenschaft hinzufügen, eine weitere solche Prüfung durchzuführen! Zum Glück gibt es eine einfache Lösung: Wertobjekte.

Wertobjekte


Wertobjekte sind mächtige, aber einfache Dinge. Das Problem, das wir zu lösen versuchten, war, dass es notwendig ist, alle unsere Eigenschaften ständig zu validieren. Wir tun dies jedoch, weil wir nicht wissen, ob es möglich ist, den Eigenschaften des Objekts zu vertrauen, ob sie gültig sind. Was wäre, wenn wir könnten?

Um Werten zu vertrauen, benötigen sie zwei Attribute: Sie müssen validiert sein und sollten sich seit der Validierung nicht geändert haben. Schauen Sie sich diese Klasse an:

 final class Amount {  private $amountInCents;  private $currencyCode;  public function __construct(int $amountInCents, string $currencyCode): self  {    Assert::that($amountInCents)->greaterThan(0);    $this->amountInCents = $amountInCents;    $this->currencyCode = $currencyCode;  }  public function getAmountInCents(): int  {    return $this->amountInCents;  }  public function getCurrencyCode(): string  {    return $this->currencyCode;  } } 

Ich benutze das beberlei / assert-Paket. Es wird eine Ausnahme ausgelöst, wenn die Prüfung fehlschlägt. Dies entspricht der Ausnahme für null im Quellcode, es sei denn, wir haben die Prüfung in diesen Konstruktor verschoben.

Da wir Typdeklarationen verwenden, garantieren wir, dass auch der Typ korrekt ist. Wir können also nicht auf strtolower übergehen. Wenn Sie eine ältere Version von PHP verwenden, die keine Typdeklarationen unterstützt, können Sie mit diesem Paket Typen mit -> integer () und -> string () prüfen.

Nach dem Erstellen des Objekts können die Werte nicht geändert werden, da wir nur Getter, aber keine Setter haben. Dies nennt man Immunität. Durch das Hinzufügen von final kann diese Klasse nicht um Setter oder magische Methoden erweitert werden. Wenn in den Methodenparametern Amount $ amount angezeigt wird, können Sie zu 100% sicher sein, dass alle Eigenschaften überprüft wurden und das Objekt sicher verwendet werden kann. Wenn die Werte nicht gültig wären, könnten wir kein Objekt erstellen.

Jetzt können wir mit Hilfe von Wertobjekten unser Beispiel weiter verbessern:

 $discount = new Discount(  'Discount name',  'Discount description',  new Amount(100, 'CAD'),  Discount::STATUS_ACTIVE ) insertDiscount($discount); 

Bitte beachten Sie, dass wir zuerst einen Rabatt erstellen und den Betrag als Argument verwenden. Auf diese Weise wird sichergestellt, dass die insertDiscount-Methode ein gültiges Rabattobjekt erhält und dass der gesamte Codeblock einfacher zu verstehen ist.

Zero Horror Story


Betrachten wir einen interessanten Fall, in dem Null in einer Anwendung schädlich sein kann. Die Idee ist, die Sammlung aus der Datenbank zu extrahieren und zu filtern.

 $collection = $this->findBy(['key' => 'value']); $result = $this->filter($collection, $someFilterMethod); if ($result === null) {   $result = $collection; } 

Wenn das Ergebnis null ist, verwenden Sie die ursprüngliche Auflistung als Ergebnis? Dies ist problematisch, da die Filtermethode null zurückgibt, wenn keine geeigneten Werte gefunden wurden. Wenn also alles herausgefiltert ist, ignorieren wir den Filter und geben alle Werte zurück. Dies verstößt gegen die Logik.

Warum wird die ursprüngliche Sammlung als Ergebnis verwendet? Wir werden es nie erfahren. Ich vermute, dass der Entwickler eine gewisse Vermutung hatte, was Null in diesem Zusammenhang bedeutet, aber es stellte sich als falsch heraus.

Dies ist das Problem mit Nullwerten. In den meisten Fällen ist nicht klar, was sie bedeuten, und daher können wir nur raten, wie wir darauf reagieren sollen. Es ist sehr einfach, einen Fehler zu machen. Die Ausnahme dagegen ist sehr klar:

 try {  $result = $this->filter($collection, $someFilterMethod); } catch (CollectionCannotBeEmpty $e) {  // ... } 

Dieser Code ist eindeutig. Es ist unwahrscheinlich, dass ein Entwickler dies falsch interpretiert.

Lohnt sich die Mühe?


All dies scheint ein zusätzlicher Aufwand für das Schreiben von Code zu sein, der dasselbe bewirkt. Ja, das ist es. Gleichzeitig verbringen Sie jedoch viel weniger Zeit mit dem Lesen und Verstehen des Codes, sodass sich die Bemühungen angemessen lohnen. Jede zusätzliche Stunde, die ich mit dem Schreiben von Code verbringe, erspart mir Tage, in denen ich den Code nicht ändern oder neue Funktionen hinzufügen muss. Stellen Sie sich das als garantierte Investition mit hoher Rendite vor.

Das Ende meiner Null-Tirade ist also gekommen. Ich hoffe, dies hilft Ihnen dabei, verständlicheren und wartbareren Code zu schreiben.

Source: https://habr.com/ru/post/de478760/


All Articles