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.Now
kö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.
Antworten:
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.Now
hö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
AccountRepo
benötigen Sie beispielsweise in möglicherweise keine Schnittstellen, sondern nur eine Schnittstelle für den tatsächlichen Datenbankzugriff, der in dieses Repository eingespeist wird.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.
quelle
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:
(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.
quelle
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.
Ä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:
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.
quelle
AppSettings
URL oder den Dateipfad abzurufen.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.
quelle
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.
Customer
undOrder
wäre zum Beispiel Objekte. Wenn ich nun auf der Grundlage der Klassennamen, die Sie als Ausgangspunkt hatten, eine VermutungAccountLogic
anstellen 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 keinAccount
Objekt abrufen und es dann an eineAccountLogic
Klasse übergeben und diese Klasse Änderungen amAccount
Objekt vornehmen lassen . Dies wird als anämisches Modell bezeichnet und repräsentiert OO nicht sehr gut. Stattdessen deinAccount
Klasse sollte Verhalten haben, wieAccount.Close()
oderAccount.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.UpdateEmail
Mö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 IhreAccount
Klasse beispielsweise vonIEmailUpdate
einer der vielen Schnittstellen abhängig sein , die von einemAccountRepository
Objekt implementiert werden . Sie möchten keine ganzeIAccountRepository
Schnittstelle an dasAccount
Objekt übergeben, da dies wahrscheinlich zu viel bewirken würde, z. B. das Suchen und Finden anderer (beliebiger) Konten, auf die dasAccount
Objekt möglicherweise keinen Zugriff haben soll, obwohlAccountRepository
möglicherweise beide implementiert werdenIAccountRepository
undIEmailUpdate
Schnittstellen, dieAccount
Objekt 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.
quelle
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:
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.
quelle
+++
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).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.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:
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.)
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.
quelle