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.