Angenommen, Sie haben eine Anwendung mit einer weit verbreiteten Klasse namens User
. Diese Klasse enthüllt alle Informationen über den Benutzer, seine ID, seinen Namen, die Zugriffsebenen auf die einzelnen Module, die Zeitzone usw.
Die Benutzerdaten sind natürlich systemweit referenziert, aber aus welchem Grund auch immer, das System ist so eingerichtet, dass wir nicht dieses Benutzerobjekt in davon abhängige Klassen übergeben, sondern nur einzelne Eigenschaften davon übergeben.
Eine Klasse, die die Benutzer-ID benötigt, benötigt einfach die GUID userId
als Parameter. Manchmal benötigen wir auch den Benutzernamen, damit dieser als separater Parameter übergeben wird. In einigen Fällen wird dies an einzelne Methoden übergeben, sodass die Werte überhaupt nicht auf Klassenebene gespeichert werden.
Jedes Mal, wenn ich Zugriff auf eine andere Information aus der Benutzerklasse benötige, muss ich Änderungen vornehmen, indem ich Parameter hinzufüge. Wenn das Hinzufügen einer neuen Überladung nicht angemessen ist, muss ich auch jeden Verweis auf die Methode oder den Klassenkonstruktor ändern.
Der Benutzer ist nur ein Beispiel. Dies ist in unserem Code weit verbreitet.
Habe ich Recht, wenn ich denke, dass dies eine Verletzung des Open / Closed-Prinzips ist? Nicht nur die Änderung bestehender Klassen, sondern deren Einrichtung, sodass in Zukunft mit großer Wahrscheinlichkeit umfassende Änderungen erforderlich sein werden?
Wenn wir das User
Objekt nur übergeben , könnte ich eine kleine Änderung an der Klasse vornehmen, mit der ich arbeite. Wenn ich einen Parameter hinzufügen muss, muss ich möglicherweise Dutzende von Änderungen an Verweisen auf die Klasse vornehmen.
Werden andere Prinzipien durch diese Praxis verletzt? Abhängigkeitsinversion vielleicht? Obwohl wir nicht auf eine Abstraktion verweisen, gibt es nur einen Benutzertyp, so dass keine echte Benutzeroberfläche erforderlich ist.
Werden andere nicht-SOLID-Prinzipien verletzt, wie z. B. grundlegende defensive Programmierungsprinzipien?
Sollte mein Konstruktor so aussehen:
MyConstructor(GUID userid, String username)
Oder dieses:
MyConstructor(User theUser)
Hat es geposted:
Es wurde vorgeschlagen, die Frage unter "Pass ID oder Objekt?" Zu beantworten. Dies beantwortet nicht die Frage, inwiefern die Entscheidung für einen der beiden Wege einen Versuch beeinflusst, den SOLID-Grundsätzen zu folgen, die im Mittelpunkt dieser Frage stehen.
I
inSOLID
?MyConstructor
Grundsätzlich sagt jetzt "Ich brauche einGuid
und einstring
". Warum also nicht eine Schnittstelle haben, die aGuid
und a bereitstellt, diese Schnittstelle implementieren und von einer Instanz abhängenstring
lassen,User
die diese SchnittstelleMyConstructor
implementiert? Und wenn sich die AnforderungenMyConstructor
ändern, ändern Sie die Benutzeroberfläche. - Es hat mir sehr geholfen, über Schnittstellen nachzudenken, die eher dem Verbraucher als dem Anbieter "gehören" . Denken Sie also: "Als Verbraucher brauche ich etwas, das dies und das tut", anstatt "als Anbieter kann ich dies und das tun".Antworten:
Es ist absolut nichts Falsches daran, ein gesamtes
User
Objekt als Parameter zu übergeben. Tatsächlich kann dies helfen, den Code zu verdeutlichen und Programmierern klarer zu machen, was eine Methode benötigt, wenn die Methodensignatur a erfordertUser
.Es ist nett, einfache Datentypen zu übergeben, bis sie etwas anderes bedeuten als das, was sie sind. Betrachten Sie dieses Beispiel:
Und eine Beispielnutzung:
Können Sie den Defekt erkennen? Der Compiler kann nicht. Die "Benutzer-ID", die übergeben wird, ist nur eine Ganzzahl. Wir benennen die Variable
user
, initialisieren aber ihren Wert anhand desblogPostRepository
Objekts, das vermutlichBlogPost
Objekte und nichtUser
Objekte zurückgibt. Der Code wird jedoch kompiliert und Sie erhalten einen Wonky-Laufzeitfehler.Betrachten Sie nun dieses geänderte Beispiel:
Möglicherweise verwendet die
Bar
Methode nur die "Benutzer-ID", für die Methodensignatur ist jedoch einUser
Objekt erforderlich . Kehren wir nun zur vorherigen Beispielverwendung zurück, ändern Sie sie jedoch, um den gesamten "Benutzer" zu übergeben:Jetzt haben wir einen Compilerfehler. Die
blogPostRepository.Find
Methode gibt einBlogPost
Objekt zurück, das wir geschickt als "Benutzer" bezeichnen. Dann übergeben wir diesen "Benutzer" an dieBar
Methode und erhalten umgehend einen Compilerfehler, da wir a nichtBlogPost
an eine Methode übergeben können, die a akzeptiertUser
.Das Typensystem der Sprache wird genutzt, um korrekten Code schneller zu schreiben und Fehler zur Kompilierungszeit und nicht zur Laufzeit zu identifizieren.
Es ist nur ein Symptom für andere Probleme, eine Menge Code umgestalten zu müssen, weil sich Benutzerinformationen ändern. Übergeben Sie ein gesamtes
User
Objekt, erhalten Sie die oben genannten Vorteile, zusätzlich zu den Vorteilen, dass Sie nicht alle Methodensignaturen umgestalten müssen, die Benutzerinformationen akzeptieren, wenn sich etwas an derUser
Klasse ändert.quelle
Nein, es ist keine Verletzung dieses Prinzips. Bei diesem Prinzip geht es darum, sich nicht
User
in einer Weise zu ändern , die sich auf andere Teile des Codes auswirkt, die ihn verwenden. Ihre Änderungen anUser
könnten einen solchen Verstoß darstellen, haben aber nichts damit zu tun.Nein. Was Sie beschreiben - nur die erforderlichen Teile eines Benutzerobjekts in jede Methode zu injizieren - ist das Gegenteil: Es ist eine reine Abhängigkeitsinversion.
Nein. Dieser Ansatz ist eine absolut gültige Art der Codierung. Es verstößt nicht gegen solche Prinzipien.
Aber die Abhängigkeitsinversion ist nur ein Prinzip; Es ist kein unzerbrechliches Gesetz. Und reine DI kann das System komplexer machen. Wenn Sie feststellen, dass nur das Einfügen der erforderlichen Benutzerwerte in Methoden, anstatt das gesamte Benutzerobjekt in die Methode oder den Konstruktor zu übergeben, Probleme verursacht, tun Sie dies nicht auf diese Weise. Es geht darum, ein Gleichgewicht zwischen Prinzipien und Pragmatismus zu finden.
So adressieren Sie Ihren Kommentar:
Ein Teil des Problems hier ist, dass Sie diesen Ansatz eindeutig nicht mögen, wie aus dem Kommentar "unnötig [bestanden] ..." hervorgeht. Und das ist fair genug; Hier gibt es keine richtige Antwort. Wenn Sie es als lästig empfinden, tun Sie es nicht so.
Wenn Sie sich jedoch strikt an das Open / Closed-Prinzip halten, ist "... alle Verweise auf alle fünf vorhandenen Methoden ändern ..." ein Hinweis darauf, dass diese Methoden geändert wurden, wann sie geändert werden sollten Änderungen vorbehalten. In der Realität ist das Open / Closed-Prinzip für öffentliche APIs zwar sinnvoll, für die Interna einer App jedoch wenig sinnvoll.
Aber dann wandern Sie in YAGNI-Territorium und es wäre immer noch orthogonal zum Prinzip. Wenn Sie eine Methode haben
Foo
, die einen Benutzernamen hat, und dann auch einFoo
Geburtsdatum haben möchten , fügen Sie nach dem Prinzip eine neue Methode hinzu.Foo
bleibt unverändert. Auch dies ist eine gute Vorgehensweise für öffentliche APIs, aber für internen Code ein Unsinn.Wie bereits erwähnt, geht es in jeder Situation um Ausgewogenheit und gesunden Menschenverstand. Wenn sich diese Parameter häufig ändern, verwenden Sie sie
User
direkt. Dies erspart Ihnen umfangreiche Änderungen, die Sie beschreiben. Aber wenn sie sich nicht oft ändern, ist es auch ein guter Ansatz, nur das zu übergeben, was benötigt wird.quelle
User
Instanz übergeben und dieses Objekt abfragen, um einen Parameter zu erhalten, werden die Abhängigkeiten nur teilweise invertiert. Es gibt immer noch Fragen. Wahre Abhängigkeitsinversion ist 100% "sagen, nicht fragen". Aber es kommt zu einem Komplexitätspreis.Ja, das Ändern einer vorhandenen Funktion verstößt gegen das Open / Closed-Prinzip. Sie ändern etwas, das aufgrund geänderter Anforderungen nicht geändert werden darf. Ein besseres Design (um sich nicht zu ändern, wenn sich die Anforderungen ändern) wäre, den Benutzer an Dinge weiterzuleiten, die für Benutzer funktionieren sollten .
Aber das könnte afoul der Schnittstelle Segregations Prinzip laufen, da man entlang vorbei sein könnte Art und Weise mehr Informationen als die Funktion Bedürfnisse seiner Arbeit zu tun.
Also, wie bei den meisten Dingen - es kommt darauf an .
Wenn Sie nur einen Benutzernamen verwenden, wird die Funktion flexibler und Sie können mit Benutzernamen arbeiten, unabhängig davon, woher sie kommen, und ohne ein voll funktionsfähiges Benutzerobjekt zu erstellen. Es bietet Ausfallsicherheit für Änderungen, wenn Sie glauben, dass sich die Datenquelle ändern wird.
Durch die Verwendung des gesamten Benutzers wird die Verwendung klarer und der Vertrag mit den Anrufern wird fester. Es bietet Flexibilität bei Änderungen, wenn Sie der Meinung sind, dass mehr Benutzer benötigt werden.
quelle
User.find()
. In der Tat sollte es nicht einmal sein einUser.find
. Es sollte niemals in der Verantwortung von liegen, einen Benutzer zu findenUser
.User
mit der Funktion. Vielleicht macht das Sinn. Aber vielleicht sollte sich die Funktion nur um den Namen des Benutzers kümmern - und die Weitergabe von Daten wie dem Beitrittsdatum oder der Adresse des Benutzers ist nicht korrekt.User
Klasse nicht haben sollte. Es kann eineUserRepository
oder ähnliche geben, die sich mit solchen Dingen befasst.Dieser Entwurf folgt dem Parameterobjektmuster . Es behebt Probleme, die durch viele Parameter in der Methodensignatur entstehen.
Die Anwendung dieses Musters aktiviert das Open / Close-Prinzip (OCP). Beispielsweise können abgeleitete Klassen von
User
als Parameter bereitgestellt werden, die ein anderes Verhalten in der konsumierenden Klasse hervorrufen.Es kann passieren Lassen Sie mich anhand der SOLID-Prinzipien erklären.
Das Prinzip der einmaligen Verantwortung ( Single Responsibility Principle, SRP) kann verletzt werden, wenn es das von Ihnen erläuterte Design aufweist:
Das Problem ist mit allen Informationen . Wenn die
User
Klasse viele Eigenschaften hat, wird sie zu einem riesigen Datenübertragungsobjekt, das nicht zusammenhängende Informationen aus der Sicht der konsumierenden Klassen transportiert. Beispiel: Aus Sicht einer konsumierenden Klasse sindUserAuthentication
die EigenschaftenUser.Id
undUser.Name
relevant, aber nichtUser.Timezone
.Das Prinzip der Schnittstellentrennung (ISP) wird ebenfalls mit einer ähnlichen Begründung verletzt, fügt jedoch eine andere Perspektive hinzu. Beispiel: Angenommen, für eine konsumierende Klasse
UserManagement
muss die EigenschaftUser.Name
aufgeteilt werden,User.LastName
undUser.FirstName
die KlasseUserAuthentication
muss auch dafür geändert werden.Glücklicherweise gibt Ihnen ISP auch einen Ausweg aus dem Problem: Normalerweise beginnen solche Parameterobjekte oder Datentransportobjekte klein und wachsen mit der Zeit. Wenn dies unhandlich wird, erwägen Sie den folgenden Ansatz: Stellen Sie Schnittstellen bereit, die auf die Bedürfnisse der konsumierenden Klassen zugeschnitten sind. Beispiel: Stellen Sie Interfaces vor und lassen Sie die
User
Klasse davon ableiten:Jede Schnittstelle sollte eine Teilmenge verwandter Eigenschaften der
User
Klasse verfügbar machen, die eine konsumierende Klasse benötigt, um ihre Operation zu erfüllen. Suchen Sie nach Cluster von Eigenschaften. Versuchen Sie, die Schnittstellen wiederzuverwenden. Im Falle der konsumierenden KlasseUserAuthentication
verwenden SieIUserAuthenticationInfo
stattUser
. Teilen Sie dieUser
Klasse dann nach Möglichkeit in mehrere konkrete Klassen auf, indem Sie die Schnittstellen als "Schablone" verwenden.quelle
Wenn ich mit diesem Problem in meinem eigenen Code konfrontiert werde, bin ich zu dem Schluss gekommen, dass grundlegende Modellklassen / -objekte die Antwort sind.
Ein häufiges Beispiel wäre das Repository-Muster. Wenn Sie eine Datenbank über Repositorys abfragen, verwenden viele der Methoden im Repository häufig dieselben Parameter.
Meine Faustregeln für Repositories sind:
Wenn mehrere Methoden dieselben zwei oder mehr Parameter verwenden, sollten die Parameter zu einem Modellobjekt zusammengefasst werden.
Wenn eine Methode mehr als 2 Parameter akzeptiert, sollten die Parameter zu einem Modellobjekt zusammengefasst werden.
Modelle können von einer gemeinsamen Basis erben, aber nur dann, wenn dies wirklich sinnvoll ist (normalerweise ist es besser, eine Umgestaltung später vorzunehmen, als die Vererbung im Auge zu behalten).
Die Probleme bei der Verwendung von Modellen aus anderen Ebenen / Bereichen werden erst sichtbar, wenn das Projekt etwas komplexer wird. Nur dann werden Sie feststellen, dass weniger Code mehr Arbeit oder mehr Komplikationen verursacht.
Und ja, es ist völlig in Ordnung, zwei verschiedene Modelle mit identischen Eigenschaften zu haben, die verschiedenen Ebenen / Zwecken dienen (z. B. ViewModels vs POCOs).
quelle
Schauen wir uns nur die einzelnen Aspekte von SOLID an:
Eine Sache, die Designinstinkte verwirrt, ist, dass die Klasse im Wesentlichen für globale Objekte und im Wesentlichen schreibgeschützt ist. In einer solchen Situation schadet es nicht sehr, Abstraktionen zu verletzen: Nur das Lesen von Daten, die nicht geändert wurden, erzeugt eine ziemlich schwache Kopplung. erst wenn es zu einem riesigen Haufen wird, macht sich der Schmerz bemerkbar.
Nehmen Sie einfach an, dass das Objekt nicht sehr global ist, um die Designinstinkte wiederherzustellen. Welchen Kontext würde eine Funktion benötigen, wenn das
User
Objekt jederzeit mutiert werden könnte? Welche Komponenten des Objekts würden wahrscheinlich zusammen mutiert sein? Diese können herausgesplittet werdenUser
, ob als referenziertes Unterobjekt oder als Schnittstelle, die nur ein "Stück" verwandter Felder anzeigt, ist nicht so wichtig.Ein weiteres Prinzip: Sehen Sie sich die Funktionen an, die Teile von verwenden
User
und sehen Sie, welche Felder (Attribute) zusammenpassen. Das ist eine gute vorläufige Liste von Unterobjekten - Sie müssen sich auf jeden Fall überlegen, ob sie tatsächlich zusammengehören.Es ist viel Arbeit und etwas schwierig, und Ihr Code wird etwas weniger flexibel, da es etwas schwieriger wird, das Teilobjekt (die Teilschnittstelle) zu identifizieren, das an eine Funktion übergeben werden muss, insbesondere wenn sich die Teilobjekte überschneiden.
Das Aufteilen
User
wird tatsächlich hässlich, wenn sich die Unterobjekte überlappen, und die Leute werden verwirrt sein, welches ausgewählt werden soll, wenn alle erforderlichen Felder aus der Überlappung stammen. Wenn Sie sich hierarchisch aufteilen (z. B.UserMarketSegment
welcheUserLocation
), sind sich die Leute nicht sicher, auf welcher Ebene sich die von ihnen geschriebene Funktion befindet: Handelt es sich um Benutzerdaten auf derLocation
Ebene oder auf derMarketSegment
Ebene? Es hilft nicht gerade, dass sich dies im Laufe der Zeit ändern kann, dh, Sie sind wieder mit dem Ändern von Funktionssignaturen beschäftigt, manchmal über eine gesamte Aufrufkette hinweg.Mit anderen Worten: Wenn Sie Ihre Domäne nicht wirklich kennen und keine genaue Vorstellung davon haben, welches Modul mit welchen Aspekten zu tun hat
User
, lohnt es sich nicht, die Programmstruktur zu verbessern.quelle
Das ist eine wirklich interessante Frage. Es kommt darauf an.
Wenn Sie der Meinung sind, dass sich Ihre Methode in Zukunft möglicherweise intern ändert, um andere Parameter des User-Objekts zu erfordern, sollten Sie auf jeden Fall das Ganze weitergeben. Der Vorteil besteht darin, dass der Code außerhalb der Methode vor Änderungen innerhalb der Methode in Bezug auf die verwendeten Parameter geschützt ist, was, wie Sie sagen, eine Kaskade von externen Änderungen zur Folge hätte. Durch die Weitergabe des gesamten Benutzers wird die Kapselung erhöht.
Wenn Sie sich ziemlich sicher sind, dass Sie niemals etwas anderes als die E-Mail des Benutzers verwenden müssen, sollten Sie dies weitergeben. Der Vorteil ist, dass Sie die Methode dann in einem breiteren Bereich von Kontexten verwenden können: Sie könnten sie zum Beispiel verwenden mit der E-Mail eines Unternehmens oder mit einer E-Mail, die gerade eingegeben wurde. Dies erhöht die Flexibilität.
Dies ist Teil einer breiteren Palette von Fragen zum Erstellen von Klassen, um einen weiten oder engen Bereich zu haben, einschließlich der Frage, ob Abhängigkeiten eingefügt werden sollen oder nicht und ob global verfügbare Objekte vorhanden sein sollen. Im Moment gibt es eine unglückliche Tendenz zu denken, dass ein engerer Anwendungsbereich immer gut ist. Es gibt jedoch immer einen Kompromiss zwischen Kapselung und Flexibilität, wie in diesem Fall.
quelle
Ich finde es am besten, so wenige Parameter wie möglich und so viele wie nötig zu übergeben. Dies erleichtert das Testen und erfordert keine Kistenbildung ganzer Objekte.
Wenn Sie in Ihrem Beispiel nur die Benutzer-ID oder den Benutzernamen verwenden, ist dies alles, was Sie übergeben sollten. Wenn sich dieses Muster mehrmals wiederholt und das tatsächliche Benutzerobjekt viel größer ist, ist mein Rat, eine kleinere Schnittstelle (n) dafür zu erstellen. Es könnte sein
oder
Dies erleichtert das Testen mit Spott erheblich und Sie wissen sofort, welche Werte wirklich verwendet werden. Andernfalls müssen Sie häufig komplexe Objekte mit vielen anderen Abhängigkeiten initialisieren, obwohl Sie am Ende nur ein oder zwei Eigenschaften benötigen.
quelle
Folgendes ist mir von Zeit zu Zeit begegnet:
User
(oderProduct
was auch immer), das viele Eigenschaften hat, obwohl die Methode nur einige davon verwendet.User
Objekt vorhanden ist. Es erstellt eine Instanz und initialisiert nur die Eigenschaften, die die Methode tatsächlich benötigt.User
Argument stoßen, müssen Sie nach einer Weile nach Aufrufen dieser Methode suchen, um herauszufinden, woher dieUser
stammt, damit Sie wissen, welche Eigenschaften ausgefüllt sind. Handelt es sich um einen "echten" Benutzer mit einer E-Mail-Adresse, oder wurde er nur erstellt, um eine Benutzer-ID und einige Berechtigungen zu übergeben?Wenn Sie eine erstellen
User
und nur einige Eigenschaften ausfüllen, weil dies die Eigenschaften sind, die die Methode benötigt, weiß der Aufrufer wirklich mehr über die Funktionsweise der Methode, als er sollte.Noch schlimmer ist , wenn Sie haben eine Instanz
User
, müssen Sie wissen , woher es gekommen ist, damit Sie wissen , welche Eigenschaften bestückt sind. Das willst du nicht wissen müssen.Wenn Entwickler die
User
Verwendung als Container für Methodenargumente sehen, können sie im Laufe der Zeit beginnen, ihr Eigenschaften für Einmalszenarien hinzuzufügen. Jetzt wird es hässlich, weil die Klasse mit Eigenschaften überfüllt ist, die fast immer null oder default sind.Eine solche Beschädigung ist nicht unvermeidlich, aber sie tritt immer wieder auf, wenn wir ein Objekt herumreichen, nur weil wir Zugriff auf einige seiner Eigenschaften benötigen. In der Gefahrenzone sieht man zum ersten Mal jemanden, der eine Instanz erstellt
User
und nur einige Eigenschaften ausfüllt, damit er sie an eine Methode übergeben kann. Setz deinen Fuß darauf, denn es ist ein dunkler Pfad.Wenn möglich, geben Sie dem nächsten Entwickler das richtige Beispiel, indem Sie nur das übergeben, was Sie übergeben müssen.
quelle