Einleitung: Warum Refactoring und SOLID Hand in Hand gehen

Jedes Softwaresystem, das seit mehr als ein paar Monaten aktiv entwickelt wird, häuft unweigerlich technische Schulden an. Schnelle Korrekturen, sich ändernde Anforderungen und der Druck, neue Funktionen zu liefern, führen oft zu Code, der zerbrechlich, schwer zu verstehen und schwer zu erweitern ist. Zwei Praktiken zeichnen sich als die effektivsten Gegenmittel gegen diesen Zerfall aus: refactoring und die SOLID-Prinzipien.

Refactoring ist die disziplinierte Technik, bestehenden Code zu restrukturieren, ohne sein externes Verhalten zu verändern. Es behebt keine Fehler oder fügt Funktionen hinzu; stattdessen verbessert es die interne Struktur, so dass zukünftige Änderungen sicherer und schneller werden. Die SOLID-Prinzipien, die von Robert C. Martin eingeführt wurden, bieten eine Reihe von Designrichtlinien, die, wenn sie befolgt werden, Code ergeben, der wartbar, testbar und widerstandsfähig gegen Veränderungen ist. Die beiden Disziplinen sind natürliche Verbündete. Refactoring ist das Werkzeug, das es Ihnen ermöglicht, eine bestehende Codebasis schrittweise in Übereinstimmung mit SOLID zu bringen, selbst wenn das ursprüngliche Design bei weitem nicht ideal war.

In der Praxis haben viele Entwicklungsteams Schwierigkeiten, SOLID-Prinzipien rückwirkend anzuwenden. Der ursprüngliche Code kann monolithisch sein, eng gekoppelt oder mit bedingter Logik übersät sein. Ohne einen systematischen Ansatz fühlt sich der Versuch, „es SOLID zu machen, überwältigend an. Hier leuchten Refactoring-Techniken. Indem man die Arbeit in kleine, verhaltenserhaltende Schritte aufteilt, kann man eine Codebasis schrittweise transformieren, bis sie mit jedem SOLID-Prinzip übereinstimmt. Dieser Artikel untersucht konkrete Refactoring-Techniken für jedes der fünf Prinzipien, komplett mit praktischen Anleitungen, wonach zu suchen ist und wie man vorgeht.

Die soliden Prinzipien verstehen

Bevor wir uns mit Refactoring-Techniken befassen, wird eine kurze Zusammenfassung des SOLID-Akronyms die Bühne bereiten:

  • Single Responsibility Principle (SRP): Eine Klasse sollte nur einen Grund haben, sich zu ändern – das heißt, sie sollte eine einzige, klar definierte Verantwortung haben.
  • Open/Closed Principle (OCP): Software-Entitäten (Klassen, Module, Funktionen) sollten zur Erweiterung geöffnet, aber zur Änderung geschlossen sein.
  • Liskov Substitution Principle (LSP): Objekte einer Superklasse sollten durch Objekte einer Subklasse ersetzt werden können, ohne die Richtigkeit des Programms zu beeinträchtigen.
  • Interface Segregation Principle (ISP): Clients sollten nicht gezwungen werden, sich auf Schnittstellen zu verlassen, die sie nicht verwenden.
  • Abhängigkeits-Umkehrungsprinzip (DIP): Hochrangige Module sollten nicht von niedrigrangigen Modulen abhängen; beide sollten von Abstraktionen abhängen.

Jedes Prinzip richtet sich an eine bestimmte Art von Codegeruch. SRP bekämpft Klassen, die „zu viel wissen. OCP bekämpft spröde, bedingte Ketten. LSP verhindert fragile Vererbungshierarchien. ISP bekämpft fette Schnittstellen, die nutzlose Methodenimplementierungen erzwingen. DIP geht es um die enge Kopplung an konkrete Implementierungen. Als nächstes betrachten wir, wie Refactoring diese Gerüche systematisch beseitigen kann.

Refactoring für das Prinzip der einheitlichen Verantwortung

Identifizierung von Verstößen

Das häufigste Symptom einer SRP-Verletzung ist eine Klasse, die mehr als einen Grund hat, sich zu ändern. Zum Beispiel eine Klasse namens , die Summen berechnet, die Rechnung zur Anzeige formatiert, sie in einer Datenbank speichert und eine E-Mail sendet, hat mindestens vier Verantwortlichkeiten. Jede Änderung der Steuerberechnung, HTML-Formatierung, Speicherschema oder E-Mail-Inhalte erzwingt eine Änderung derselben Klasse. Im Laufe der Zeit wird die Klasse groß, eng gekoppelt und schwer zu testen.

