Aikido

Warum Klassen dem Single Responsibility Principle folgen sollten

Lesbarkeit

Regel
Klassen sollten eine einzige Verantwortung haben.
Klassen, die mehrere Belange behandeln, verletzen
das Single Responsibility Principle.

Unterstützte Sprachen: JS, TS, PY, JAVA, C/C++,
C#, Swift/Objective C, Ruby. PHP, Kotlin, 
Scala, Rust, Haskell, Groovy, Dart. Julia,
Elixit, Klojure, OCaml, Delphi

Einleitung

Klassen, die zu viel tun, werden zu Engpässen. Eine Klasse, die Authentifizierung, E-Mails und Validierung handhabt, erfordert Änderungen, sobald sich ein Aspekt weiterentwickelt, was das Risiko von Brüchen in nicht verwandter Funktionalität birgt. Tests erfordern das Mocken der gesamten Klasse, selbst wenn nur ein Aspekt getestet wird. Das Single Responsibility Principle besagt, dass eine Klasse nur einen Grund zur Änderung haben sollte.

Warum es wichtig ist

Code-Wartbarkeit: Klassen mit mehreren Verantwortlichkeiten ändern sich häufiger, da die Entwicklung eines Anliegens die gesamte Klasse betrifft.

Testkomplexität: Das Testen von Klassen mit mehreren Verantwortlichkeiten erfordert das Mocking aller Abhängigkeiten, selbst wenn nur eine Funktion getestet wird.

Wiederverwendbarkeit: Man kann eine Verantwortung nicht extrahieren, ohne alle Abhängigkeiten mitzubringen. Entwickelnde duplizieren Code, anstatt Klassen mit mehreren Verantwortlichkeiten zu entwirren.

Teamkoordination: Mehrere Entwickelnde, die an derselben Klasse für verschiedene Features arbeiten, erzeugen häufig Merge-Konflikte. Klassen mit einfacher Verantwortlichkeit ermöglichen parallele Entwicklung ohne Konflikte.

Code-Beispiele

❌ Nicht konform:

class UserManager {
    async createUser(userData) {
        const user = await db.users.insert(userData);
        await this.sendWelcomeEmail(user.email);
        await this.logEvent('user_created', user.id);
        await cache.set(`user:${user.id}`, user);
        return user;
    }

    async sendWelcomeEmail(email) {
        const template = this.loadEmailTemplate('welcome');
        await emailService.send(email, template);
    }

    async logEvent(event, userId) {
        await analytics.track(event, { userId, timestamp: Date.now() });
    }
}

Warum es falsch ist: Diese Klasse verarbeitet Datenbankoperationen, E-Mail-Versand, Protokollierung und Caching. Änderungen an E-Mail-Vorlagen, Protokollformaten oder der Cache-Strategie erfordern alle eine Modifikation dieser Klasse. Das Testen der Benutzererstellung bedeutet das Mocken von E-Mail-Diensten, Analysen und Cache, was Tests langsam und anfällig macht.

✅ Konform:

class UserRepository {
    async create(userData) {
        return await db.users.insert(userData);
    }
}

class EmailNotificationService {
    async sendWelcomeEmail(email) {
        const template = await this.templateLoader.load('welcome');
        return await this.emailSender.send(email, template);
    }
}

class UserEventLogger {
    async logCreation(userId) {
        return await this.analytics.track('user_created', {
            userId,
            timestamp: Date.now()
        });
    }
}

class UserService {
    constructor(repository, emailService, eventLogger, cache) {
        this.repository = repository;
        this.emailService = emailService;
        this.eventLogger = eventLogger;
        this.cache = cache;
    }

    async createUser(userData) {
        const user = await this.repository.create(userData);
        await Promise.all([
            this.emailService.sendWelcomeEmail(user.email),
            this.eventLogger.logCreation(user.id),
            this.cache.set(`user:${user.id}`, user)
        ]);
        return user;
    }
}

Warum dies wichtig ist: Jede Klasse hat eine klare Verantwortung: Datenpersistenz, E-Mail-Versand, Event-Logging oder Orchestrierung. Änderungen an E-Mail-Vorlagen betreffen nur EmailNotificationService. Beim Testen der Benutzererstellung können einfache Stubs für Abhängigkeiten verwendet werden. Klassen können unabhängig voneinander über verschiedene Funktionen hinweg wiederverwendet werden.

Fazit

