Single-Responsibility-Prinzip

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Das Single-Responsibility-Prinzip (SRP, deutsch Prinzip der eindeutigen Verantwortlichkeit) ist eine Entwurfsrichtlinie in der Softwarearchitektur.

Eine weit verbreitete, aber fehlerhafte Annahme ist, dass SRP aussagt, dass jede Klasse nur eine fest definierte Aufgabe zu erfüllen habe.[1]

Der Ausdruck wurde von Robert C. Martin in einem Teilartikel gleichen Namens in seiner Publikation Principles of Object Oriented Design[2] eingeführt:

“There should never be more than one reason for a class to change.”

„Es sollte nie mehr als einen Grund geben, eine Klasse zu ändern.“

Robert C. Martin: SRP: The Single Responsibility Principle[3]

Bekannt wurde der Ausdruck durch sein Buch Agile Software Development: Principles, Patterns, and Practices.

In seinem Buch Clean Architecture: A Craftsman’s Guide to Software Structure and Design geht Robert C. Martin auf die Fehlinterpretation des SRP ein und schlägt die „finale Version“ der Definition vor.

“A module should be responsible to one, and only one, actor.”

„Ein Modul sollte einem, und nur einem, Akteur gegenüber verantwortlich sein.“

Robert C. Martin: Clean Architecture: A Craftsman’s Guide to Software Structure and Design

Somit geht es beim SRP nicht nur um die einzelnen Klassen oder Funktionen. Vielmehr geht es um durch die Anforderungen eines Akteurs definierten Sammlungen an Funktionalitäten und Datenstrukturen.

Verallgemeinerung des Single-Responsibility-Prinzips

[Bearbeiten | Quelltext bearbeiten]

Funktionen und Variablen

[Bearbeiten | Quelltext bearbeiten]

Eine Verallgemeinerung des SRP stellt Curly’s Law dar, welches SRP, methods should do one thing,[4] once and only once (OAOO),[5] don’t repeat yourself (DRY) und single source of truth (SSOT) zusammenfasst. Das SRP kann und soll demnach für alle Aspekte eines Softwareentwurfs angewendet werden. Dazu gehören nicht nur Klassen, sondern unter anderem auch Funktionen und Variablen. Es ist daher auch bei der Verwendung von nicht-objektorientierten Programmiersprachen und dem Entwurf von Serviceschnittstellen gültig.

“A functional unit on a given level of abstraction should only be responsible for a single aspect of a system’s requirements. An aspect of requirements is a trait or property of requirements, which can change independently of other aspects.”

Ralf Westphal[6]

“A variable should mean one thing, and one thing only. It should not mean one thing in one circumstance, and carry a different value from a different domain some other time. It should not mean two things at once. It must not be both a floor polish and a dessert topping. It should mean One Thing, and should mean it all of the time.”

Tim Ottinger[7]

Beispiel
In dem folgenden Beispiel wird eine Reihe von Zahlen sortiert:

var numbers = new [] { 5,8,4,3,1 };
numbers = numbers.OrderBy(i => i);

Da die Variable numbers zuerst die unsortierten Zahlen repräsentiert und nachher die sortierten Zahlen, wird Curly’s Law verletzt. Dies lässt sich auflösen, indem eine zusätzliche Variable eingeführt wird:

var numbers = new [] { 5,8,4,3,1 };
var orderedNumbers = numbers.OrderBy(i => i);

Auch in der Unix-Philosophie kommt ein ähnliches Prinzip vor, denn hier sollen Anwendungen einen einzelnen Zweck erfüllen.

Make each program do one thing well. By focusing on a single task, a program can eliminate much extraneous code that often results in excess overhead, unnecessary complexity, and a lack of flexibility.

Gestalte jedes Programm so, dass es eine Aufgabe gut erledigt. Durch die Fokussierung auf eine einzelne Aufgabe, kann ein Programm viel unnötigen Code eliminieren, welcher oft zu übertriebenem Overhead, unnötiger Komplexität und mangelnder Flexibilität führt.“

Mike Gancarz: The UNIX Philosophy[8]

Anwendungen und Benutzerschnittstellen nach einem einzelnen Zweck aufzuteilen, besitzt nicht nur in der Entwicklung Vorteile. Auch für Benutzer sind Programme und Benutzerschnittstellen mit einem klar bestimmten Aufgabenzweck besser verständlich und schneller erlernbar. Nicht zuletzt ergeben sich Vorteile bei beschränkten Bildschirmgrößen, wie dies z. B. bei Smartphones der Fall ist.

Verwandte Muster

[Bearbeiten | Quelltext bearbeiten]

