Solid Prinzip
Das Akronym “SOLID” beschreibt Prinzipien, die beim Entwurf eines objektorientierten Designs zu berücksichtigen sind. Durch das Einhalten dieser Prinzipien werden entscheidend wichtige Qualitätsmerkmale in der Softwareentwicklung wie Stabilität, Wartbarkeit, Testbarkeit, Erweiterbarkeit etc. erreicht. Dieses Prinzipien kann sowohl auf Klassen wie auch auf Komponenten angewendet werden.
Wie definiert sich Solid?
Solid präsentiert sich mittels folgenden Prinzipien:
S |
Single-Responsibility-PrinzipDas Single-Responsibility-Prinzip besagt, dass jede Klasse nur eine einzige Verantwortung haben soll. Sie sollen die Verantwortung über bestimmte abgegrenzte Aktivitäten beinhalten und nur eine bestimmte Menge an Informationen haben. Ein Beispiel liefert die Klasse Person, die nebst Angaben zu einer Person eine Funktion einer AHV-Nr.-Validierung beinhaltet. Hier treffen wir auf zwei unterschiedliche Verantwortlichkeiten Erstens: Alles was zu einer Person gehört Zweitens: Die AHV-Nr.-Validierung, welche nicht zum Aufgabenbereich dieser Klasse gehört, obwohl eine Person eine AHV-Nummer besitzt. Diese Verantwortlichkeiten müssen getrennt werden. Je besser sich eine Klasse auf eine Verantwortung oder Aufgabe konzentriert, je höher ist ihre Kohäsion* Bei diesem Prinzip ist auch dieser Grundsatz einzuhalten: |
O |
Open/closed PrinzipKomponenten sind so zu bauen, dass Erweiterungen ohne Änderungen des bestehenden Quellcodes möglich sind. Schnittstelle und Verhalten dürfen sich nicht ändern. Änderungen und Erweiterungen erfolgen in neuem Code. Hier ein Beispiel einer Log-Komponente, welches dieses Prinzip berücksichtigt: 1.) Definieren einer Schnittstelle interface ILogger { void Log(string message); } 2.) Erstellen der Klasse ConsoleLogger, welche Logs auf dem Bildschirm aus gibt. Diese Klasse implementiert die Schnittstelle ILogger. class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } } 3.) Erstellen der Klasse LogEngine, welche ebenfalls ILogger implementiert. Über den Konstruktor wird eine Instanz der Klasse ConsoleLogger zur Verfügung gestellt. class LogEngine : ILogger { private ILogger _logger = null; public LogEngine(ILogger logger) { this._logger = logger; } public void Log(string message) { _logger.Log(message); } } Dieser Klasse LogEngine können beliebige Implementationen übergeben werden, indem diese Objekte im Konstruktor injiziert* werden. Kommt z.B. eine neue Anforderung hinzu, dass z.B. die Software Log-Informationen in die Datenbank schreiben soll, muss am bestehenden Code nichts geändert werden. Es benötigt nur eine neue Klasse, welche die Schnittstelle ILogger implementiert und dafür sorgt, dass die Daten in die Datenbank geschrieben werden. Dies könnte dann wie folgt aussehen: class DbLogger : ILogger { public void Log(string message) { using (var context = new myModel()) { context.Log.Add(new Log() { Message = message }); context.SaveChanges(); } } } In der Praxis treffen wir oft auf Beispiele wie Folgendes, welches gegen dieses Prinzip verstösst, weil der bestehende Code verändert wird. public void WriteLog(Log log) { if (log.Type = consoleLog) { WriteToConsole(log) } else if (logType.Type = fileLog) { WriteToFile(log) } //Um folgendes "else if" wird der Code erweitert. //Änderungen am bestehenden Code sind vermeiden! else if (logType.Type = fileDB) { WriteToDB(log) } } |
L |
Liskovsches SubstitutionsprinzipDas Liskovsches Substitutionsprinzip (LSP) oder Ersetzbarkeitsprinzip fordert, dass eine Instanz einer abgeleiteten Klasse sich so verhalten muss, dass jemand, der meint, ein Objekt der Basisklasse vor sich zu haben, nicht durch unerwartetes Verhalten überrascht wird, wenn es sich dabei tatsächlich um ein Objekt eines Subtyps handelt. Hier ein Beispiel, welches gegen dieses Prinzip verstosst: Es beseht die Superklasse wie folgt: public class Rectangle { private int Height { get; set; } private int Width { get; set; } void ChangeHeight(int height) { Height = height; } void ChangeWidth(int width) { Width = width; } } Nun wird eine neue Klasse Square benötig. Diese wird von der Klasse Rectangle abgeleitet. public class Square : Rectangle { … Somit stellt die Klasse Square die Methoden ChangeHeight und ChangeWidth zur Verfügung. Aber was geschieht, wenn diese beiden Funktionen aufgerufen werden? Dann wird eine Seite es Quadrat verändert und folglich ist das Objekt dann kein Quadrat mehr, sondern ein Rechteck. Die Methode ChangeHeight und ChangeWidth verhalten sich anders als erwartet. |
I |
Interface-Segregation-PrinzipDas Interface-Segregation-Prinzip oder Schnittstellenaufteilungsprinzip besagt, dass grosse Schnittstellen in mehrere kleinere aufzuteilen sind, damit die zu implementierenden Klassen nicht unnötige Methoden beinhalten. Betrachten wir dies am Beispiel der CRUD-Operationen. Je nach Rolle ist es nur erlaubt, nur Daten zu lesen. andere rollen dürfen nebst Lesen auch Daten ändern oder hinzufügen. Damit gäbe es für unterschiedliche Rollen unterschiedliche Interfaces. So könnte es z.B. ein Interface geben, das nur das Lesen von Daten erlaubt und eines, das zusätzlich die Modifikation erlaubt. So kann beim Lesen von Daten auf IRead zugegriffen werden – mit einem weit schlankeren Interface. Hier ein Beispiel, was die Folgen davon sind, wenn Schnittstellen nicht optimal definiert sind: Da die Klasse Repository das Interface IDataOperation implementiert, jedoch nur lesender Datenzugriff erlaubt ist, muss demzufolge diese Klasse die Methode Add, Edit und Delete trotzdem beinhalten. Demzufolge werden diese Methoden wie im folgenden Beispiel zur Verfügung gestellt, jedoch nicht mit einer sinnvollen Implementierung bzw. so, dass bei der Verwendung eine Exception geworfen wird. Im folgenden Beispiel wird die beschriebene Problematik verdeutlicht: class Repository : IDataOperation { public Person Read(int? id) { using (var db = new MyContextDB()) { return result = db.Person.SingleOrDefault(b => b.id == id); } } public void Add(Person person) { throw new NotSupportedException(); } public void Delete(int? id) { throw new NotSupportedException(); } public void Edit(int? id) { throw new NotSupportedException(); } } Finden Sie also NotImplemtedException bzw. NotSupportedException im Code, könnte dies ein Indiz für die Verletzung des Interface-Segregation-Prinzip sein.
Besser zu lösen wäre die obige Aufgabe gemäss folgenden Beispiel, indem wir Schnittstellen verwenden, welche zu den erwartenden Aufgaben einer Klasse gehören. public class Repository : IRead, IModify { public void Modify(Person person) { using (var db = new MyContextDB()) { var result = db.Person.SingleOrDefault(b => b.id == id); if (result != null) { result.SomeValue = "Some other value"; db.SaveChanges(); } } } public Person Read(int? id) { using (var db = new MyContextDB()) { return result = db.Person.SingleOrDefault(b => b.id == id); } } }
|
D |
Dependency-Inversion-PrinzipDas Dependency Inversion Principle (DIP) beschäftigt sich mit der Abhängigkeit von Modulen. Im Allgemeinen wird das DIP wie folgt beschrieben:Module höherer Ebenen sollten nicht von Modulen niedrigerer Ebenen abhängen. Beide sollten von Abstraktionen abhängen. Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.Hier ein Beispiel, welches diesem Prinzip gerecht wird:1.) Definieren einer Schnittstelle public interface ILight{ void On(); void Off(); } 2.) Implementieren ILight public class Light : ILight { private bool IsOn = false; public void Off() { IsOn = false; } public void On() { IsOn = true; } } 3.) Implemetieren Switch public class Switch { private ILight switchClient; private Boolean switchClientPressed; public Switch(ILight switchClient) { this.switchClient = switchClient; } public void Press() { switchClientPressed = !switchClientPressed; if (switchClientPressed) { this.switchClient.Off(); } else { this.switchClient.On(); } } } In diesem Beispiel wird der Klasse Switch einfach eine Instanz der Klasse Light im Konstruktor mitgegeben. Switch stellt nur Methoden wie z.B. Press zur Verfügung, welches mit der Methoden des übergebenen Objektes interagiert. Dadurch werden Abhängigkeiten verhindert. Hier ein Beispiel, welches gegen dieses Prinzip verstösst : public class Switch { Light light = new Light(); public void Press() { if (this.light.IsOn) { this.light.Off(); } else { this.light.On(); } } } In der Klasse Switch wird die Klasse Light instanziert. Dadurch erzeugen wir eine Abhängigkeit zwischen diesen beiden Klassen. Grundsätzlich kann gesagt werden, “wenn in einer Klasse eine andere Klasse instanziert wird, entsteht eine Abhängigkeit”. |
Kohäsion*
Kohäsion ist ein Mass , wie gut eine Aufgabe bzw. eine Verantwortung mittels Software abgebildet wird. Kümmert sich eine Klasse nur um eine Aufgabe (Single-Responsibility-Prinzip ist erfüllt), so hat sie eine sehr hohe Kohäsion. Kümmert sie sich um viele verschiedene, nicht zusammengehörige Aufgaben, so hat sie eine geringe Kohäsion. Manager-Klassen haben typsicherweise eine sehr geringe Kohäsion.
Injiziert*
Eine Technik, wo ein Objekt über den Konstruktor eines anderen Objektes übergeben bzw. injiziert wird. Diese Technik wird bei Dependency Injection angewendet.