SOLID-Prinzipien und Code-Struktur


150

Bei einem kürzlich durchgeführten Vorstellungsgespräch konnte ich keine Frage zu SOLID beantworten - über die grundlegende Bedeutung der verschiedenen Prinzipien hinaus. Es nervt mich wirklich. Ich habe ein paar Tage lang herumgegraben und muss noch eine zufriedenstellende Zusammenfassung finden.

Die Interviewfrage lautete:

Wenn Sie sich ein .Net-Projekt ansehen würden, von dem ich Ihnen sagte, dass es die SOLID-Prinzipien strikt befolgt, was würden Sie in Bezug auf das Projekt und die Codestruktur erwarten?

Ich zappelte ein bisschen herum, beantwortete die Frage nicht wirklich und bombardierte dann.

Wie hätte ich besser mit dieser Frage umgehen können?


3
Ich habe mich gefragt, was in der Wiki-Seite für SOLID
BЈовић am

Erweiterbare abstrakte Bausteine.
Rwong

Wenn Sie die SOLID-Prinzipien des objektorientierten Designs befolgen, sind Ihre Klassen normalerweise klein, faktoriell und leicht zu testen. Quelle: docs.asp.net/en/latest/fundamentals/…
WhileTrueSleep

Antworten:


188

S = Prinzip der Einzelverantwortung

Ich würde also eine gut organisierte Ordner- / Dateistruktur und Objekthierarchie erwarten. Jede Klasse / Funktion sollte so benannt werden, dass ihre Funktionalität sehr offensichtlich ist, und sie sollte nur Logik enthalten, um diese Aufgabe auszuführen.

Wenn Sie riesige Managerklassen mit Tausenden von Codezeilen sehen würden, wäre dies ein Zeichen dafür, dass eine einzelne Verantwortung nicht befolgt wird.

O = offenes / geschlossenes Prinzip

Dies ist im Grunde die Idee, dass neue Funktionen durch neue Klassen hinzugefügt werden sollten, die nur minimale Auswirkungen auf vorhandene Funktionen haben oder deren Änderung erforderlich ist.

Ich würde erwarten, dass Objektvererbung, Untertypisierung, Schnittstellen und abstrakte Klassen in großem Umfang zum Einsatz kommen, um das Design eines Teils der Funktionalität von der tatsächlichen Implementierung zu trennen und anderen zu ermöglichen, nebenbei andere Versionen zu implementieren, ohne die zu beeinträchtigen Original.

L = Liskov-Substitutionsprinzip

Dies hat mit der Möglichkeit zu tun, Untertypen als übergeordnete Typen zu behandeln. Dies ist in C # ein Kinderspiel, wenn Sie eine ordnungsgemäße Hierarchie geerbter Objekte implementieren.

Ich würde erwarten, dass Code allgemeine Objekte als Basistyp behandelt und Methoden für die Basis- / Abstraktionsklassen aufruft, anstatt die Untertypen selbst zu instanziieren und zu bearbeiten.

I = Prinzip der Grenzflächentrennung

Dies ist ähnlich wie bei SRP. Grundsätzlich definieren Sie kleinere Teilmengen von Funktionalität als Schnittstellen und mit denen arbeiten , um Ihr System zu halten entkoppelt (zB FileManagerkönnten die einzelnen responsibilty Umgang mit Datei - I / O, aber das könnte ein implementieren IFileReaderund IFileWriterdie enthaltenen spezifischen Methodendefinitionen für das Lesen und Schreiben von Dateien).

D = Abhängigkeitsumkehrprinzip.

Auch hier geht es darum, ein System entkoppelt zu halten. Vielleicht möchten Sie eine .NET Dependency Injection-Bibliothek verwenden, die in der Lösung wie Unityoder Ninjectoder in einem ServiceLocator-System wie verwendet wird AutoFacServiceLocator.


