Entwerfen einer Klasse, um ganze Klassen als Parameter und nicht einzelne Eigenschaften zu verwenden

30

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 userIdals 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 UserObjekt 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.

Jimbo
quelle
11
@gnat: Dies ist definitiv kein Duplikat. Bei dem möglichen Duplikat handelt es sich um eine Methodenverkettung, um tief in eine Objekthierarchie einzudringen. Diese Frage scheint überhaupt nicht danach zu fragen.
Greg Burghardt
2
Die zweite Form wird häufig verwendet, wenn die Anzahl der übergebenen Parameter unhandlich geworden ist.
Robert Harvey
12
Das einzige, was mir an der ersten Signatur nicht gefällt, ist, dass es keine Garantie dafür gibt, dass die Benutzer-ID und der Benutzername tatsächlich vom selben Benutzer stammen. Dies ist ein potenzieller Fehler, der vermieden wird, indem Benutzer überall herumgereicht werden. Aber die Entscheidung hängt wirklich davon ab, was die aufgerufenen Methoden mit den Argumenten machen.
17 von 26
9
Das Wort "analysieren" macht in dem Kontext, in dem Sie es verwenden, keinen Sinn. Meinten Sie stattdessen „pass“?
Konrad Rudolph
5
Was ist mit dem Iin SOLID? MyConstructorGrundsätzlich sagt jetzt "Ich brauche ein Guidund ein string". Warum also nicht eine Schnittstelle haben, die a Guidund a bereitstellt, diese Schnittstelle implementieren und von einer Instanz abhängen stringlassen, Userdie diese Schnittstelle MyConstructorimplementiert? Und wenn sich die Anforderungen MyConstructorä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".
Corak

Antworten:

31

Es ist absolut nichts Falsches daran, ein gesamtes UserObjekt 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 erfordert User.

Es ist nett, einfache Datentypen zu übergeben, bis sie etwas anderes bedeuten als das, was sie sind. Betrachten Sie dieses Beispiel:

public class Foo
{
    public void Bar(int userId)
    {
        // ...
    }
}

Und eine Beispielnutzung:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user.Id);

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 des blogPostRepositoryObjekts, das vermutlich BlogPostObjekte und nicht UserObjekte zurückgibt. Der Code wird jedoch kompiliert und Sie erhalten einen Wonky-Laufzeitfehler.

Betrachten Sie nun dieses geänderte Beispiel:

public class Foo
{
    public void Bar(User user)
    {
        // ...
    }
}

Möglicherweise verwendet die BarMethode nur die "Benutzer-ID", für die Methodensignatur ist jedoch ein UserObjekt erforderlich . Kehren wir nun zur vorherigen Beispielverwendung zurück, ändern Sie sie jedoch, um den gesamten "Benutzer" zu übergeben:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user);

Jetzt haben wir einen Compilerfehler. Die blogPostRepository.FindMethode gibt ein BlogPostObjekt zurück, das wir geschickt als "Benutzer" bezeichnen. Dann übergeben wir diesen "Benutzer" an die BarMethode und erhalten umgehend einen Compilerfehler, da wir a nicht BlogPostan eine Methode übergeben können, die a akzeptiert User.

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 UserObjekt, 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 der UserKlasse ändert.

Greg Burghardt
quelle
6
Ich würde sagen, dass Ihre Argumentation für sich genommen tatsächlich auf vorbeifahrende Felder hinweist, die Felder jedoch den wahren Wert einhüllen. In diesem Beispiel hat ein Benutzer ein Feld vom Typ Benutzer-ID und eine Benutzer-ID ein einzelnes Feld mit einem ganzzahligen Wert. Die Deklaration von Bar sagt Ihnen jetzt sofort, dass Bar nicht alle Informationen über den Benutzer verwendet, sondern nur seine ID. Sie können jedoch keine dummen Fehler wie die Übergabe einer Ganzzahl machen, die nicht aus einer Benutzer-ID in Bar stammt.
Ian
(Forts.) Natürlich ist diese Art von Programmierstil ziemlich mühsam, insbesondere in einer Sprache, die keine gute syntaktische Unterstützung bietet (Haskell ist zum Beispiel für diesen Stil gut, da Sie nur eine Übereinstimmung mit "UserID id" finden können). .
Ian
5
@ Ian: Ich denke, eine ID in ihren eigenen Typ-Skates zu packen, um das ursprüngliche Problem des OP zu umgehen. Strukturelle Änderungen an der User-Klasse machen es erforderlich, viele Methodensignaturen umzugestalten. Das Übergeben des gesamten Benutzerobjekts behebt dieses Problem.
Greg Burghardt
@ Ian: Obwohl ich ehrlich bin, war ich selbst bei der Arbeit in C # sehr versucht, Ids und die Sortierung in ein Struct zu packen, um ein bisschen mehr Klarheit zu schaffen.
Greg Burghardt
1
"Es ist nichts Falsches daran, einen Zeiger an seiner Stelle herumzureichen." Oder eine Referenz, um alle Probleme mit Zeigern zu vermeiden, auf die Sie stoßen könnten.
Yay295
17

