Verwalten und organisieren Sie die massiv gestiegene Anzahl von Klassen nach dem Wechsel zu SOLID?


50

In den letzten Jahren haben wir langsam und schrittweise auf immer besser geschriebenen Code umgestellt. Wir beginnen endlich, auf etwas umzusteigen, das zumindest SOLID ähnelt, aber wir sind noch nicht ganz da. Seit dem Wechsel ist eine der größten Beschwerden der Entwickler, dass sie es nicht ertragen können, Dutzende und Dutzende von Dateien zu überprüfen und zu durchlaufen, bei denen es zuvor für jede Aufgabe nur erforderlich war, dass der Entwickler 5 bis 10 Dateien berührt.

Bevor wir mit dem Wechsel begannen, war unsere Architektur wie folgt organisiert (selbstverständlich mit ein oder zwei Größenordnungen mehr Dateien):

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

Aktenmäßig war alles unglaublich linear und kompakt. Es gab offensichtlich viel Code-Duplikation, enge Kopplung und Kopfschmerzen, aber jeder konnte es durchgehen und herausfinden. Komplette Neulinge, die noch nie Visual Studio geöffnet hatten, konnten es in nur wenigen Wochen herausfinden. Aufgrund der insgesamt fehlenden Komplexität der Dateien ist es für unerfahrene Entwickler und neue Mitarbeiter relativ einfach, Beiträge zu leisten, ohne dass auch die Anlaufzeit zu lang wird. Aber dies ist so ziemlich der Punkt, an dem die Vorteile des Codestils aus dem Fenster gehen.

Ich unterstütze von ganzem Herzen jeden Versuch, unsere Codebasis zu verbessern, aber es kommt sehr häufig vor, dass der Rest des Teams zu massiven Paradigmenwechseln wie diesem zurückgedrängt wird. Einige der größten Probleme sind derzeit:

  • Unit-Tests
  • Klassenanzahl
  • Peer-Review-Komplexität

Unit-Tests waren für das Team ein unglaublich schwerer Verkauf, da sie alle glauben, dass sie Zeitverschwendung sind und dass sie in der Lage sind, ihren Code viel schneller als jedes Teil einzeln zu testen. Unit-Tests als Bestätigung für SOLID zu verwenden, war größtenteils vergeblich und ist zu diesem Zeitpunkt größtenteils zu einem Scherz geworden.

Die Anzahl der Klassen ist wahrscheinlich die größte Hürde, die es zu überwinden gilt. Aufgaben, für die früher 5-10 Dateien benötigt wurden, können jetzt 70-100 dauern! Während jede dieser Dateien einem bestimmten Zweck dient, kann das schiere Dateivolumen überwältigend sein. Die Antwort des Teams war größtenteils Stöhnen und Kopfkratzen. Zuvor waren für eine Task möglicherweise ein oder zwei Repositorys, ein oder zwei Modelle, eine Logikschicht und eine Controllermethode erforderlich.

Um eine einfache Anwendung zum Speichern von Dateien zu erstellen, müssen Sie mit einer Klasse prüfen, ob die Datei bereits vorhanden ist, mit einer Klasse, mit der die Metadaten geschrieben werden, mit einer Klasse, die Sie abstrahieren DateTime.Nowkönnen, damit Sie Zeiten für Komponententests einfügen können, und mit Schnittstellen für jede Datei, die Logikdateien enthält Enthält Unit-Tests für jede Klasse und eine oder mehrere Dateien, um alles zu Ihrem DI-Container hinzuzufügen.

Für kleine bis mittlere Anwendungen ist SOLID ein supereinfaches Produkt. Jeder sieht den Vorteil und die einfache Wartbarkeit. Sie sehen jedoch gerade in sehr großen Anwendungen kein gutes Preis-Leistungs-Verhältnis für SOLID. Deshalb versuche ich, Wege zu finden, um Organisation und Management zu verbessern und die wachsenden Schmerzen zu überwinden.


Ich dachte, ich würde ein Beispiel des Dateivolumens basierend auf einer kürzlich abgeschlossenen Aufgabe etwas genauer beschreiben. Ich hatte die Aufgabe, einige Funktionen in einem unserer neueren Microservices zu implementieren, um eine Dateisynchronisierungsanforderung zu erhalten. Wenn die Anforderung empfangen wird, führt der Dienst eine Reihe von Suchen und Überprüfungen durch und speichert das Dokument schließlich auf einem Netzlaufwerk sowie in zwei separaten Datenbanktabellen.

Um das Dokument auf dem Netzlaufwerk zu speichern, benötigte ich einige bestimmte Klassen:

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

Das sind also insgesamt 15 Klassen (ohne POCOs und Gerüste), um ein relativ einfaches Speichern durchzuführen. Diese Zahl stieg erheblich an, als ich POCOs erstellen musste, um Entitäten in einigen Systemen darzustellen, einige Repos für die Kommunikation mit Systemen von Drittanbietern erstellte, die mit unseren anderen ORMs nicht kompatibel sind, und logische Methoden erstellte, um die Kompliziertheiten bestimmter Vorgänge zu bewältigen.


52
"Aufgaben, für die früher 5-10 Dateien benötigt wurden, können jetzt 70-100 dauern!" Wie zum Teufel? Das ist keineswegs normal. Welche Art von Änderungen machen Sie, die das Ändern so vieler Dateien erfordern?
Euphorischer

43
Die Tatsache, dass Sie mehr Dateien pro Task ändern müssen (deutlich mehr!), Bedeutet, dass Sie SOLID falsch machen. Der springende Punkt ist, Ihren Code (im Laufe der Zeit) so zu organisieren, dass er die beobachteten Änderungsmuster widerspiegelt und die Änderungen einfacher macht. Jedes Prinzip in SOLID hat eine bestimmte Begründung (wann und warum es angewendet werden sollte). Es sieht so aus, als hätten Sie sich in diese Situation gebracht, indem Sie diese blind angewendet haben. Gleiches gilt für Unit-Tests (TDD). Wenn Sie es tun, ohne ein gutes Verständnis für Design / Architektur zu haben, werden Sie sich in ein Loch graben.
Filip Milovanović