36
Ich habe in C # viele LSP-Verstöße gesehen. Jedes Mal, wenn jemand entscheidet, dass sein bestimmter Subtyp spezialisiert ist, muss er kein Stück der Schnittstelle implementieren und löst stattdessen nur eine Ausnahme für dieses Stück aus. Dies ist ein gängiger Ansatz für Junioren um das Problem des Missverständnisses der Schnittstellenimplementierung und -gestaltung zu lösen
Jimmy Hoffa

2
@JimmyHoffa Das ist einer der Hauptgründe, warum ich auf der Verwendung von Codeverträgen bestehe. Der Denkprozess beim Entwerfen der Verträge hilft dabei, die Menschen von dieser schlechten Angewohnheit zu befreien.
Andy

12
Ich mag es nicht, wenn der "LSP in C # sofort einsatzbereit ist" und DIP mit der Abhängigkeitsinjektionspraxis gleichgesetzt wird.
Euphoric

3
+1 aber Abhängigkeitsinversion <> Abhängigkeitsinjektion. Sie spielen gut zusammen, aber Abhängigkeitsinversion ist viel mehr als nur Abhängigkeitsinjektion. Hinweis: DIP in freier Wildbahn
Marjan Venema

3
@Andy: Was ebenfalls hilft, sind Komponententests, die auf den Schnittstellen definiert sind, an denen alle Implementierer (jede Klasse, die instanziiert werden kann / kann) getestet werden.
Marjan Venema

17

Überall gibt es viele kleine Klassen und Schnittstellen mit Abhängigkeitsinjektion. Wahrscheinlich würden Sie in einem großen Projekt auch ein IoC-Framework verwenden, um die Lebensdauer all dieser kleinen Objekte zu konstruieren und zu verwalten. Siehe https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-looking-into

Beachten Sie, dass ein großes .NET-Projekt, das STRIKT SOLID-Prinzipien folgt, nicht unbedingt eine gute Codebasis für jedermann bedeutet. Je nachdem, wer der Interviewer war, wollte er / sie, dass Sie zeigen, dass Sie verstehen, was SOLID bedeutet, und / oder überprüfen, wie dogmatisch Sie die Gestaltungsprinzipien befolgen.

Sie sehen, um SOLID zu sein, müssen Sie folgen:

S ingle Verantwortung Prinzip, so dass Sie nur viele kleine Klassen jeder von ihnen eine Sache tut haben

O Pen-Closed-Prinzip, das in .NET normalerweise mit Abhängigkeitsinjektion implementiert wird, was auch das I und das D unten erfordert ...

Das L iskov-Substitutionsprinzip lässt sich wahrscheinlich nicht mit einem Einzeiler in c # erklären. Zum Glück gibt es andere Fragen , die es Adressierung, zB https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

I nterface Segregations Prinzip arbeitet zusammen mit dem Open-Closed - Prinzip. Wenn man es wörtlich befolgt, würde es bedeuten, eine große Anzahl sehr kleiner Schnittstellen zu bevorzugen, anstatt nur wenige "große" Schnittstellen

Prinzip der D- Ependenz-Inversion Klassen auf hoher Ebene sollten nicht von Klassen auf niedriger Ebene abhängen, beide sollten von Abstraktionen abhängen.


SRP bedeutet nicht "nur eine Sache tun".
Robert Harvey

13

