Warum Sie Utility-Klassen in Ihren Projekten vermeiden sollten

vg

In der Softwareentwicklung stehen Entwickler häufig vor der Herausforderung, wieder verwendbare Lösungen für häufig auftretende Probleme zu finden. Eine weit verbreitete Methode, diese zu lösen, sind sogenannte Utility-Klassen, die eine Sammlung statischer Methoden enthalten. Diese Klassen bieten einfache Funktionen, wie etwa die Validierung von Eingabewerten oder mathematische Berechnungen. Doch obwohl sie auf den ersten Blick praktisch erscheinen, bergen Utility-Klassen mehrere Probleme, die die Wartbarkeit und Erweiterbarkeit eines Projekts beeinträchtigen können, insbesondere in größeren und komplexeren Anwendungen, die Best Practices wie SOLID-Prinzipien oder Domain-Driven Design (DDD) folgen. In diesem Artikel werden die Schwächen von Utility-Klassen beleuchtet und gezeigt, wie sie durch elegantere, domänenorientierte Ansätze ersetzt werden können.

Was ist die Utility-Klasse?

In der Softwareentwicklung ist es üblich, auf Utility-Klassen zu stoßen, die mit statischen Methoden gefüllt sind, um alltägliche Probleme zu lösen, wie z. B. CPF-Validierung (brasilianische Steueridentifikationsnummern), String-Manipulation oder mathematische Berechnungen. Obwohl sie praktisch erscheinen mögen, können diese Klassen auf lange Sicht problematisch werden, insbesondere bei Projekten, die darauf abzielen, Prinzipien wie SOLID und Domain-Driven Design (DDD) zu folgen. In diesem Artikel untersuchen wir, warum Utility-Klassen vermieden werden sollten und wie sie durch Lösungen ersetzt werden können, die auf Best Design Practices abgestimmt sind.

Das Problem mit Utility-Klassen

Hilfsklassen sind im Wesentlichen eine Sammlung statischer Methoden, die in einer einzigen Klasse gruppiert sind. Diese Methoden haben keinen Zustand und stellen kein sinnvolles Konzept in der Domäne dar. Gängige Beispiele sind , oder sogar . Obwohl diese Kurse hilfreich erscheinen, werfen sie mehrere Probleme auf:StringUtilsMathUtilsCPFUtils

  1. Verstoß gegen das Single Responsibility Principle (SRP)
    Utility-Klassen häufen oft mehrere, nicht miteinander verknüpfte Verantwortlichkeiten an. In einer Klasse finden Sie z. B. Methoden für die CPF-Validierung, Formatierung und sogar die Generierung von CPF-Nummern. Dies führt zu einer geringen Kohäsion und einem erhöhten Wartungsaufwand.CPFUtils
  2. Break Encapsulation
    Utility-Klassen kapseln keine Verhaltensweisen innerhalb des Domänenkontexts. Stattdessen bieten sie generische Funktionen, die von der Domäne losgelöst sind. Dies verstößt gegen DDD-Prinzipien, bei denen jeder Codeabschnitt ein sinnvolles Konzept aus der Domäne der Anwendung widerspiegeln sollte.
  3. Erschweren von Komponententests
    Statische Methoden in Hilfsprogrammklassen sind schwer zu simulieren und als Abhängigkeiten zu injizieren. Dies erschwert das Testen und kann zu unnötigen Abhängigkeiten in Testumgebungen führen.
  4. Low Contextual Reusability
    Utility-Klassen sind weder erweiterbar noch polymorph. Wenn Sie beispielsweise CPF in verschiedenen Kontexten unterschiedlich handhaben möchten, müssen Sie die Logik neu implementieren oder zusätzliche statische Methoden erstellen, was die Redundanz und das Risiko von Inkonsistenzen erhöht.

Ein praktisches Beispiel: CPF-Validierung

Zum Vergleich: CPF (Cadastro de Pessoas Físicas) ist die brasilianische Steueridentifikationsnummer, die aus 11 Ziffern besteht, die zur Identifizierung bei Finanz- und Rechtsgeschäften verwendet werden.

Hier sehen Sie ein Beispiel für die CPF-Validierung mithilfe einer Hilfsklasse:

public class CPFUtils {

public static boolean isValid(String cpf) {
// Logic to validate CPF
return cpf != null && cpf.matches("\\d{11}") && validateDigits(cpf);
}

public static String format(String cpf) {
// Formats CPF as xxx.xxx.xxx-xx
return cpf.replaceAll("(\\d{3})(\\d{3})(\\d{3})(\\d{2})", "$1.$2.$3-$4");
}

private static boolean validateDigits(String cpf) {
// Logic to validate CPF check digits
return true; // Simplified for demonstration
}
}

Dieser Kurs funktioniert möglicherweise, weist jedoch die folgenden Probleme auf:

  • Kumulierte Verantwortlichkeiten: Validierung, Formatierung und Ziffernüberprüfung werden in einer Klasse gemischt.
  • Kein Domänenkonzept: Der CPF wird nur als Zeichenfolge und nicht als Objekt mit Bedeutung in der Domäne behandelt.
  • Einschränkungen beim Testen: Das Simulieren oder Ändern des Verhaltens statischer Methoden in Tests ist umständlich.

Ein domänenorientierter Ansatz als Alternative zu Utility-Klassen

Anstatt eine Hilfsklasse zu verwenden, können wir ein Value-Objekt erstellen , das CPF als Teil der Domäne darstellt.

import java.util.Objects;

public class CPF {

private final String value;

public CPF(String value) {
if (!isValid(value)) {
throw new IllegalArgumentException("Invalid CPF: " + value);
}
this.value = format(value);
}

private boolean isValid(String cpf) {
return cpf != null && cpf.matches("\\d{11}") && validateDigits(cpf);
}

private boolean validateDigits(String cpf) {
// Logic to validate CPF check digits
return true; // Simplified for demonstration
}

private String format(String cpf) {
return cpf.replaceAll("(\\d{3})(\\d{3})(\\d{3})(\\d{2})", "$1.$2.$3-$4");
}

public String getValue() {
return value;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CPF cpf = (CPF) o;
return Objects.equals(value, cpf.value);
}

@Override
public int hashCode() {
return Objects.hash(value);
}

@Override
public String toString() {
return value;
}
}

Verwenden der Klasse in einem DomänenmodellCPF

Integrieren Sie diese Klasse in ein Domänenmodell, z. B. eine Klasse:CPFPerson

public class Person {

private String name;
private CPF cpf;

public Person(String name, String cpf) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank");
}
this.name = name;
this.cpf = new CPF(cpf);
}

public String getName() {
return name;
}

public CPF getCpf() {
return cpf;
}

@Override
public String toString() {
return "Person{name='" + name + "', cpf=" + cpf + '}';
}
}

Anwendungsbeispiel

public class Main {

public static void main(String[] args) {
try {
Person person = new Person("John Doe", "12345678909");
// if person was instantiate correctly, we know for sure that cpf is valid!

System.out.println(person);
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
}
}
}

Vorteile dieses Ansatzes

  1. Kapselung und Gültigkeitsgarantie: Das Objekt garantiert die Gültigkeit zum Zeitpunkt der Erstellung.CPF
  2. Sinnvolle Domänendarstellung: Die Klasse spiegelt Domänenkonzepte klarer wider.Person
  3. Sauberere Geschäftslogik: Die Validierungslogik ist in der Klasse gekapselt, sodass sich die Klasse auf ihre Rolle konzentrieren kann.CPFPerson
  4. Einfache Wartung: Das Hinzufügen neuer Verhaltensweisen (z. B. CPF-Maskierung) kann innerhalb der Klasse erfolgen, ohne andere Teile des Codes zu beeinträchtigen.CPF

Fazit

Obwohl Utility-Klassen wie eine schnelle Lösung für wiederkehrende Probleme erscheinen mögen, verstoßen sie häufig gegen grundlegende Softwareentwurfsprinzipien wie SRP und stehen im Widerspruch zu den empfohlenen DDD-Praktiken. Das Ersetzen durch Objekte, die Domänenkonzepte darstellen, führt zu saubererem, besser testbarem Code und einer besseren langfristigen Projektausrichtung.

Wenn Sie das nächste Mal erwägen, ein zu erstellen, fragen Sie sich: Könnte dies stattdessen ein Domänenobjekt sein? XYZUtils

Weitere Themen: Jeder irrt sich mit dem S in SOLID

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)