60
Sie haben SOLID eindeutig als Religion und nicht als pragmatisches Werkzeug für die Erledigung Ihrer Aufgaben eingesetzt. Wenn etwas in SOLID mehr Arbeit macht oder es schwieriger macht, tun Sie es nicht.
Whatsisname

25
@Euphoric: Das Problem kann auf beide Arten auftreten. Ich vermute, Sie reagieren auf die Möglichkeit, dass 70-100 Klassen übertrieben sind. Aber es ist nicht unmöglich, dass dies ein gewaltiges Projekt ist, das in 5-10 Dateien (ich habe vorher in 20KLOC-Dateien gearbeitet ...) gepackt wurde und eigentlich 70-100 Dateien sind.
Flater

18
Es gibt eine Denkstörung, die ich "Objektglückskrankheit" nenne. Dies ist der Glaube, dass OO-Techniken ein Selbstzweck sind und nicht nur eine von vielen möglichen Techniken, um die Kosten für die Arbeit in einer großen Codebasis zu senken. Sie haben eine besonders fortgeschrittene Form, "SOLID Glückskrankheit". SOLID ist nicht das Ziel. Das Ziel ist es, die Kosten für die Aufrechterhaltung der Codebasis zu senken. Bewerten Sie Ihre Vorschläge in diesem Zusammenhang und nicht, ob es sich um doctrinaire SOLID handelt. (Dass Ihre Vorschläge wahrscheinlich auch nicht wirklich doctrinaire SOLID sind, ist auch ein guter Punkt zu berücksichtigen.)
Eric Lippert

Antworten:


104

Um eine einfache Anwendung zum Speichern von Dateien zu erstellen, müssen Sie mit einer Klasse prüfen, ob die Datei bereits vorhanden ist, mit einer Klasse, mit der die Metadaten geschrieben werden, mit einer Klasse, mit der DateTime abstrahiert werden kann. Jetzt können Sie für jede Datei Schnittstellen zum Testen von Einheiten einfügen Logik, Dateien, die Komponententests für jede Klasse da draußen enthalten, und eine oder mehrere Dateien, um alles zu Ihrem DI-Container hinzuzufügen.

Ich denke, Sie haben die Idee einer einzelnen Verantwortung falsch verstanden. Die einzige Verantwortung einer Klasse könnte darin bestehen, "eine Datei zu speichern". Zu diesem Zweck kann diese Verantwortung in eine Methode zerlegt werden, die prüft, ob eine Datei vorhanden ist, eine Methode, die Metadaten schreibt usw. Jede dieser Methoden hat dann eine einzelne Verantwortung, die Teil der Gesamtverantwortung der Klasse ist.

Eine Klasse zum Abstrahieren DateTime.Nowhört sich gut an. Sie benötigen jedoch nur eines davon, und es kann mit anderen Umgebungsmerkmalen in einer einzigen Klasse zusammengefasst werden, die für die Abstraktion von Umgebungsmerkmalen verantwortlich ist. Wieder eine Einzelverantwortung mit mehreren Teilverantwortungen.

Sie benötigen keine "Schnittstellen für jede Datei, die Logik enthält", sondern Schnittstellen für Klassen, die Nebenwirkungen haben, z. B. Klassen, die in Dateien oder Datenbanken lesen / schreiben. und selbst dann werden sie nur für die öffentlichen Teile dieser Funktionalität benötigt. So AccountRepobenötigen Sie beispielsweise in möglicherweise keine Schnittstellen, sondern nur eine Schnittstelle für den tatsächlichen Datenbankzugriff, der in dieses Repository eingespeist wird.

Unit-Tests waren für das Team ein unglaublich schwerer Verkauf, da sie alle glauben, dass sie Zeitverschwendung sind und dass sie in der Lage sind, ihren Code viel schneller als jedes Teil einzeln zu testen. Unit-Tests als Bestätigung für SOLID zu verwenden, war größtenteils vergeblich und ist zu diesem Zeitpunkt größtenteils zu einem Scherz geworden.

Dies deutet darauf hin, dass Sie auch Unit-Tests missverstanden haben. Die "Einheit" eines Komponententests ist keine Codeeinheit. Was ist überhaupt eine Codeeinheit? Eine Klasse? Eine Methode? Eine Variable? Eine einzelne Maschinenanweisung? Nein, die "Einheit" bezieht sich auf eine Isolationseinheit, dh Code, der isoliert von anderen Teilen des Codes ausgeführt werden kann. Ein einfacher Test, ob ein automatisierter Test ein Komponententest ist oder nicht, besteht darin, ob Sie ihn parallel zu allen anderen Komponententests ausführen können, ohne das Ergebnis zu beeinträchtigen. Es gibt noch ein paar Faustregeln für Unit-Tests, aber das ist Ihr Schlüsselmaß.

Wenn also Teile Ihres Codes tatsächlich als Ganzes getestet werden können, ohne andere Teile zu beeinflussen, dann tun Sie dies.

Seien Sie immer pragmatisch und denken Sie daran, dass alles ein Kompromiss ist. Je mehr Sie sich an DRY halten, desto enger muss Ihr Code verknüpft werden. Je mehr Sie Abstraktionen einführen, desto einfacher ist der Code zu testen, aber desto schwieriger ist es, ihn zu verstehen. Vermeiden Sie Ideologie und finden Sie eine gute Balance zwischen Ideal und Einfachheit. Hier liegt der Sweet Spot für maximale Effizienz sowohl bei der Entwicklung als auch bei der Wartung.