Einige grundlegende Dinge, die ich in der Codebasis eines Shops erwarten würde, der SOLID in seiner täglichen Arbeit unterstützt:

  • Viele kleine Codedateien - mit einer Klasse pro Datei als Best Practice in .NET und dem Prinzip der Einzelverantwortung, das kleine modulare Klassenstrukturen fördert, würde ich erwarten, viele Dateien zu sehen, die jeweils eine kleine, fokussierte Klasse enthalten.
  • Viele Adapter- und Composite-Muster - Ich würde die Verwendung vieler Adapter-Muster (eine Klasse, die eine Schnittstelle implementiert, indem sie die Funktionalität einer anderen Schnittstelle "durchläuft") erwarten, um das Einstecken einer für einen Zweck entwickelten Abhängigkeit in geringfügig zu rationalisieren verschiedene Orte, an denen auch seine Funktionalität benötigt wird. Aktualisierungen, die so einfach wie das Ersetzen eines Konsolenprotokollierers durch einen Dateiprotokollierer sind, verstoßen gegen LSP / ISP / DIP, wenn die Schnittstelle aktualisiert wird, um ein Mittel zum Angeben des zu verwendenden Dateinamens bereitzustellen. Stattdessen macht die Dateiprotokollierungsklasse die zusätzlichen Member verfügbar, und ein Adapter lässt die Dateiprotokollierung wie eine Konsolenprotokollierung aussehen, indem er die neuen Elemente verbirgt. Nur das Objekt, das all dies zusammenfasst, muss den Unterschied kennen.

    Wenn eine Klasse eine Abhängigkeit einer ähnlichen Schnittstelle zu einer vorhandenen hinzufügen muss, um das Ändern des Objekts (OCP) zu vermeiden, besteht die übliche Antwort darin, ein zusammengesetztes / strategisches Muster (eine Klasse, die die Abhängigkeitsschnittstelle implementiert und mehrere andere verwendet) zu implementieren Implementierungen dieser Schnittstelle mit unterschiedlich viel Logik, die es der Klasse ermöglichen, einen Aufruf an eine, einige oder alle Implementierungen weiterzuleiten).

  • Viele Schnittstellen und ABCs - DIP erfordert notwendigerweise das Vorhandensein von Abstraktionen, und der ISP empfiehlt, diese eng zu fassen. Daher sind Schnittstellen und abstrakte Basisklassen die Regel, und Sie benötigen viele davon, um die gemeinsam genutzten Abhängigkeitsfunktionen Ihrer Codebasis abzudecken. Während strenges SOLID das Injizieren von allem erfordern würde , ist es offensichtlich, dass Sie irgendwo erstellen müssen. Wenn also ein GUI-Formular immer nur als untergeordnetes Element eines übergeordneten Formulars erstellt wird, indem eine Aktion für dieses übergeordnete Element ausgeführt wird, kann ich das untergeordnete Formular nicht neu erstellen aus dem Code direkt im übergeordneten Element. Normalerweise mache ich diesen Code zu einer eigenen Methode. Wenn also zwei Aktionen der gleichen Form das Fenster öffnen, rufe ich einfach die Methode auf.
  • Viele Projekte - Ziel ist es, den Umfang der Änderungen zu begrenzen. Zu den Änderungen gehört das erneute Kompilieren (eine relativ triviale Aufgabe, die bei vielen prozessor- und bandbreitenkritischen Vorgängen, z. B. beim Bereitstellen von Updates für eine mobile Umgebung, weiterhin wichtig ist). Wenn eine Datei in einem Projekt neu erstellt werden muss, müssen alle Dateien neu erstellt werden. Das heißt, wenn Sie Interfaces in die gleichen Bibliotheken wie ihre Implementierungen stellen, verpassen Sie den Punkt. Sie müssen alle Verwendungen neu kompilieren, wenn Sie eine Implementierung der Schnittstelle ändern, da Sie auch die Schnittstellendefinition selbst neu kompilieren, sodass Verwendungen auf eine neue Position in der resultierenden Binärdatei verweisen müssen. Halten Sie daher die Schnittstellen von den Verwendungen und getrennt Implementierungen sind eine typische Best Practice, obwohl sie zusätzlich nach allgemeinem Verwendungsbereich getrennt sind.
  • Viel Aufmerksamkeit für die Terminologie "Gang of Four" - Die in den 1994 erschienenen Design Patterns identifizierten Design Patterns betonen das mundgerechte, modulare Code-Design, das SOLID erstellen möchte. Das Abhängigkeitsumkehrprinzip und das Offen / Geschlossen-Prinzip bilden beispielsweise das Herzstück der meisten identifizierten Muster in diesem Buch. Als solches würde ich erwarten, dass ein Geschäft, das sich stark an die SOLID-Prinzipien hält, auch die Terminologie in Gang of Fours Buch und die Namensklassen entsprechend ihrer Funktion in diese Richtungen einbezieht, wie z. B. "AbcFactory", "XyzRepository", "DefToXyzAdapter" "," A1Command "usw.
  • Ein generisches Repository - In Übereinstimmung mit ISP, DIP und SRP ist das Repository im SOLID-Design nahezu allgegenwärtig, da es dem konsumierenden Code ermöglicht, Datenklassen auf abstrahierte Weise anzufordern, ohne spezifische Kenntnisse über den Abruf- / Persistenzmechanismus zu benötigen Hiermit wird der Code, der dies ausführt, an einer anderen Stelle als mit dem DAO-Muster abgelegt (wenn Sie beispielsweise eine Invoice-Datenklasse hätten, hätten Sie auch eine InvoiceDAO, mit der hydratisierte Objekte dieses Typs erstellt wurden usw.) alle Datenobjekte / Tabellen in der Codebasis / Schema).
  • Ein IoC-Container - ich zögere, diesen hinzuzufügen, da ich kein IoC-Framework verwende, um den Großteil meiner Abhängigkeitsinjektion durchzuführen. Es wird schnell zu einem Gott-Objekt-Anti-Muster, das alles in den Behälter wirft, ihn aufrüttelt und die vertikal hydratisierte Abhängigkeit ausgießt, die Sie durch eine eingespritzte Fabrikmethode benötigen. Klingt großartig, bis Sie feststellen, dass die Struktur ziemlich monolithisch wird und das Projekt mit den Registrierungsinformationen, wenn es "fließend" ist, jetzt alles über alles in Ihrer Lösung wissen muss. Das sind viele Gründe, sich zu ändern. Wenn es nicht fließend ist (spät gebundene Registrierungen mit Konfigurationsdateien), dann stützt sich ein Schlüsselelement Ihres Programms auf "magische Strings", eine ganz andere Dose Würmer.

