Dieser Blog-Eintrag soll der erste einer kurzen Serie zu SOLID sein. SOLID ist ein Akronym für fünf Prinzipien zur Gestaltung von Programmiercode. Natürlich gibt es neben diesen noch viele weitere, aber SOLID bietet eine gute Grundlage, welche sich auf die meisten Projekte, welche mit objektorientierten Programmierparadigma umgesetzt werden.
SOLID ist
- S: Single Responsibility Principle
- O: Open/Closed Principle
- L: Liskov'sches Substitutions-Prinzip
- I: Interface Segregation
- D: Dependency Inversion
In diesem ersten Eintrag von heute soll es um das Single Responsibility Principle gehen.
Vorwort
Die SOLID-Prinzipien machen Aussagen dazu, wie Programmcode strukturiert werden soll. Ob man diesen Prinzipien folgt oder nicht sollte keinen Unterschied auf die Funktionstüchtigkeit der Anwendung machen, bzw. es ist nicht notwendig, eines oder mehrere dieser Prinzipien zu befolgen, damit eine Anwendung funktioniert.
Beim Single Responsibility Principle (kurz SRP) ist es besonders schwierig festzustellen, ab wann das Prinzip ausreichend respektiert wird. Während es zwar ohne Probleme möglich ist, das Prinzip in einigen eleganten Sätzen zu definieren und charakterisieren, ist es alles andere als offensichtlich, wie SRP-konformer Code in der Praxis aussehen soll. Die Meinungen zu dieser Fragestellung gehen jeweils auch weit auseinander, und auch dieser Blogpost enthält nicht mehr als meine eigene Meinung, wie eine sinnvolle Umsetzung des Prinzips aussehen könnte.
Ziele des Prinzips
Wie erwähnt spielt es also keine Rolle, ob man nun SRP anwendet oder nicht, damit ein Programm funktioniert wie es soll. Sich zu überlegen, wie man SRP umsetzen möchte, wie auch es dann tatsächlich einzubauen (oder Code von Beginn an nach SRP zu schreiben) benötigt zusätzliche Zeit und Denkarbeit. Damit wäre es wohl gut, wenn für den zusätzlichen Aufwand auch etwas herausspringen würde.
Die Absichten und Ziele des SRP sind hauptsächlich:
- Wenn zeitgleich Änderungen an zwei unterschiedlichen Funktionalitäten vorgenommen werden müssen, sollen für diese Änderungen mit SRP auch unterschiedliche Module (Klassen) betroffen sein, und
somit verhindert werden, dass Konflikte entstehen.
- Änderungen an einer bestimmten Funktionalität sollen so wenige Module wie möglich betreffen, und nicht Änderungen durch die ganze Anwendung nach sich ziehen. Damit sinkt das Risiko, dass
fehlerhafte Änderung weitreichende Konsequenzen haben.
- Einfacher verständlicher und übersichtlicherer Code.
SRP-konforme Applikationen bieten also den Vorteil einer einfacheren Wartbarkeit.
Das Prinzip - theoretisch
Der Leitsatz, welcher das Prinzip prägt ist folgender:
A class should have only one reason to change.
(Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices)
Gemeint ist damit, dass es pro Klasse nur eine Funktionalität geben soll, und somit nur einen möglich Grund diese Klasse zu ändern, nämlich um diese Funktionalität zu ändern. Eine Klasse soll also nur "ein Ding" tun. Da es praktisch immer möglich ist, Funktionalitäten in kleinere Unter-Funktionalitäten aufzuteilen, besteht das Risiko, irgendwann eine Unzahl an Klassen zu erhalten, deren Funktionalitäten so klein sind, dass es bereits wieder schwieriger wird, zu verstehen wie eine Anwendung funktioniert.
In einem Blogpost offeriert Robert Martin eine andere Formulierung für das SRP:
Gather together the things that change for the same reasons. Separate those things that change for different reasons.
Als Gründe für eine Änderung nennt Martin die Personen, welche Änderungen beantragen, und illustriert, dass verschiedene Personen in einem Unternehmen an verschiedenen Aspekten der Anwendung interessiert sind und somit an unterschiedlichen Teilen der Anwendung Änderungen beantragen können.
Das Prinzip - praktisch
Das Gute an einer theoretischen Definition eines solchen Prinzipes ist natürlich, dass es so ziemlich auf alle Fälle von Programmierprojekten anwendbar ist. Leider ist eine solche Definition allerdings dann oft so vage, dass, selbst wenn man das Prinzip versteht, sich fragen muss, was es jetzt konkret zu tun gibt.
Eine häufig anzutreffende Art von Applikation sind Web-Applikationen. Das SRP rät also, nicht den gesamten Code in dieselbe Klasse zu schreiben. Bei grösseren Webapplikation entstehen damit sehr schnell sehr viele Klassen. Um die ganze Angelegenheit übersichtlich zu halten, ist es also sehr naheliegend, diese Klassen in einer hierarchischen Struktur zu organisieren, womit man sagen könnte, dass das SRP andere bekannte Prinzipien impliziert, z.B. die bekannten 3-Tier Strukturen MVC und MVVM.
Für die Klassen im Model-Tier ist es einfach, sie SRP-konform zu machen, denn ihre einzige Aufgabe (also ihre "single responsibility") ist es, je eine Entität aus der Datenbank zu repräsentieren. Sofern also keine weiteren Funktionalitäten in diese Klassen geschrieben werden, sind diese damit SRP-konform.
Ebenfalls recht einfach ist der Presentation Layer bzw. View-Tier. Hier besteht die einzige Aufgabe darin, die Daten, welche von aussen in die View gespeist werden darzustellen. Wichtig hier ist, dass allfällige Funktionalitäten, welche nichts mit der graphischen Darstellung zu tun haben in eigene Klassen ausgelagert werden. Typischerweise zum Beispiel client-seitige Formularvalidierungen, welche eine eigene Funktionalität darstellen, und somit ein eigener "Reason for Change" sind. Zudem sollten Elemente, welche wiederverwendet werden, zum Beispiel die Navigation, ausgelagert werden, um Code-Redundanzen zu vermeiden.
Kniffliger ist der mittlere Layer einer 3-Tier Applikation. Hier finden sich die Controller, welche das grösste Potenzial haben, weit mehr als nur eine Funktionalität zu beinhalten. Der gängigste Ansatz ist es, alle Logik, welche nicht strikt mit dem Datenaustausch zwischen den Layern zu tun haben in sog. Services auszulagern. Typische Services sind solche welche das Laden von Daten aus externen API's übernehmen oder Daten verarbeiten. Jedoch, meiner Meinung nach, wäre es übertrieben, für jede einzelne Art Request einen eigenen Controller zu schreiben. Andererseits ist es nicht ratsam, bei grossen WebApps mit vielen Seiten alle Request durch denselben Controller abzuarbeiten. Eine gängige Praxis ist es, Entitäts-spezifische CRUD-Controller zu haben. Das bedeutet, dass ein Controller alle Requests behandelt, welche mit einer spezifischen Entität zu tun haben. Dies ist oft möglich, da oft Webseiten bestimmte Datensätze präsentieren (z.B. eine Liste von Benutzern --> UserController).
Fazit
Das Single Responsibility Prinzip ist der Grund, wieso heutzutage Applikationen nicht in einer einzigen Datei auscodiert werden, und damit enorm wichtig. Aber man sollte das Prinzip mit Vorsicht geniessen und immer wieder kritisch hinterfragen, ob es wirklich sinnvoll ist, eine bestehende Funktionalität noch weiter aufzuteilen. Meinungen gibt es viele, und die einzig klare Aussage ist, dass jeder für sich selbst, bzw. sein Team bestimmen muss, wie granular der Code modularisiert werden soll.