Das Interface-Segregation-Prinzip kann als ein Spezialfall des SRP gesehen werden. Es entsteht durch die Anwendung des SRP auf Interfaces.

Command-Query-Separation dient dazu, Funktionen und Entitäten nach ihrer Aufgabe zu trennen, indem zwischen Kommandos (Commands) und Abfragen (Queries) unterschieden wird. Ähnliches gilt für CQRS, welches unterschiedliche Codepfade für Datenbankzugriffe definiert, welche unabhängig voneinander optimiert werden können.

Querschnittsaspekte

[Bearbeiten | Quelltext bearbeiten]

Querschnittsaspekte, welche die gesamte Anwendung betreffen, stellen bezüglich des SRP eine besondere Herausforderung dar. Hierzu zählt insbesondere das Logging.

Bewusster Verstoß gegen das SRP

[Bearbeiten | Quelltext bearbeiten]

Viele Entwickler vertreten die Ansicht, dass bei Querschnittsaspekten gegen das SRP verstoßen werden sollte, da Querschnittsaspekte, wie das Logging, so nah wie möglich an der zuständigen Geschäftslogik sein sollten.

public sealed class PersonRepository : IPersonRepository
{
    private static ILogger Log = ...;

    public Person GetByName(string name)
    {
        try
        {
            return ...;
        }
        catch(Exception ex)
        {
            Log.Error(ex, $"Could not get Person named {name}");
            throw;
        }
    }
}

Das Logging direkt in der Methode führt allerdings dazu, dass das SRP nicht eingehalten und die Methode spaghettifiziert wird. Das Lesen und Testen der Geschäftslogik wird durch den Code des Aspekts erschwert.

Decorator-Methode

[Bearbeiten | Quelltext bearbeiten]

Eine Decorator-Methode ist eine einfache Möglichkeit, den Aspekt und die Geschäftslogik in getrennte Methoden auszulagern.

public sealed class PersonRepository : IPersonRepository
{
    private static ILogger Log = ...;

    public Person GetByName(string name)
    {
        try
        {
            return GetByNameWithoutLogging(name);
        }
        catch(Exception ex)
        {
            Log.Error(ex, $"Could not get Person named {name}");
            throw;
        }
    }

    private Person GetByNameWithoutLogging(string name)
    {
        return ...;
    }
}

Nachteilig ist, dass der Aspekt zwar auf Methodenebene ausgelagert wurde, allerdings weiterhin in der Klasse vorhanden ist. Dies stellt daher eine Verletzung des SRP auf Klassenebene dar. Zwar wird die Lesbarkeit verbessert, jedoch stellt sich beim Testen weiterhin die Herausforderung, dass der Aspekt mitgetestet werden muss.

Aspektorientierte Programmierung

[Bearbeiten | Quelltext bearbeiten]

Die Aspektorientierte Programmierung (AOP) stellt einen alternativen Ansatz dar, um den Aspekt auszulagern. Hierbei wird die Logik lediglich über eine Auszeichnung definiert und von einem Aspekt-Weaver implementiert.

public sealed class PersonRepository : IPersonRepository
{
    [LogToErrorOnException]
    public Person GetByName(string name)
    {
        return ...;
    }
}

Nachteilig ist hierbei, dass das SRP nicht eingehalten wird, da der Aspekt weiterhin in der Klasse verbleibt. Zudem können eventuell nicht alle Aspekte ausgegliedert werden. Beispielsweise kann im obigen Beispiel mit einem Attribut keine parametrisierte Fehlermeldung angegeben werden. Dies führt dazu, dass die Lösung an vielen Stellen annähernd dieselbe Komplexität aufweist wie die ursprüngliche Lösung:

public sealed class PersonRepository : IPersonRepository
{
    public Person GetByName(string name)
    {
        try
        {
            return ...;
        }
        catch(Exception ex)
        {
            LogTo.Error(ex, $"Could not get Person named {name}");
            throw;
        }
    }
}

Zudem befindet sich die Logik des Aspekt nach dem Kompiliervorgang weiterhin in der Klasse und erschwert daher weiterhin die Testbarkeit.

Eine weitere Möglichkeit den Aspekt von der Geschäftslogik zu trennen besteht darin, abgeleitete Klassen einzuführen.

public class PersonRepository : IPersonRepository
{
    public virtual Person GetByName(string name)
    {
        return ...;
    }
}

public sealed class LoggingPersonRepository : PersonRepository
{
    private static ILogger Log = ...;

    public override Person GetByName(string name)
    {
        try
        {
            return base.GetByName(name);
        }
        catch(Exception ex)
        {
            Log.Error(ex, $"Could not get Person named {name}");
            throw;
        }
    }
}