1
warum die downvotes?
KeithS

Ich denke das ist eine gute Antwort. Anstatt den vielen Blog-Posts zu diesen Begriffen ähnlich zu sein, haben Sie Beispiele und Erklärungen aufgeführt, die deren Verwendung und Wert
belegen

10

Lenken Sie sie mit Jon Skeets Diskussion darüber ab, wie das 'O' in SOLID "nicht hilfreich und schlecht verstanden" ist, und bringen Sie sie dazu, über Alistair Cockburns "geschützte Variante" und Josh Blochs "Erbschaftsentwurf" zu sprechen oder es zu verbieten.

Kurze Zusammenfassung von Skeets Artikel (obwohl ich nicht empfehlen würde, seinen Namen zu streichen, ohne den ursprünglichen Blog-Beitrag zu lesen!):

  • Die meisten Menschen wissen nicht, was "offen" und "geschlossen" im "offen-geschlossen-Prinzip" bedeuten, selbst wenn sie glauben, dies zu tun.
  • Häufige Interpretationen sind:
    • Diese Module sollten immer durch die Vererbung der Implementierung erweitert werden
    • dass der Quellcode des ursprünglichen Moduls niemals geändert werden kann.
  • Die zugrunde liegende Absicht von OCP und die ursprüngliche Formulierung von Bertrand Meyer ist in Ordnung:
    • Diese Module sollten genau definierte Schnittstellen haben (nicht unbedingt im technischen Sinne von 'Schnittstelle'), auf die sich ihre Kunden verlassen können, aber
    • Es sollte möglich sein, ihre Möglichkeiten zu erweitern, ohne diese Schnittstellen zu beschädigen.
  • Aber die Wörter "offen" und "geschlossen" verwirren das Thema nur, auch wenn sie ein schönes aussprechbares Akronym ergeben.

Das OP fragte: "Wie hätte ich besser mit dieser Frage umgehen können?" Als leitender Ingenieur, der ein Interview führt, wäre ich unermesslich mehr an einem Kandidaten interessiert, der intelligent über die Vor- und Nachteile verschiedener Code-Design-Stile sprechen kann, als an jemandem, der eine Liste von Stichpunkten auf den Kopf stellt.