Um diese Verstöße zu erkennen, suchen Sie nach Klassennamen, die Wörter wie „Manager, „Prozessor, „Hilfe oder „Util enthalten. Diese Namen verbergen oft mehrere Verantwortlichkeiten. Untersuchen Sie auch die Methodensignaturen der Klasse: Wenn einige Methoden Parameter verwenden, die für andere Methoden irrelevant sind, ist das ein weiterer Hinweis. Eine Klasse, die viele verschiedene Pakete oder Module importiert, ist ebenfalls verdächtig.

Refactoring-Techniken

Das primäre Refactoring für SRP ist Extrahieren Klasse . Sie identifizieren eine Reihe von verwandten Feldern und Methoden, die ein zusammenhängendes Konzept bilden und sie in eine neue Klasse verschieben. Zum Beispiel können Sie aus der Klasse , und extrahieren. Jede neue Klasse hat jetzt einen einzigen Grund, sich zu ändern.

Wenn die Logik auf einige wenige Methoden und nicht auf eine ganze Klasse verteilt ist, verwenden Sie die Methode , um einen bestimmten Teil des Verhaltens zu isolieren. Dies macht die Verantwortung sichtbarer und bereitet den Boden für eine zukünftige Extrahierungsklasse. Eine verwandte Technik ist die Methode , wenn eine Methode logischer zu einer anderen Klasse zu gehören scheint als ihr aktueller Host.

Eine weitere wertvolle Technik ist Ersetzen Sie Inline-Code durch Funktionsaufruf, wenn Sie wiederholte Logik bemerken, die zu einer anderen Domäne gehört. Indem Sie diese Logik zu einer dedizierten Funktion oder Klasse verschieben, reduzieren Sie die Oberfläche der primären Klasse und machen Verantwortlichkeiten explizit. Das Ziel ist, dass jede Klasse in einem einzigen Satz beschrieben werden kann, ohne das Wort "und" zu verwenden.

Refactoring für das offene/geschlossene Prinzip anwenden

Ersetzen von Bedingungen durch Polymorphismus

Code, der OCP verletzt, enthält oft große oder -Anweisungen, die einen Typ oder Modus überprüfen. Zum Beispiel wird eine Methode, die die Versandkosten basierend auf einem String , oder berechnet, für neue Versandmethoden geschlossen.

Das Standard-Refactoring ist Ersetzen Sie Bedingt durch Polymorphismus. Sie erstellen eine abstrakte Basisklasse oder Schnittstelle (z. B. ) mit einer Methode . Jede Versandmethode wird zu einer konkreten Unterklasse. Der ursprüngliche Client-Code verwendet dann die Abstraktion, und neue Versandmethoden werden hinzugefügt, indem eine neue Unterklasse erstellt wird - offen für Erweiterungen, geschlossen für Änderungen.

Verwenden der Strategie und Dekorator Patterns

Das Strategie-Muster ist der klassische OCP-Treiber. Im Refactoring-Schritt beginnen Sie normalerweise mit der Definition der Strategieschnittstelle, verschieben dann die bedingten Zweige in separate Strategieklassen. Schließlich injizieren Sie die entsprechende Strategie zur Laufzeit in den Client. Dies geht oft Hand in Hand mit Extrahieren Klasse und Introduce Parameter Object, um die Strategiemethoden sauber zu halten.

Das Muster Decorator hilft, wenn Sie einem Objekt Verhalten hinzufügen müssen, ohne dessen Kernklasse zu ändern. Wenn Sie beispielsweise eine Klasse haben, die Klartext erzeugt, können Sie es mit oder dekorieren, ohne zu ändern. Das Refactoring in Richtung des Dekoratormusters beinhaltet normalerweise Extrahieren Superklasse oder Klasse in Interface konvertieren, so dass sowohl die Kernkomponente als auch die Dekorateure eine gemeinsame Abstraktion haben.

Selbst ohne formale Designmuster hilft das Prinzip, Komposition gegenüber Vererbung zu bevorzugen, OCP. Wenn Sie das Verhalten variieren müssen, erstellen Sie die Klasse aus kleineren austauschbaren Teilen, anstatt Logik in die Klasse selbst zu stopfen.