27
Ich möchte hinzufügen, dass ähnliche Kopfschmerzen auftreten, wenn Leute versuchen, sich an das zu oft wiederholte Mantra von "Methoden sollten nur eine Sache tun" zu halten und am Ende Tonnen von einzeiligen Methoden erhalten, nur weil sie technisch zu einer Methode gemacht werden können .
Logarr

8
Zu "Sei immer pragmatisch und denke daran, dass alles ein Kompromiss ist" : Die Jünger von Onkel Bob sind dafür nicht bekannt (unabhängig von der ursprünglichen Absicht).
Peter Mortensen

13
Um den ersten Teil zusammenzufassen: Normalerweise haben Sie einen Kaffee-Praktikanten, nicht eine ganze Reihe von Plug-In-Perkolatoren, Flip-Switches, Check-If-Sugar-Needs-Refilling, Open-Refrigerator, Get-Out-Milk, Get -Out-Löffel, Get-Down-Tassen, Kaffee einschenken, Zucker hinzufügen, Milch hinzufügen, Tasse umrühren und Tasse liefern Praktikanten. ; P
Justin Time 2 Setzen Sie Monica

12
Die Hauptursache für das OP-Problem scheint das Missverständnis des Unterschieds zwischen Funktionen, die eine einzelne Aufgabe ausführen sollten , und Klassen, die eine einzelne Verantwortung tragen sollten, zu sein.
Alephzero

6
"Regeln sind für die Führung der Weisen und den Gehorsam der Narren." - Douglas Bader
Calanus

29

Aufgaben, für die früher 5-10 Dateien benötigt wurden, können jetzt 70-100 dauern!

Dies ist das Gegenteil des Single-Responsibility-Prinzips (SRP). Um zu diesem Punkt zu gelangen, müssen Sie Ihre Funktionalität sehr fein strukturiert aufgeteilt haben, aber darum geht es in der SRP nicht - dabei wird die Schlüsselidee der Kohäsivität ignoriert .

Gemäß der SRP sollte Software in Module unterteilt werden, die nach ihren möglichen Änderungsgründen definiert sind, damit eine einzelne Entwurfsänderung in nur einem Modul angewendet werden kann, ohne dass Änderungen an anderer Stelle erforderlich sind. Ein einzelnes "Modul" in diesem Sinne kann mehr als einer Klasse entsprechen. Wenn Sie bei einer Änderung jedoch Dutzende von Dateien berühren müssen, handelt es sich entweder um mehrere Änderungen, oder Sie machen SRP-Fehler.

Bob Martin, der ursprünglich die SRP formuliert hatte, schrieb vor einigen Jahren einen Blog-Beitrag , um die Situation zu klären. Es wird ausführlich besprochen, was ein "Änderungsgrund" für die Zwecke des SRP ist. Es ist in seiner Gesamtheit eine Lektüre wert, aber unter den Dingen, die besondere Aufmerksamkeit verdienen, ist diese alternative Formulierung des SRP:

Sammeln Sie die Dinge, die sich aus den gleichen Gründen ändern . Trennen Sie die Dinge, die sich aus verschiedenen Gründen ändern.

(Hervorhebung von mir). Bei der SRP geht es nicht darum, Dinge in möglichst kleine Teile aufzuteilen. Das ist kein gutes Design, und Ihr Team ist zu Recht dagegen. Es erschwert die Aktualisierung und Pflege Ihrer Codebasis. Es hört sich so an, als ob Sie versuchen würden, Ihr Team auf der Grundlage von Unit-Tests zu verkaufen, aber das würde bedeuten, den Karren vor das Pferd zu stellen.

In ähnlicher Weise sollte das Prinzip der Grenzflächentrennung nicht als absolut angesehen werden. Es ist nicht mehr ein Grund, Ihren Code so fein zu unterteilen, als es die SRP ist, und sie stimmt im Allgemeinen ziemlich gut mit der SRP überein. Dass eine Schnittstelle einige Methoden enthält , die einige Clients nicht verwenden, ist kein Grund, sie aufzulösen. Sie suchen wieder nach Zusammenhalt.

Darüber hinaus fordere ich Sie auf, das Open-Closed-Prinzip oder das Liskov-Substitutionsprinzip nicht als Grund für die Bevorzugung tiefer Vererbungshierarchien heranzuziehen. Es gibt keine engere Kopplung als eine Unterklasse mit ihren Oberklassen, und die enge Kopplung ist ein Konstruktionsproblem. Bevorzugen Sie stattdessen die Komposition gegenüber der Vererbung, wo immer dies sinnvoll ist. Dies reduziert Ihre Kopplung und damit die Anzahl der Dateien, die eine bestimmte Änderung möglicherweise berühren muss, und passt gut zur Abhängigkeitsinversion.


1
Ich versuche nur herauszufinden, wo die Linie ist. In einer kürzlich durchgeführten Aufgabe musste ich eine relativ einfache Operation ausführen, die sich jedoch in einer Codebasis befand, in der es weder ein Gerüst noch Funktionen gab. Daher war alles, was ich tun musste, sehr einfach, aber alles ziemlich einzigartig und schien nicht in gemeinsame Klassen zu passen. In meinem Fall musste ich ein Dokument auf einem Netzlaufwerk speichern und es in zwei separaten Datenbanktabellen protokollieren. Die Regeln für jeden Schritt waren ziemlich genau. Sogar die Generierung der Dateinamen (eine einfache Anleitung) enthielt einige Klassen, um das Testen bequemer zu machen.
JD Davis

3
Wiederum stellt @JDDavis die Auswahl mehrerer Klassen anstelle einer einzigen Klasse nur zu Testzwecken vor das Pferd und widerspricht direkt der SRP, die die Gruppierung zusammenhängender Funktionen erfordert. Ich kann Sie nicht über Einzelheiten beraten, aber das Problem, dass einzelne funktionale Änderungen das Ändern vieler Dateien erfordern, sollten Sie ansprechen (und versuchen, dies zu vermeiden), und nicht eines, das Sie zu rechtfertigen versuchen sollten.
John Bollinger