Habe ich Recht, wenn ich denke, dass dies eine Verletzung des Open / Closed-Prinzips ist?

Nein, es ist keine Verletzung dieses Prinzips. Bei diesem Prinzip geht es darum, sich nicht Userin einer Weise zu ändern , die sich auf andere Teile des Codes auswirkt, die ihn verwenden. Ihre Änderungen an Userkönnten einen solchen Verstoß darstellen, haben aber nichts damit zu tun.

Werden andere Prinzipien durch diese Praxis verletzt? Abhängigkeitsinversion perahaps?

Nein. Was Sie beschreiben - nur die erforderlichen Teile eines Benutzerobjekts in jede Methode zu injizieren - ist das Gegenteil: Es ist eine reine Abhängigkeitsinversion.

Werden andere nicht-SOLID-Prinzipien verletzt, wie z. B. grundlegende defensive Programmierungsprinzipien?

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:

Es gibt Probleme, einen neuen Wert unnötigerweise auf fünf Ebenen der Kette zu analysieren und dann alle Verweise auf alle fünf vorhandenen Methoden zu ändern ...

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 ein Plan, sich an diesen Grundsatz zu halten, soweit dies praktikabel ist, würde Strategien zur Verringerung des Bedarfs an künftigen Veränderungen einschließen?

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 ein FooGeburtsdatum haben möchten , fügen Sie nach dem Prinzip eine neue Methode hinzu. Foobleibt 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 Userdirekt. 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.

David Arno
quelle
Es gibt Probleme, wenn Sie einen neuen Wert unnötigerweise auf fünf Ebenen der Kette analysieren und dann alle Verweise auf alle fünf vorhandenen Methoden ändern müssen. Warum sollte das Open / Closed-Prinzip nur für die User-Klasse gelten und nicht auch für die Klasse, die ich gerade bearbeite und die auch von anderen Klassen verwendet wird? Mir ist bewusst, dass es beim Prinzip speziell darum geht, Veränderungen zu vermeiden, aber würde ein Plan, an diesem Prinzip festzuhalten, soweit dies praktikabel ist, Strategien zur Verringerung des Bedarfs an zukünftigen Veränderungen beinhalten?
Jimbo
@ Jimbo, ich habe meine Antwort aktualisiert, um zu versuchen, Ihren Kommentar zu adressieren.
David Arno
Ich schätze Ihren Beitrag. BTW. Nicht einmal Robert C. Martin akzeptiert, dass das Open / Closed-Prinzip eine harte Regel hat. Es ist eine Faustregel, die unweigerlich gebrochen wird. Die Anwendung des Prinzips ist eine Übung, um zu versuchen, es so gut wie möglich zu halten. Deshalb habe ich früher das Wort "praktikabel" verwendet.
Jimbo
Es ist keine Abhängigkeitsumkehrung, die Parameter von User und nicht von User selbst zu übergeben.
James Ellis-Jones
@ JamesEllis-Jones, Abhängigkeitsinvertierung kehrt die Abhängigkeiten von "fragen" zu "sagen" um. Wenn Sie eine UserInstanz ü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.
David Arno
10

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.

Telastyn
quelle
+1, aber ich bin mir nicht sicher über Ihre Formulierung "Sie könnten weitere Informationen weitergeben". Wenn Sie (Benutzer der Benutzer) übergeben, übergeben Sie das Minimum an Informationen, einen Verweis auf ein Objekt. Zwar kann die Referenz verwendet werden, um weitere Informationen abzurufen, der aufrufende Code muss diese jedoch nicht abrufen. Wenn Sie übergeben (GUID userid, string username), kann die aufgerufene Methode immer User.find (userid) aufrufen, um die öffentliche Schnittstelle des Objekts zu finden, sodass Sie nichts wirklich verbergen.
Dcorking
5
@dcorking, " Wenn Sie (User theuser) übergeben, übergeben Sie das Minimum an Informationen, einen Verweis auf ein Objekt ". Sie übergeben die maximale Information, die sich auf dieses Objekt bezieht: das gesamte Objekt. " Die aufgerufene Methode kann immer User.find (userid) aufrufen ...". In einem gut konzipierten System wäre dies nicht möglich, da die betreffende Methode keinen Zugriff darauf hätte User.find(). In der Tat sollte es nicht einmal sein ein User.find. Es sollte niemals in der Verantwortung von liegen, einen Benutzer zu finden User.
David Arno
2
@dcorking - enh. Dass Sie eine Referenz übergeben, die zufällig klein ist, ist ein technischer Zufall. Sie koppeln das Ganze Usermit 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.
Telastyn
@ DavidArno vielleicht ist das der Schlüssel für eine klare Antwort auf OP. Wessen Verantwortung sollte es sein, einen Benutzer zu finden? Gibt es eine Bezeichnung für das Konstruktionsprinzip, den Sucher / die Fabrik von der Klasse zu trennen?
Dcorking
1
@dcorking Ich würde sagen, das ist eine Implikation des Single Responsibility Prinzips. Zu wissen, wo Benutzer gespeichert sind und wie sie anhand ihrer ID abgerufen werden können, ist eine separate Aufgabe, die eine UserKlasse nicht haben sollte. Es kann eine UserRepositoryoder ähnliche geben, die sich mit solchen Dingen befasst.
Hulk
3