Refactoring unterstützt das Liskov-Substitutionsprinzip

Subtyping und Verhaltensverträge

LSP-Verstöße treten häufig als Methoden in einer Unterklasse auf, die unerwartete Ausnahmen auslösen, die Rückkehr von , wobei die Basisklasse ein gültiges Objekt zurückgibt, oder die Vorbedingungen schwächen und Postbedingungen stärken. Ein klassisches Beispiel ist eine Klasse, die von erbt, aber den / Vertrag verletzt, weil ein Quadrat beide Dimensionen gleich halten muss.

Der erste Refactoring-Schritt besteht darin, den Vertrag zu identifizieren Introduce Assertion oder Ersetzen Sie die Ausnahme durch Precondition Check, um implizite Verträge explizit zu machen. Wenn eine Unterklasse den Vertrag nicht einhalten kann, müssen Sie die Vererbung brechen. Im Fall von Rechteck/Quadrat besteht das richtige Refactoring darin, die Vererbung durch eine gemeinsame Schnittstelle zu ersetzen (z. B. /-Paar, das nicht das /]-Paar enthält. Sowohl als auch implementieren unabhängig voneinander.

Verwenden von Schnittstellen zur Durchsetzung von LSP

Ein praktischer Ansatz ist es, Interface aus der Basisklasse zu extrahieren, wenn Sie ein Verhalten der Unterklasse erkennen, das nicht übereinstimmt. Dann muss der Client nur von der Schnittstelle abhängen. Wenn die Schnittstellenmethoden so eng sind, dass jede Implementierung sie erfüllen kann, wird LSP automatisch zufrieden gestellt. Zum Beispiel, anstatt eine Basisklasse mit einer Methode zu haben, definieren Sie eine Schnittstelle beide und können als abstrakte Klasse implementieren, aber nur implementiert . Dadurch wird vermieden, dass eine leere Methode hat, die eine Ausnahme auslöst.

Eine weitere nützliche Refactoring-Methode ist Push Down Methode: Wenn eine Methode in einer Superklasse nur für einige Unterklassen sinnvoll ist, verschieben Sie sie nach unten zu diesen Unterklassen. Dies eliminiert das Risiko, dass eine Unterklasse eine unangemessene Methode erbt.

Implementieren der Schnittstellentrennung über Refactoring

Schnittstelle zum Teilen von Fetten

Der ISP wird oft verletzt, wenn eine einzelne Schnittstelle zu viele Methoden ansammelt. Zum Beispiel zwingt eine -Schnittstelle mit , , und einen einfachen Textdrucker, Stub-Methoden zu implementieren. Die Refactoring-Lösung ist Extract Interface (oder Split Interface), um kleinere, zusammenhängendere Schnittstellen zu erstellen: , , usw. Jeder Client hängt dann nur von den Schnittstellen ab, die er tatsächlich benötigt.

Wenn Sie sich aufteilen, suchen Sie nach Methodengruppen, die häufig von bestimmten Clients zusammen verwendet werden. Ein häufiger Fehler ist die vorzeitige Aufteilung in viele kleine Schnittstellen. Ziel ist eine Rollenschnittstelle: eine Schnittstelle, die eine einzelne Fähigkeit darstellt, die ein Client möglicherweise möchte. Zum Beispiel könnte eine -Schnittstelle und haben; diese beiden Methoden sind logisch gekoppelt und werden wahrscheinlich nicht getrennt.

Refactoring des bestehenden Kundencodes

Sobald die fette Schnittstelle aufgeteilt ist, müssen Sie jeden Client umgestalten, um nur die relevante Schnittstelle zu implementieren. Dies ist eine Mischung aus Methodensignatur ändern (um die schmalere Schnittstelle zu akzeptieren) und Klasse umbenennen (um die neue Rolle widerzuspiegeln). Möglicherweise müssen Sie auch große Implementierungsklassen aufteilen: Wenn eine Klasse beide implementiert und , aber nur einige Clients scannen, ist es völlig in Ordnung, dass die Klasse beide implementiert, solange kein Client gezwungen ist, von beiden abhängig zu sein.

Refactoring für das Dependency Inversion Prinzip

Abhängigkeiten abstrahieren