Ich stimme zu und füge dies hinzu. Um Wikipedia zu zitieren: "Martin definiert eine Verantwortung als Grund für eine Änderung und kommt zu dem Schluss, dass eine Klasse oder ein Modul nur einen Grund für eine Änderung haben sollte (dh umgeschrieben werden sollte)." und "er hat in jüngerer Zeit festgestellt, dass es bei diesem Prinzip um Menschen geht." "Ich glaube, dass dies bedeutet, dass sich die" Verantwortung "in SRP auf Interessengruppen bezieht, nicht auf die Funktionalität. Eine Klasse sollte für Änderungen verantwortlich sein, die nur von einem Stakeholder verlangt werden (Person, die Änderungen an Ihrem Programm verlangt), damit Sie als Reaktion auf die unterschiedlichen Stakeholder, die Änderungen fordern, so wenige Dinge wie möglich ändern.
Corrodias

12

Aufgaben, für die früher 5-10 Dateien benötigt wurden, können jetzt 70-100 dauern!

Das ist eine Lüge. Die Aufgaben dauerten nie nur 5-10 Dateien.

Sie lösen keine Aufgaben mit weniger als 10 Dateien. Warum? Weil Sie C # verwenden. C # ist eine Hochsprache. Sie verwenden mehr als 10 Dateien, nur um Hallo Welt zu erstellen.

Oh sicher, dass Sie sie nicht bemerken, weil Sie sie nicht geschrieben haben. Also schaust du nicht in sie hinein. Du vertraust ihnen.

Das Problem ist nicht die Anzahl der Dateien. Es ist so, dass jetzt so viel los ist, dass du nicht vertraust.

Stellen Sie also fest, wie diese Tests so funktionieren, dass Sie diesen Dateien genau so vertrauen, wie Sie den Dateien in .NET vertrauen, wenn sie bestanden wurden. Dies zu tun ist der Punkt des Unit-Tests. Niemand kümmert sich um die Anzahl der Dateien. Sie kümmern sich um die Anzahl der Dinge, denen sie nicht vertrauen können.

Für kleine bis mittlere Anwendungen ist SOLID ein supereinfaches Produkt. Jeder sieht den Vorteil und die einfache Wartbarkeit. Sie sehen jedoch gerade in sehr großen Anwendungen kein gutes Preis-Leistungs-Verhältnis für SOLID.

Änderungen sind bei sehr umfangreichen Anwendungen schwierig, ganz gleich, was Sie tun. Die beste Weisheit, die man hier anwenden kann, kommt nicht von Onkel Bob. Es stammt von Michael Feathers in seinem Buch Working Effectively with Legacy Code.

Starten Sie kein Rewrite-Fest. Der alte Code steht für hart erarbeitetes Wissen. Wenn Sie es wegwerfen, weil es Probleme hat und nicht in einem neuen und verbesserten Paradigma zum Ausdruck kommt, fordert X lediglich eine Reihe neuer Probleme und kein hart erarbeitetes Wissen.

Suchen Sie stattdessen nach Möglichkeiten, Ihren alten nicht testbaren Code testbar zu machen (Legacy-Code in Feathers). In dieser Metapher ist Code wie ein Hemd. Große Teile werden an natürlichen Nähten zusammengefügt, die rückgängig gemacht werden können, um den Code so zu trennen, wie Sie die Nähte entfernen möchten. Tun Sie dies, damit Sie Testhüllen anbringen können, mit denen Sie den Rest des Codes isolieren können. Wenn Sie nun die Testhüllen erstellen, haben Sie Vertrauen in die Hüllen, da Sie dies mit einem funktionierenden Hemd getan haben. (Nun, diese Metapher fängt an zu schmerzen).

Diese Idee basiert auf der Annahme, dass, wie in den meisten Geschäften, die einzigen aktuellen Anforderungen im Arbeitscode enthalten sind. Auf diese Weise können Sie dies in Tests sperren, mit denen Sie Änderungen am bewährten Arbeitscode vornehmen können, ohne dass dieser den bewährten Arbeitsstatus verliert. Mit dieser ersten Testwelle können Sie nun Änderungen vornehmen, die den (nicht testbaren) "Legacy" -Code testbar machen. Sie können mutig sein, weil die Nahtprüfungen Sie unterstützen, indem sie sagen, dass dies immer so war, und die neuen Tests zeigen, dass Ihr Code tatsächlich das tut, was Sie denken, dass es funktioniert.

Was hat das alles zu tun mit:

Verwalten und organisieren Sie die massiv gestiegene Anzahl von Klassen nach dem Wechsel zu SOLID?

Abstraktion.

Sie können mich dazu bringen, jede Codebasis mit schlechten Abstraktionen zu hassen. Eine schlechte Abstraktion lässt mich nach innen schauen. Überrasche mich nicht, wenn ich nach innen schaue. Sei so ziemlich das, was ich erwartet hatte.

Geben Sie mir einen guten Namen, lesbare Tests (Beispiele), die zeigen, wie die Benutzeroberfläche verwendet wird, und organisieren Sie sie so, dass ich Dinge finden kann, und es ist mir egal, ob wir 10, 100 oder 1000 Dateien verwendet haben.

Sie helfen mir, Dinge mit aussagekräftigen Namen zu finden. Setzen Sie Dinge mit guten Namen in Dinge mit guten Namen.

Wenn Sie dies alles richtig machen, werden Sie die Dateien dahin abstrahieren, wo Sie zum Beenden einer Aufgabe nur abhängig von 3 bis 5 anderen Dateien sind. Die 70-100 Dateien sind noch da. Aber sie verstecken sich hinter den 3 bis 5. Das funktioniert nur, wenn Sie den 3 bis 5 vertrauen, dass sie das richtig machen.