Diese Lösung verstößt allerdings gegen das Prinzip Komposition an Stelle von Vererbung einzusetzen. Ein weiterer Nachteil ist, dass sämtliche Klassen und Methoden für Vererbung geöffnet werden müssen, wodurch zudem gegen das Open-Closed-Prinzip verstoßen wird.

Unterklassen zur Auslagerung von Aspekten stellen daher ein Antipattern dar.

Aspekte lassen sich mittels eines Decorators realisieren und somit von der Geschäftslogik trennen.

public sealed class PersonRepository : IPersonRepository
{
    public Person GetByName(string name)
    {
        return ...;
    }
}

public sealed class PersonRepositoryLoggingFacade : IPersonRepository
{
    private static ILogger Log = ...;
    public IPersonRepository Repository { get; }

    public PersonRepositoryLoggingFacade(PersonRepository repository)
    {
        Repository = repository;
    }

    public Person GetByName(string name)
    {
        try
        {
            return Repository.GetByName(name);
        }
        catch(Exception ex)
        {
            Log.Error(ex, $"Could not get Person named {name}");
            throw;
        }
    }
}

Der Vorteil hierbei ist, dass das Prinzip der Komposition an Stelle von Vererbung eingehalten wird. Die Klasse PersonRepository kann in Folge gegenüber Vererbung geschlossen werden, da eine Erweiterung durch Komposition jederzeit möglich ist. Ein weiterer Vorteil ist, dass der Aspekt durch eine Konfiguration der Dependency Injection ausgetauscht werden kann. Zudem kann die Logging-Logik unabhängig von der Business-Logik getestet werden.

Nachteilig ist allerdings ein höherer Wartungsaufwand, da in der Dependency Injection sowohl die Klasse mit der Geschäftslogik, als auch die Klasse mit dem Aspekt verwaltet werden muss. Durch die Trennung wird zudem die Nachvollziehbarkeit (z. B. in welcher Klasse ein Fehler aufgetreten ist) erschwert.

Die konsequente Anwendung des Single-Responsibility-Prinzips führt dazu, dass anstatt des Spaghetticodes ein sogenannter Raviolicode entsteht.[9] Dabei handelt es sich um Code mit sehr vielen kleinen Klassen und kleinen Methoden.

Raviolicode besitzt den Nachteil, dass die Menge an Klassen in großen Projekten dazu führt, dass eine geringere Übersichtlichkeit gegeben ist. Dies betrifft insbesondere die in objektorientierten Programmiersprachen auftretenden Functor-Klassen,[10] also Klassen mit nur einer einzigen Methode.

Das SRP macht somit eine saubere Strukturierung mittels Modulen, Namespaces und Fassaden zwingend notwendig, damit die Übersichtlichkeit nicht verloren geht.

Einzelnachweise

[Bearbeiten | Quelltext bearbeiten]
  1. Robert C. Martin: Clean Architecture: A Craftsman’s Guide to Software Structure and Design. 1. Auflage. Prentice Hall, 2017, ISBN 978-0-13-449416-6, S. 62.
  2. Robert C. Martin: The Principles of OOD. 11. Mai 2005, abgerufen am 22. April 2014 (englisch).
  3. Robert C. Martin: SRP: The Single Responsibility Principle. (PDF) Februar 1997, archiviert vom Original am 7. April 2014; abgerufen am 22. April 2014 (englisch).  Info: Der Archivlink wurde automatisch eingesetzt und noch nicht geprüft. Bitte prüfe Original- und Archivlink gemäß Anleitung und entferne dann diesen Hinweis.@1@2Vorlage:Webachiv/IABot/www.objectmentor.com
  4. Robert C. Martin: Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall International, ISBN 978-0-13-235088-4.
  5. Once And Only Once. In: Cunningham & Cunningham. Abgerufen am 26. April 2014 (englisch).
  6. Ralf Westphal: Taking the Single Responsibility Principle Seriously. In: developerFusion. 6. Februar 2012, abgerufen am 22. April 2014 (englisch).
  7. Jeff Atwood: Curly’s Law: Do One Thing. In: Coding Horror. 1. März 2007, abgerufen am 22. April 2014 (englisch).
  8. Mike Gancarz: The UNIX Philosophy. 1995, ISBN 1-55558-123-4, The UNIX Philosophy in a Nutshell, S. 4 (englisch).
  9. Ravioli Code. In: Portland Pattern Repository. 21. Mai 2013, abgerufen am 4. März 2017 (englisch).
  10. Functor Object. In: Portland Pattern Repository. 10. November 2014, abgerufen am 4. März 2017 (englisch).