Dieser Entwurf folgt dem Parameterobjektmuster . Es behebt Probleme, die durch viele Parameter in der Methodensignatur entstehen.

Habe ich Recht, wenn ich denke, dass dies eine Verletzung des Open / Closed-Prinzips ist?

Die Anwendung dieses Musters aktiviert das Open / Close-Prinzip (OCP). Beispielsweise können abgeleitete Klassen von Userals Parameter bereitgestellt werden, die ein anderes Verhalten in der konsumierenden Klasse hervorrufen.

Werden andere Prinzipien durch diese Praxis verletzt?

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:

Diese Klasse enthüllt alle Informationen über den Benutzer, seine ID, seinen Namen, die Zugriffsebenen auf die einzelnen Module, die Zeitzone usw.

Das Problem ist mit allen Informationen . Wenn die UserKlasse 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 sind UserAuthenticationdie Eigenschaften User.Idund User.Namerelevant, aber nicht User.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 UserManagementmuss die Eigenschaft User.Nameaufgeteilt werden, User.LastNameund User.FirstNamedie Klasse UserAuthenticationmuss 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 UserKlasse davon ableiten:

class User : IUserAuthenticationInfo, IUserLocationInfo { ... }

Jede Schnittstelle sollte eine Teilmenge verwandter Eigenschaften der UserKlasse 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 Klasse UserAuthenticationverwenden Sie IUserAuthenticationInfostattUser . Teilen Sie die UserKlasse dann nach Möglichkeit in mehrere konkrete Klassen auf, indem Sie die Schnittstellen als "Schablone" verwenden.

Theo Lenndorff
quelle
1
Sobald der Benutzer kompliziert wird, kommt es zu einer kombinatorischen Explosion möglicher Subschnittstellen. Wenn der Benutzer beispielsweise nur 3 Eigenschaften hat, gibt es 7 mögliche Kombinationen. Ihr Vorschlag klingt nett, ist aber nicht umsetzbar.
user949300
1. Analytisch haben Sie recht. Abhängig davon, wie die Domäne modelliert wird, neigen jedoch Bits verwandter Informationen dazu, sich zu gruppieren. Es ist also praktisch nicht notwendig, alle möglichen Kombinationen von Schnittstellen und Eigenschaften zu behandeln. 2. Der skizzierte Ansatz war nicht als universelle Lösung gedacht, aber vielleicht sollte ich der Antwort noch etwas mehr "Mögliches" und "Können" hinzufügen.
Theo Lenndorff
2

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).

Dom
quelle
2

Schauen wir uns nur die einzelnen Aspekte von SOLID an:

  • Einzelverantwortung: möglicherweise bestraft, wenn die Leute dazu neigen, nur Teile der Klasse zu durchlaufen.
  • Offen / Geschlossen: Egal, wo Abschnitte der Klasse herumgereicht werden, nur dort, wo das gesamte Objekt herumgereicht wird. (Ich denke, hier setzt die kognitive Dissonanz ein: Sie müssen den Ferncode ändern, aber die Klasse selbst scheint in Ordnung zu sein.)
  • Liskov-Substitution: Keine Ausgabe, wir machen keine Unterklassen.
  • Abhängigkeitsinversion (abhängig von Abstraktionen, nicht von konkreten Daten). Ja, das ist verletzt: Die Leute haben keine Abstraktionen, sie nehmen konkrete Elemente der Klasse heraus und geben sie weiter. Ich denke, das ist das Hauptproblem hier.

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 UserObjekt 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 Userwird 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. UserMarketSegmentwelche UserLocation), sind sich die Leute nicht sicher, auf welcher Ebene sich die von ihnen geschriebene Funktion befindet: Handelt es sich um Benutzerdaten auf der Location Ebene oder auf der MarketSegmentEbene? 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.

Werkzeugschmied
quelle
1

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.

James Ellis-Jones
quelle
1

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

interface IIdentifieable
{
    Guid ID { get; }
}

oder

interface INameable
{
    string Name { get; }
}

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.

t3chb0t
quelle
1

Folgendes ist mir von Zeit zu Zeit begegnet:

  • Eine Methode akzeptiert ein Argument vom Typ User(oder Productwas auch immer), das viele Eigenschaften hat, obwohl die Methode nur einige davon verwendet.
  • Aus irgendeinem Grund muss ein Teil des Codes diese Methode aufrufen, obwohl kein vollständig gefülltes UserObjekt vorhanden ist. Es erstellt eine Instanz und initialisiert nur die Eigenschaften, die die Methode tatsächlich benötigt.
  • Dies passiert einige Male.
  • Wenn Sie auf eine Methode mit einem UserArgument stoßen, müssen Sie nach einer Weile nach Aufrufen dieser Methode suchen, um herauszufinden, woher die Userstammt, 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 Userund 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 UserVerwendung 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 Userund 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.

Scott Hannen
quelle