Was Sie also wirklich brauchen, ist das Vokabular, um gute Namen für all diese Dinge und Tests zu finden, denen die Leute vertrauen, damit sie nicht mehr durch alles waten. Ohne das würdest du mich auch verrückt machen.

@Delioth macht einen guten Punkt über wachsende Schmerzen. Wenn Sie daran gewöhnt sind, dass sich das Geschirr im Schrank über der Spülmaschine befindet, müssen Sie sich erst daran gewöhnen, dass es sich über der Frühstücksbar befindet. Erschwert einige Dinge. Erleichtert einige Dinge. Aber es verursacht alle Arten von Albträumen, wenn die Leute sich nicht einig sind, wohin das Geschirr geht. Bei einer großen Codebasis besteht das Problem darin, dass Sie nur einige der Gerichte gleichzeitig verschieben können. So, jetzt haben Sie Geschirr an zwei Orten. Es ist verwirrend. Macht es schwer zu vertrauen, dass die Gerichte dort sind, wo sie sein sollen. Wenn Sie daran vorbeikommen möchten, müssen Sie nur das Geschirr in Bewegung halten.

Das Problem dabei ist, dass Sie wirklich gerne wissen möchten, ob es sich lohnt, das Geschirr über der Frühstücksbar zu haben, bevor Sie diesen ganzen Unsinn durchgehen. Nun, dafür kann ich nur Camping empfehlen.

Wenn Sie ein neues Paradigma zum ersten Mal ausprobieren, sollten Sie es zuletzt in einer großen Codebasis anwenden. Dies gilt für jedes Mitglied des Teams. Niemand sollte darauf vertrauen, dass SOLID funktioniert, dass OOP funktioniert oder dass funktionale Programmierung funktioniert. Jedes Teammitglied sollte die Möglichkeit haben, in einem Spielzeugprojekt mit der neuen Idee zu spielen. Damit können sie zumindest sehen, wie es funktioniert. Es lässt sie sehen, was es nicht gut macht. So können sie lernen, es richtig zu machen, bevor sie ein großes Chaos anrichten.

Wenn Sie den Menschen einen sicheren Ort zum Spielen bieten, können Sie neue Ideen einbringen und ihnen die Zuversicht geben, dass die Gerichte in ihrem neuen Zuhause wirklich funktionieren können.


3
Es kann erwähnenswert sein, dass einige der Schmerzen in der Frage wahrscheinlich auch nur zugenommen haben - obwohl sie möglicherweise 15 Dateien für diese eine Sache erstellen müssen ... Jetzt müssen sie nie wieder einen GUIDProvider oder einen BasePathProvider schreiben , oder ein ExtensionProvider, etc. Es ist die gleiche Hürde, die Sie bekommen, wenn Sie ein neues Greenfield-Projekt starten - eine Menge unterstützender Funktionen, die meistens trivial sind, dumm geschrieben werden und dennoch noch geschrieben werden müssen. Es ist scheiße, sie zu bauen, aber wenn sie erst einmal da sind, sollte man nicht mehr an sie denken müssen.
Delioth

@Delioth Ich bin unglaublich geneigt zu glauben, dass dies der Fall ist. Wenn wir zuvor eine Teilmenge der Funktionalität benötigten (sagen wir, wir wollten einfach eine URL in AppSettings), hatten wir einfach eine massive Klasse, die herumgereicht und verwendet wurde. Mit dem neuen Ansatz gibt es keinen Grund, die gesamte AppSettingsURL oder den Dateipfad abzurufen.
JD Davis

1
Starten Sie kein Rewrite-Fest. Der alte Code steht für hart erarbeitetes Wissen. Wenn Sie es wegwerfen, weil es Probleme hat und nicht in einem neuen und verbesserten Paradigma zum Ausdruck kommt, fordert X lediglich eine Reihe neuer Probleme und kein hart erarbeitetes Wissen. Diese. Absolut.
Flot2011

10

Es hört sich so an, als ob Ihr Code nicht sehr gut entkoppelt ist und / oder Ihre Aufgaben viel zu groß sind.

Code - Änderungen sollten 5-10 Dateien sein , wenn Sie ein oder codemod großen Maßstab Refactoring zu tun. Wenn eine einzelne Änderung viele Dateien berührt, bedeutet dies wahrscheinlich, dass Ihre Änderungen überlappen. Einige verbesserte Abstraktionen (mehr Einzelverantwortung, Schnittstellentrennung, Abhängigkeitsinversion) sollten helfen. Es ist auch möglich, dass Sie zu eigenverantwortlich geworden sind und etwas mehr Pragmatismus vertragen - kürzere und dünnere Hierarchien. Das sollte auch das Verständnis des Codes erleichtern, da Sie nicht Dutzende von Dateien verstehen müssen, um zu wissen, was der Code tut.

Es könnte auch ein Zeichen dafür sein, dass Ihre Arbeit zu groß ist. Anstelle von "Hey, füge diese Funktion hinzu" (für die Änderungen an der Benutzeroberfläche und an der API sowie Änderungen am Datenzugriff und an der Sicherheit sowie Änderungen am Test erforderlich sind und ...). Dies ist leichter zu überprüfen und zu verstehen, da Sie angemessene Verträge zwischen den Bits einrichten müssen.

Und natürlich helfen Unit-Tests dabei. Sie zwingen Sie zu anständigen Schnittstellen. Sie zwingen Sie dazu, Ihren Code flexibel genug zu gestalten, um die zum Testen erforderlichen Bits einzufügen (wenn es schwierig ist zu testen, ist es schwierig, ihn wiederzuverwenden). Und sie schieben die Leute von übermäßigem Engineering ab, denn je mehr Sie konstruieren, desto mehr müssen Sie testen.