Das Single Responsibility Principle (Prinzip der einzigen Verantwortung) geht nicht darum, Klassen so klein wie möglich zu machen, sondern sicherzustellen, dass jede Klasse einen klaren Grund zur Änderung hat. Wenn eine Klasse beginnt, mehrere Belange zu behandeln, refaktorieren Sie, indem Sie jede Verantwortung in eine eigene Klasse mit einer fokussierten Schnittstelle extrahieren. Dies erleichtert das Testen, Warten und Weiterentwickeln von Code, ohne kaskadierende Änderungen über nicht verwandte Funktionalitäten hinweg zu verursachen.

FAQs

Haben Sie Fragen?

Wie identifiziere ich, wann eine Klasse zu viele Verantwortlichkeiten hat?

Suchen Sie nach Klassen mit mehreren Änderungsgründen. Wenn die Änderung der E-Mail-Logik, des Logging-Formats und des Datenbankschemas alle die gleiche Klasse erfordern, hat diese zu viele Verantwortlichkeiten. Überprüfen Sie Methodennamen: Wenn sie in derselben Klasse nicht verwandte Verben wie `sendEmail()`, `logEvent()` und `validateData()` abdecken, ist das ein Warnsignal. Klassen mit mehr als 300-400 Zeilen weisen oft auf mehrere Verantwortlichkeiten hin, obwohl die Größe allein nicht ausschlaggebend ist.

Erzeugt das Aufteilen von Klassen nicht mehr Dateien und Komplexität?

Mehr Dateien bedeuten nicht mehr Komplexität. Zehn fokussierte Klassen von je 50 Zeilen sind leichter zu verstehen als eine 500-Zeilen-Klasse, die alles abdeckt. Der Schlüssel ist, dass jede Klasse einfach ist und einen klaren Zweck hat. Die Navigation in modernen IDEs macht die Dateianzahl irrelevant. Die Komplexitätsreduzierung ergibt sich aus der Möglichkeit, jede Klasse unabhängig zu betrachten, ohne irrelevantes berücksichtigen zu müssen.

Was ist mit Klassen, die naturgemäß mehrere Operationen koordinieren müssen?

Koordination ist selbst eine Verantwortung. Eine UserService-Klasse kann Aufrufe an UserRepository, EmailService und EventLogger orchestrieren, ohne diese Belange selbst zu implementieren. Dies ist das Orchestrator- oder Fassadenmuster. Der Unterschied besteht darin, dass der Orchestrator an spezialisierte Klassen delegiert, anstatt mehrere Belange direkt zu implementieren. Es ist dünner Glue Code, keine Geschäftslogik.

Wie lässt sich dieses Prinzip auf Utility-Klassen mit statischen Methoden anwenden?

Utility-Klassen sind besonders anfällig für die Verletzung des Single-Responsibility-Prinzips, da es einfach ist, immer wieder nicht verwandte statische Methoden hinzuzufügen. Eine StringUtils-Klasse könnte mit Formatierungshelfern beginnen, aber um Validierung, Parsing, Verschlüsselung und Kodierung erweitert werden. Teilen Sie diese in fokussierte Utility-Klassen wie StringFormatter, StringValidator und StringEncoder auf. Jede verfügt über eine kohärente Menge verwandter Operationen.

Wie refaktorisere ich bestehende Klassen, die dieses Prinzip verletzen?

Beginnen Sie damit, unterschiedliche Verantwortlichkeiten innerhalb der Klasse zu identifizieren. Extrahieren Sie zuerst die einfachste in eine neue Klasse, aktualisieren Sie Tests und verifizieren Sie, dass alles funktioniert. Wiederholen Sie dies iterativ, anstatt einen großen Refactor zu versuchen. Verwenden Sie das Strangler Fig Pattern: Erstellen Sie neue Single-Responsibility-Klassen und verschieben Sie den Code schrittweise aus der alten Klasse. Sobald die alte Klasse leer oder minimal ist, dekommissionieren Sie sie. Jeder Schritt sollte ein funktionierendes, testbares Inkrement sein.

Bedeutet Single Responsibility eine einzelne Methode?

Nein. Eine Klasse kann mehrere Methoden haben, solange sie alle mit derselben Verantwortlichkeit zusammenhängen. Eine UserRepository-Klasse könnte create(), update(), delete() und findById() Methoden haben, da sie alle der einzigen Verantwortlichkeit der Benutzerdatenpersistenz dienen. Die Methoden sind kohäsive Variationen desselben Anliegens, nicht separate, zusammengefasste Anliegen.

Werden Sie jetzt sicher.

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.