Eine weitere gute Antwort wäre: "Nun, das hängt davon ab, wie gut sie es verstanden haben. Wenn sie nur die SOLID-Schlagworte kennen, würde ich mit Missbrauch der Vererbung, übermäßigem Einsatz von Frameworks zur Abhängigkeitsinjektion und einer Million kleiner Schnittstellen rechnen, von denen keine spiegeln das Domain-Vokabular wider, das für die Kommunikation mit dem Produktmanagement verwendet wird ... "


6

Es gibt wahrscheinlich eine Reihe von Möglichkeiten, wie dies mit unterschiedlichem Zeitaufwand beantwortet werden kann. Ich denke jedoch, dass dies eher im Sinne von "Wissen Sie, was SOLID bedeutet?" Die Beantwortung dieser Frage läuft also wahrscheinlich darauf hinaus, die Punkte zu treffen und sie anhand eines Projekts zu erklären.

Sie erwarten also Folgendes:

  • Klassen haben eine einzelne Verantwortung (z. B. eine Datenzugriffsklasse für Kunden erhält nur Kundendaten aus der Kundendatenbank).
  • Klassen können einfach erweitert werden, ohne das vorhandene Verhalten zu beeinflussen. Ich muss keine Eigenschaften oder andere Methoden ändern, um zusätzliche Funktionen hinzuzufügen.
  • Abgeleitete Klassen können durch Basisklassen ersetzt werden, und Funktionen, die diese Basisklassen verwenden, müssen die Basisklasse nicht in den spezifischeren Typ entpacken, um sie zu verarbeiten.
  • Schnittstellen sind klein und leicht verständlich. Wenn eine Klasse eine Schnittstelle verwendet, muss sie nicht von mehreren Methoden abhängen, um eine Aufgabe auszuführen.
  • Der Code ist so abstrahiert, dass die Implementierung auf hoher Ebene nicht konkret von einer bestimmten Implementierung auf niedriger Ebene abhängt. Ich sollte in der Lage sein, die Implementierung auf niedriger Ebene zu wechseln, ohne den Code auf hoher Ebene zu beeinflussen. Beispielsweise kann ich die SQL-Datenzugriffsebene für einen Webdienst ändern, ohne den Rest meiner Anwendung zu beeinträchtigen.

4

Dies ist eine ausgezeichnete Frage, obwohl ich denke, dass es eine schwierige Interviewfrage ist.

SOLID-Prinzipien bestimmen Klassen und Interfaces und wie sie sich zueinander verhalten.

Diese Frage hat mehr mit Dateien und nicht unbedingt mit Klassen zu tun .

Eine kurze Beobachtung oder Antwort, die ich geben möchte, ist, dass Sie im Allgemeinen Dateien sehen, die nur eine Schnittstelle enthalten, und häufig ist die Konvention, dass sie mit einem Groß-I beginnen. Darüber hinaus würde ich erwähnen, dass die Dateien keinen doppelten Code (insbesondere innerhalb eines Moduls, einer Anwendung oder einer Bibliothek) haben und dass der Code über bestimmte Grenzen zwischen Modulen, Anwendungen oder Bibliotheken hinweg sorgfältig geteilt wird.

Robert Martin erörtert dieses Thema im Bereich C ++ unter Entwerfen objektorientierter C ++ - Anwendungen mit der Booch-Methode (siehe Abschnitte zu Kohäsion, Closure und Wiederverwendbarkeit) und in Clean Code .


.NET-Codierer IME folgt normalerweise der Regel "1 Klasse pro Datei" und spiegelt auch Ordner- / Namespace-Strukturen. Die Visual Studio-IDE unterstützt beide Vorgehensweisen, und verschiedene Plug-ins wie ReSharper können sie erzwingen. Daher würde ich erwarten, dass eine Projekt- / Dateistruktur die Klassen- / Schnittstellenstruktur widerspiegelt.
KeithS
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.