2
Die 5-10 Dateien zu 70-100 Dateien ist etwas mehr als hypothetisch. Meine letzte Aufgabe bestand darin, einige Funktionen in einem unserer neueren Microservices zu erstellen. Der neue Dienst sollte eine Anfrage erhalten und ein Dokument speichern. Dabei benötigte ich Klassen, um die Benutzerentitäten in jeweils 2 separaten Datenbanken und Repos darzustellen. Repos, um andere Tabellen darzustellen, in die ich schreiben musste. Spezielle Klassen für die Prüfung von Dateidaten und die Generierung von Namen. Und die Liste geht weiter. Ganz zu schweigen davon, dass jede Klasse, die Logik enthielt, durch eine Schnittstelle dargestellt wurde, damit sie für Unit-Tests nachgebildet werden konnte.
JD Davis

1
Soweit es unsere älteren Codebasen betrifft, sind sie alle eng miteinander verbunden und unglaublich monolithisch. Beim SOLID-Ansatz bestand die einzige Kopplung zwischen Klassen im Fall von POCOs, alles andere wird über DI und Schnittstellen übertragen.
JD Davis

3
@JDDavis - warte, warum arbeitet ein Mikrodienst direkt mit mehreren Datenbanken zusammen?
Telastyn

1
Es war ein Kompromiss mit unserem Entwickler-Manager. Er bevorzugt massiv monolithische und prozedurale Software. Daher sind unsere Microservices viel makroökonomischer als sie sein sollten. Je besser unsere Infrastruktur wird, desto langsamer werden die Dinge in ihre eigenen Microservices übergehen. Im Moment folgen wir dem Strangler-Ansatz, um bestimmte Funktionen in Microservices umzuwandeln. Da mehrere Dienste Zugriff auf eine bestimmte Ressource benötigen, verschieben wir diese auch in ihre eigenen Microservices.
JD Davis

4

Ich möchte auf einige der hier bereits erwähnten Dinge eingehen, aber mehr aus der Perspektive, wo Objektgrenzen gezogen werden. Wenn Sie einem domänenbasierten Design folgen, werden Ihre Objekte wahrscheinlich Aspekte Ihres Geschäfts darstellen. Customerund Orderwäre zum Beispiel Objekte. Wenn ich nun auf der Grundlage der Klassennamen, die Sie als Ausgangspunkt hatten, eine Vermutung AccountLogicanstellen würde, hätte Ihre Klasse Code, der für jedes Konto ausgeführt werden würde. In OO soll jedoch jede Klasse Kontext und Identität haben. Sie sollten kein AccountObjekt abrufen und es dann an eine AccountLogicKlasse übergeben und diese Klasse Änderungen am AccountObjekt vornehmen lassen . Dies wird als anämisches Modell bezeichnet und repräsentiert OO nicht sehr gut. Stattdessen deinAccountKlasse sollte Verhalten haben, wie Account.Close()oder Account.UpdateEmail(), und dieses Verhalten würde nur diese Instanz des Kontos betreffen.

Nun kann (und sollte in vielen Fällen) die Behandlung dieser Verhaltensweisen auf Abhängigkeiten verlagert werden, die durch Abstraktionen (dh Schnittstellen) dargestellt werden. Account.UpdateEmailMöglicherweise möchten Sie beispielsweise eine Datenbank oder eine Datei aktualisieren oder eine Nachricht an einen Servicebus usw. senden. Dies kann sich in Zukunft ändern. So kann Ihre AccountKlasse beispielsweise von IEmailUpdateeiner der vielen Schnittstellen abhängig sein , die von einem AccountRepositoryObjekt implementiert werden . Sie möchten keine ganze IAccountRepositorySchnittstelle an das AccountObjekt übergeben, da dies wahrscheinlich zu viel bewirken würde, z. B. das Suchen und Finden anderer (beliebiger) Konten, auf die das AccountObjekt möglicherweise keinen Zugriff haben soll, obwohl AccountRepositorymöglicherweise beide implementiert werden IAccountRepositoryund IEmailUpdateSchnittstellen, dieAccountObjekt hätte nur Zugriff auf die kleinen Teile, die es benötigt. Auf diese Weise können Sie das Prinzip der Schnittstellentrennung aufrechterhalten .

Wie bereits von anderen erwähnt, ist es realistisch, dass Sie bei einer Klassenexplosion das SOLID-Prinzip (und damit auch OO) falsch anwenden. SOLID soll Ihnen helfen, Ihren Code zu vereinfachen, nicht zu komplizieren. Aber es braucht Zeit, um wirklich zu verstehen, was Dinge wie die SRP bedeuten. Das Wichtigste ist jedoch, dass die Funktionsweise von SOLID sehr stark von Ihrer Domäne und den begrenzten Kontexten abhängt (ein weiterer DDD-Begriff). Es gibt keine Wunderwaffe oder Einheitsgröße.

Eine weitere Sache, die ich den Menschen, mit denen ich zusammenarbeite, besonders hervorheben möchte: Auch hier sollte ein OOP-Objekt Verhalten aufweisen und wird in der Tat durch sein Verhalten und nicht durch seine Daten definiert. Wenn Ihr Objekt nur Eigenschaften und Felder enthält, weist es immer noch Verhalten auf, wahrscheinlich jedoch nicht das von Ihnen beabsichtigte Verhalten. Eine öffentlich beschreibbare / einstellbare Eigenschaft ohne andere festgelegte Logik impliziert, dass das Verhalten für ihre enthaltende Klasse darin besteht, dass jeder an einem beliebigen Ort aus irgendeinem Grund und zu einem beliebigen Zeitpunkt den Wert dieser Eigenschaft ändern kann, ohne dass dazwischen eine Geschäftslogik oder Validierung erforderlich ist. Dies ist normalerweise nicht das Verhalten, das die Leute beabsichtigen, aber wenn Sie ein anämisches Modell haben, ist dies im Allgemeinen das Verhalten, das Ihre Klassen jedem mitteilen, der sie verwendet.


2