Eine typische DIP-Verletzung ist eine High-Level-Klasse, wie , die eine Low-Level-Klasse wie direkt instanziiert. Dies zwingt , von der konkreten Datenbankimplementierung abhängig zu sein, was es schwierig macht, mit einem In-Memory-Repository zu testen und auszutauschen. Das erste Refactoring ist Extract Interface aus der Abhängigkeitsklasse: Erstellen Schnittstelle und machen Sie implementieren Sie es. Dann ändern Sie , um von der Schnittstelle abhängig zu sein. An diesem Punkt haben Sie immer noch eine versteckte direkte Instanziierung - der nächste Schritt ist Parameter (Konstruktorinjektion) einzuführen, um die Abhängigkeit von außen zu passieren.

Abhängigkeitsinjektion und Inversion der Kontrolle

Die Refactoring-Technik Ersetzen Sie den Konstruktor durch die Fabrikmethode kann verwendet werden, wenn Sie die Konstruktoren nicht einfach ändern können. Alternativ verwenden Sie Ersetzen Sie die globale Referenz durch den Parameter, wenn die Abhängigkeit von einem statischen Singleton oder Service-Locator erhalten wird. Allmählich bewegen Sie sich dazu, alle Abhängigkeiten explizit einzuspritzen, normalerweise durch den Konstruktor. Dies macht die Klasse rein abhängig von Abstraktionen und offen für Tests mit Mocks oder Stubs.

Wenn Sie Konstruktor-Injektion haben, sollten Sie Extrahieren Methode Objekt, wenn die injizierten Abhängigkeiten in vielen Methoden verwendet werden - das kann ein Zeichen dafür sein, dass die Klasse selbst noch zu viele Verantwortlichkeiten hat.

Abstraktionen sollten dem Client gehören, nicht der konkreten Implementierung. Dies wird als Inversion of Ownership bezeichnet. Definieren Sie beim Refactoring die Abstraktion (Schnittstelle) im selben Paket wie das High-Level-Modul, das sie verwendet, nicht im Low-Level-Modul. Dadurch wird sichergestellt, dass das High-Level-Modul nicht von etwas abhängt, das das Low-Level-Modul steuert. Wenn sich Ihre Schnittstelle in der Low-Level-Bibliothek befindet, invertieren Sie sie, indem Sie die Schnittstellendefinition in das High-Level-Projekt verschieben und das Low-Level-Modul implementieren lassen (auch bekannt als Dependency Inversion auf der Paketebene.

Fazit: Refactoring zur Gewohnheit machen

Die Durchsetzung von SOLID-Prinzipien durch Refactoring ist keine einmalige Aktivität, sondern eine fortlaufende Disziplin. Die hier beschriebenen Techniken – Extrahieren von Klasse, Bedingt durch Polymorphismus ersetzen, Schnittstelle extrahieren, Parameter einführen und viele andere – sind die Bausteine, die es Ihnen ermöglichen, eine Codebasis schrittweise neu zu gestalten, ohne sie zu brechen. Jeder kleine Schritt reduziert technische Schulden, macht den Code verständlicher und öffnet die Tür für eine einfachere Erweiterung und Prüfung.

Um Ihre Praxis zu vertiefen, studieren Sie den Katalog der Refactorings in Martin Fowlers Refactoring-Website Für eine detaillierte Behandlung der SOLID-Prinzipien bietet Robert C. Martins Clean Code-Blog ausgezeichnete Erklärungen. Schließlich denken Sie daran, dass Refactoring ohne Tests gefährlich ist. Stellen Sie immer sicher, dass Sie eine solide Reihe von Tests haben, bevor Sie beginnen; Verwenden Sie Extraktmethode und Rename Variable als sichere erste Schritte, wenn Tests minimal sind. Im Laufe der Zeit wird die Kombination aus diszipliniertem Refactoring und SOLID-Denken Ihren Code in ein gut strukturiertes, formbares Asset verwandeln und nicht eine spröde Haftung.

Fangen Sie klein an: Wählen Sie eine Klasse, die gegen SRP verstößt, wenden Sie die Extrahieren-Klasse an und sehen Sie, wie der Rest des Systems reagiert. Das Vertrauen, das Sie gewinnen, wird Sie motivieren, das nächste Prinzip anzugehen. Mit konsequenter Praxis werden Sie diese Refactorings verinnerlichen und Code entwerfen, der SOLID von Anfang an respektiert.