Das sind also insgesamt 15 Klassen (ohne POCOs und Gerüste), um ein relativ einfaches Speichern durchzuführen.

Das ist verrückt ... aber diese Kurse klingen wie etwas, das ich selbst geschrieben habe. Schauen wir sie uns an. Lassen Sie uns die Schnittstellen und Tests zunächst ignorieren.

  • BasePathProvider- IMHO benötigt es jedes nicht-triviale Projekt, das mit Dateien arbeitet. Also würde ich annehmen, es gibt schon so etwas und du kannst es so verwenden, wie es ist.
  • UniqueFilenameProvider - Sicher hast du es schon, oder?
  • NewGuidProvider - Der gleiche Fall, es sei denn, Sie möchten nur GUID verwenden.
  • FileExtensionCombiner - Der gleiche Fall.
  • PatientFileWriter - Ich denke, dies ist die Hauptklasse für die aktuelle Aufgabe.

Für mich sieht es gut aus: Sie müssen eine neue Klasse schreiben, die vier Hilfsklassen benötigt. Alle vier Helferklassen klingen ziemlich wiederverwendbar, ich wette, sie befinden sich bereits irgendwo in Ihrer Codebasis. Ansonsten ist es entweder Pech (sind Sie wirklich die Person in Ihrem Team, die Dateien schreibt und GUIDs verwendet ???) oder ein anderes Problem.


Wenn Sie eine neue Klasse erstellen oder aktualisieren, sollten Sie die Testklassen testen. Wenn Sie also fünf Klassen schreiben, müssen Sie auch fünf Testklassen schreiben. Das Design wird dadurch aber nicht komplizierter:

  • Sie werden die Testklassen niemals an anderer Stelle verwenden, da sie automatisch ausgeführt werden und das ist alles.
  • Sie möchten sie immer wieder betrachten, es sei denn, Sie aktualisieren die getesteten Klassen oder verwenden sie als Dokumentation (im Idealfall zeigen die Tests eindeutig, wie eine Klasse verwendet werden soll).

In Bezug auf die Schnittstellen werden sie nur benötigt, wenn entweder Ihr DI-Framework oder Ihr Test-Framework keine Klassen verarbeiten kann. Sie können sie als Abgabe für unvollständige Werkzeuge ansehen. Oder Sie sehen sie als nützliche Abstraktion, die es Ihnen ermöglicht, zu vergessen, dass es kompliziertere Dinge gibt - das Lesen der Quelle einer Schnittstelle dauert viel kürzer als das Lesen der Quelle ihrer Implementierung.


Ich bin dankbar für diesen Standpunkt. In diesem speziellen Fall habe ich Funktionen in einen relativ neuen Microservice geschrieben. Leider ist selbst in unserer Hauptcodebasis, obwohl wir einige der oben genannten verwenden, keines davon wirklich auf eine remote wiederverwendbare Art und Weise. Alles, was wiederverwendbar sein muss, endete in einer statischen Klasse oder es wird nur kopiert und um den Code eingefügt. Ich glaube, ich bin noch ein bisschen weit gegangen, aber ich stimme zu, dass nicht alles vollständig zerlegt und entkoppelt werden muss.
JD Davis

@JDDavis Ich habe versucht, etwas anderes zu schreiben als die anderen Antworten (denen ich größtenteils zustimme). Wann immer Sie etwas kopieren und einfügen, verhindern Sie die Wiederverwendung, da Sie statt eines allgemeinen Codes ein weiteres nicht wiederverwendbares Stück Code erstellen, das Sie dazu zwingt, eines Tages mehr Code zu kopieren und einzufügen. IMHO ist es die zweitgrößte Sünde, kurz nachdem blind Regeln befolgt wurden. Sie müssen Ihren Sweet Spot finden, wo das Befolgen von Regeln Sie produktiver macht (insbesondere in Bezug auf zukünftige Änderungen) und gelegentlich ein wenig hilft, wenn der Aufwand nicht unangemessen wäre. Alles ist relativ.
Maaartinus

@JDDavis Und alles hängt von der Qualität Ihrer Werkzeuge ab. Beispiel: Es gibt Leute, die behaupten, DI sei unternehmerisch und kompliziert, während ich behaupte, es sei größtenteils kostenlos . +++Was das Brechen der Regeln angeht: Es gibt vier Klassen, die ich an Stellen brauche, an denen ich sie erst nach einer umfassenden Überarbeitung einschleusen konnte, wodurch der Code hässlicher wurde (zumindest für meine Augen), und ich beschloss, sie zu Singletons zu machen (einem besseren Programmierer) vielleicht einen besseren Weg finden, aber ich bin zufrieden damit; die Anzahl dieser Singletons hat sich seit Ewigkeiten nicht geändert).
Maaartinus

Diese Antwort drückt ziemlich genau das aus, was ich dachte, als das OP der Frage das Beispiel hinzufügte. @JDDavis Lassen Sie mich hinzufügen, dass Sie einige Boilerplate-Codes / -Klassen speichern können, indem Sie funktionale Tools für die einfachen Fälle verwenden. Ein GUI-Provider zum Beispiel - anstatt eine neue Schnittstelle in eine neue Klasse einzufügen, warum nicht einfach dafür verwenden Func<Guid>und eine anonyme Methode wie ()=>Guid.NewGuid()in den Konstruktor einfügen ? Sie müssen diese .NET Framework-Funktion nicht testen. Microsoft hat dies für Sie erledigt. Insgesamt sparen Sie 4 Klassen.
Doc Brown

... und Sie sollten prüfen, ob die anderen Fälle, die Sie vorgestellt haben, auf dieselbe Weise vereinfacht werden können (wahrscheinlich nicht alle).
Doc Brown

2

Abhängig von Abstraktionen sind das Erstellen von Klassen mit einer Verantwortung und das Schreiben von Einheitentests keine exakten Wissenschaften. Es ist völlig normal, beim Lernen zu weit in eine Richtung zu schwingen, zu extrem zu werden und dann eine sinnvolle Norm zu finden. Es hört sich einfach so an, als wäre Ihr Pendel zu weit geschwungen und könnte sogar stecken bleiben.

Hier ist, wo ich vermute, dass dies aus der Bahn gerät:

Unit-Tests waren für das Team ein unglaublich schwerer Verkauf, da sie alle glauben, dass sie Zeitverschwendung sind und dass sie in der Lage sind, ihren Code viel schneller als jedes Teil einzeln zu testen. Unit-Tests als Bestätigung für SOLID zu verwenden, war größtenteils vergeblich und ist zu diesem Zeitpunkt größtenteils zu einem Scherz geworden.

Einer der Vorteile der meisten SOLID-Prinzipien (sicherlich nicht der einzige) besteht darin, dass das Schreiben von Komponententests für unseren Code erleichtert wird. Wenn eine Klasse von Abstraktionen abhängt, können wir die Abstraktionen verspotten. Getrennte Abstraktionen sind leichter zu verspotten. Wenn eine Klasse eine Sache tut, ist ihre Komplexität wahrscheinlich geringer, was bedeutet, dass es einfacher ist, alle möglichen Pfade zu kennen und zu testen.

Wenn Ihr Team keine Komponententests schreibt, passieren zwei verwandte Dinge:

Erstens machen sie viel zusätzliche Arbeit, um all diese Schnittstellen und Klassen zu erstellen, ohne die vollen Vorteile zu realisieren. Es braucht ein wenig Zeit und Übung, um zu sehen, wie das Schreiben von Komponententests unser Leben erleichtert. Es gibt Gründe, warum Leute, die lernen, Komponententests zu schreiben, daran festhalten, aber Sie müssen lange genug durchhalten, um sie selbst zu entdecken. Wenn Ihr Team das nicht versucht, wird es das Gefühl haben, dass der Rest der zusätzlichen Arbeit, die es leistet, nutzlos ist.

Was passiert zum Beispiel, wenn sie umgestalten müssen? Wenn sie hundert kleine Klassen haben, aber keine Tests, um festzustellen, ob ihre Änderungen funktionieren oder nicht, werden diese zusätzlichen Klassen und Schnittstellen wie eine Bürde und keine Verbesserung erscheinen.

Zweitens können Sie durch Schreiben von Komponententests nachvollziehen, wie viel Abstraktion Ihr Code wirklich benötigt. Wie ich schon sagte, es ist keine Wissenschaft. Wir fangen schlecht an, drehen überall herum und werden besser. Unit-Tests ergänzen SOLID auf besondere Weise. Woher wissen Sie, wann Sie eine Abstraktion hinzufügen oder etwas aufteilen müssen? Mit anderen Worten, woher weißt du, wann du "SOLID genug" bist? Oft lautet die Antwort, wenn Sie etwas nicht testen können.

Möglicherweise kann Ihr Code getestet werden, ohne dass so viele kleine Abstraktionen und Klassen erstellt werden. Aber wenn Sie die Tests nicht schreiben, wie können Sie das beurteilen? Wie weit gehen wir? Wir können davon besessen sein, Dinge immer kleiner aufzubrechen. Es ist ein Kaninchenbau. Die Möglichkeit, Tests für unseren Code zu schreiben, hilft uns zu erkennen, wann wir unseren Zweck erfüllt haben, sodass wir nicht mehr besessen sind, weitermachen und Spaß daran haben, mehr Code zu schreiben.

Unit-Tests sind keine Wunderwaffe, die alles löst, aber sie sind eine wirklich großartige Kugel, die das Leben der Entwickler verbessert. Wir sind nicht perfekt und unsere Tests auch nicht. Aber Tests geben uns Vertrauen. Wir erwarten, dass unser Code richtig ist, und wir sind überrascht, wenn er falsch ist, nicht umgekehrt. Wir sind nicht perfekt und unsere Tests auch nicht. Aber wenn unser Code getestet wird, haben wir Vertrauen. Es ist weniger wahrscheinlich, dass wir uns die Nägel beißen, wenn unser Code bereitgestellt wird, und uns fragen, was diesmal kaputt gehen wird und ob es unsere Schuld sein wird.

Darüber hinaus beschleunigt das Schreiben von Unit-Tests die Entwicklung von Code, sobald wir den Dreh raus haben, und nicht mehr langsamer. Wir verbringen weniger Zeit damit, alten Code zu überarbeiten oder Fehler zu beheben, um Probleme zu finden, die wie Nadeln im Heuhaufen sind.

Fehler nehmen ab, wir erledigen mehr und wir ersetzen Angst durch Zuversicht. Es ist keine Modeerscheinung oder Schlangenöl. Es ist echt. Viele Entwickler werden das bestätigen. Wenn Ihr Team dies noch nicht erlebt hat, muss es diese Lernkurve überwinden und den Buckel überwinden. Geben Sie ihm eine Chance und stellen Sie fest, dass sie nicht sofort Ergebnisse erzielen. Aber wenn es passiert, werden sie froh sein und niemals zurückblicken. (Oder sie werden zu isolierten Parias und schreiben wütende Blogposts darüber, wie Zeitverschwendung Unit-Tests und die meisten anderen angesammelten Programmierkenntnisse sind.)

Seit dem Wechsel ist eine der größten Beschwerden der Entwickler, dass sie es nicht ertragen können, Dutzende und Dutzende von Dateien zu überprüfen und zu durchlaufen, bei denen es zuvor für jede Aufgabe nur erforderlich war, dass der Entwickler 5 bis 10 Dateien berührt.

Peer Review ist viel einfacher, wenn alle Komponententests bestanden wurden und ein großer Teil dieser Überprüfung nur darauf abzielt, sicherzustellen, dass die Tests aussagekräftig sind.

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.