Welches ist allgemein akzeptierte Praxis zwischen diesen beiden Fällen:
function insertIntoDatabase(Account account, Otherthing thing) {
database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}
oder
function insertIntoDatabase(long accountId, long thingId, double someValue) {
database.insertMethod(accountId, thingId, someValue);
}
Mit anderen Worten, ist es im Allgemeinen besser, ganze Objekte herumzuleiten oder nur die Felder, die Sie benötigen?
Antworten:
Keiner ist im Allgemeinen besser als der andere. Es ist eine Entscheidung, die Sie von Fall zu Fall treffen müssen.
In der Praxis müssen Sie jedoch entscheiden, auf welcher Ebene der gesamten Programmarchitektur das Objekt in Grundelemente aufgeteilt werden soll , wenn Sie in der Lage sind, diese Entscheidung tatsächlich zu treffen. Daher sollten Sie über den gesamten Aufruf nachdenken Stack , nicht nur diese eine Methode, in der Sie sich gerade befinden. Vermutlich muss die Trennung irgendwo vorgenommen werden, und es würde keinen Sinn ergeben (oder es wäre unnötig fehleranfällig), dies mehr als einmal zu tun. Die Frage ist, wo dieser eine Ort sein sollte.
Der einfachste Weg, um diese Entscheidung zu treffen, besteht darin, darüber nachzudenken, welcher Code geändert werden sollte oder nicht, wenn das Objekt geändert wird . Lassen Sie uns Ihr Beispiel etwas erweitern:
vs
In der ersten Version übergibt der UI-Code das
data
Objekt blind und es liegt am Datenbankcode, die nützlichen Felder daraus zu extrahieren. In der zweiten Version teilt der UI-Code dasdata
Objekt in nützliche Felder auf, und der Datenbankcode empfängt sie direkt, ohne zu wissen, woher sie stammen. Die wichtigste Implikation ist, dass beidata
einer Änderung der Struktur des Objekts in der ersten Version nur der Datenbankcode geändert werden muss, während in der zweiten Version nur der Benutzeroberflächencode geändert werden muss . Welche der beiden zutreffenden Angaben zutreffen, hängt weitgehend davon ab, welche Art von Daten dasdata
Objekt enthält. In der Regel ist dies jedoch sehr offensichtlich. Zum Beispiel, wenndata
Ist eine vom Benutzer bereitgestellte Zeichenfolge wie "20/05/1999", sollte es dem UI-Code überlassen sein, dieseDate
vor der Weitergabe in einen geeigneten Typ zu konvertieren .quelle
Dies ist keine vollständige Liste, aber berücksichtigen Sie einige der folgenden Faktoren, wenn Sie entscheiden, ob ein Objekt als Argument an eine Methode übergeben werden soll:
Ist das Objekt unveränderlich? Ist die Funktion 'rein'?
Nebenwirkungen sind ein wichtiger Gesichtspunkt für die Wartbarkeit Ihres Codes. Wenn Sie sehen, dass Code mit vielen veränderlichen statusbehafteten Objekten überall herumgereicht wird, ist dieser Code oft weniger intuitiv (so wie globale Statusvariablen oft weniger intuitiv sind), und das Debuggen wird oft schwieriger und zeitintensiver. verzehrend.
Als Faustregel sollten Sie sicherstellen, dass Objekte, die Sie an eine Methode übergeben, so gut wie möglich unveränderlich sind.
Vermeiden Sie (wieder so weit wie möglich) jeden Entwurf, bei dem erwartet wird, dass sich der Zustand eines Arguments infolge eines Funktionsaufrufs ändert. Eines der stärksten Argumente für diesen Ansatz ist das Prinzip des geringsten Erstaunens . Das heißt, jemand, der Ihren Code liest und ein Argument sieht, das an eine Funktion übergeben wird, erwartet mit „geringerer Wahrscheinlichkeit“, dass sich der Status nach der Rückkehr der Funktion ändert.
Wie viele Argumente hat die Methode bereits?
Methoden mit übermäßig langen Argumentlisten (auch wenn die meisten dieser Argumente 'Standard'-Werte haben) sehen aus wie ein Codegeruch. Manchmal sind solche Funktionen jedoch erforderlich, und Sie können eine Klasse erstellen, deren einziger Zweck darin besteht, sich wie ein Parameterobjekt zu verhalten .
Dieser Ansatz kann eine kleine Menge zusätzlicher Boilerplate-Codezuordnungen von Ihrem 'Quell'-Objekt zu Ihrem Parameterobjekt beinhalten, dies ist jedoch sowohl in Bezug auf die Leistung als auch auf die Komplexität recht kostengünstig, und es gibt eine Reihe von Vorteilen in Bezug auf die Entkopplung und Objekt Unveränderlichkeit.
Gehört das übergebene Objekt ausschließlich zu einer "Ebene" in Ihrer Anwendung (z. B. einem ViewModel oder einer ORM-Entität?)
Denken Sie an die Trennung von Bedenken (SoC) . Manchmal können Sie sich fragen, ob das Objekt zu derselben Ebene oder demselben Modul gehört, in der sich Ihre Methode befindet (z. B. eine handgerollte API-Wrapper-Bibliothek oder Ihre Kern-Business-Logic-Ebene usw.), um zu entscheiden, ob das Objekt wirklich an diese Ebene übergeben werden soll Methode.
SoC ist eine gute Grundlage, um sauberen, lose gekoppelten modularen Code zu schreiben. Beispielsweise sollte ein ORM-Entitätsobjekt (Zuordnung zwischen Ihrem Code und Ihrem Datenbankschema) idealerweise nicht in Ihrer Geschäftsschicht oder, noch schlimmer, in Ihrer Präsentations- / UI-Schicht weitergegeben werden.
Wenn Daten zwischen "Ebenen" übertragen werden sollen, ist es in der Regel vorzuziehen, einfache Datenparameter in eine Methode zu übertragen, anstatt ein Objekt aus der "falschen" Ebene zu übertragen. Obwohl es wahrscheinlich eine gute Idee ist, separate Modelle auf der "richtigen" Ebene zu haben, auf die Sie stattdessen abbilden können.
Ist die Funktion selbst einfach zu groß und / oder zu komplex?
Wenn eine Funktion viele Datenelemente benötigt, kann es sinnvoll sein, zu prüfen, ob diese Funktion zu viele Verantwortlichkeiten übernimmt. Suchen Sie nach potenziellen Möglichkeiten zur Umgestaltung mit kleineren Objekten und kürzeren, einfacheren Funktionen.
Soll die Funktion ein Befehls- / Abfrageobjekt sein?
In einigen Fällen kann die Beziehung zwischen den Daten und der Funktion eng sein. Überlegen Sie in diesen Fällen, ob ein Befehlsobjekt oder ein Abfrageobjekt geeignet ist.
Erzwingt das Hinzufügen eines Objektparameters zu einer Methode, dass die übergeordnete Klasse neue Abhängigkeiten übernimmt?
Manchmal ist das stärkste Argument für "Plain old data" -Argumente einfach, dass die empfangende Klasse bereits in sich geschlossen ist, und das Hinzufügen eines Objektparameters zu einer ihrer Methoden würde die Klasse verschmutzen (oder wenn die Klasse bereits verschmutzt ist, wird dies der Fall sein) Verschlechterung der bestehenden Entropie)
Müssen Sie wirklich ein komplettes Objekt weitergeben oder benötigen Sie nur einen kleinen Teil der Oberfläche dieses Objekts?
Berücksichtigen Sie das Prinzip der Schnittstellentrennung in Bezug auf Ihre Funktionen - dh, wenn Sie ein Objekt übergeben, sollte es nur von Teilen der Schnittstelle dieses Arguments abhängen, die es (die Funktion) tatsächlich benötigt.
quelle
Wenn Sie also eine Funktion erstellen, deklarieren Sie implizit einen Vertrag mit Code, der sie aufruft. Msgstr "Diese Funktion nimmt diese Informationen und wandelt sie in eine andere um (möglicherweise mit Nebenwirkungen)".
Sollte Ihr Vertrag also logisch mit den Objekten (wie auch immer sie implementiert sind) oder mit den Feldern sein, die zufällig Teil dieser anderen Objekte sind? Sie fügen die Kopplung in beide Richtungen hinzu, aber als Programmierer müssen Sie entscheiden, wo sie hingehört.
Wenn es unklar ist, ziehen Sie im Allgemeinen die kleinsten Daten vor, die für die Funktion erforderlich sind. Das bedeutet oft, dass nur die Felder übergeben werden müssen, da für die Funktion die anderen Objekte nicht benötigt werden. Manchmal ist es jedoch korrekter, das gesamte Objekt zu nehmen, da es weniger Auswirkungen hat, wenn sich die Dinge in Zukunft zwangsläufig ändern.
quelle
Es hängt davon ab, ob.
Um dies zu erläutern, sollten die von Ihrer Methode akzeptierten Parameter semantisch mit dem übereinstimmen, was Sie tun möchten. Betrachten Sie eine
EmailInviter
und diese drei möglichen Implementierungen einerinvite
Methode:Das Eingeben einer Stelle,
String
an der Sie einEmailAddress
Kennwort eingeben sollten, ist fehlerhaft, da nicht alle Zeichenfolgen E-Mail-Adressen sind. DieEmailAddress
Klasse passt semantisch besser zum Verhalten der Methode. Die Weitergabe von aUser
ist jedoch auch fehlerhaft, denn warum in aller Welt sollteEmailInviter
man sich darauf beschränken, Benutzer einzuladen? Was ist mit Unternehmen? Was passiert, wenn Sie E-Mail-Adressen aus einer Datei oder einer Befehlszeile lesen und diese nicht mit Benutzern verknüpft sind? Mailinglisten? Die Liste geht weiter.Es gibt ein paar Warnschilder, die Sie hier als Richtlinie verwenden können. Wenn Sie einen einfachen Wertetyp wie
String
oder verwenden,int
aber nicht alle Zeichenfolgen oder Ints gültig sind oder etwas "Besonderes" daran ist, sollten Sie einen aussagekräftigeren Typ verwenden. Wenn Sie ein Objekt verwenden und nur einen Getter aufrufen, sollten Sie das Objekt stattdessen direkt in den Getter übergeben. Diese Richtlinien sind weder hart noch schnell, aber es gibt nur wenige Richtlinien.quelle
Clean Code empfiehlt, so wenig Argumente wie möglich zu haben, was bedeutet, dass Object normalerweise der bessere Ansatz ist und ich denke, dass dies einen Sinn ergibt. da
ist ein besser lesbarer Anruf als
quelle
Übergeben Sie das Objekt, nicht seinen konstituierenden Zustand. Dies unterstützt die objektorientierten Prinzipien der Kapselung und des Versteckens von Daten. Das Offenlegen der Innereien eines Objekts in verschiedenen Methodenschnittstellen, in denen dies nicht erforderlich ist, verstößt gegen die wichtigsten OOP-Prinzipien.
Was passiert, wenn Sie die Felder in ändern
Otherthing
? Möglicherweise ändern Sie einen Typ, fügen ein Feld hinzu oder entfernen ein Feld. Jetzt müssen alle Methoden wie die, die Sie in Ihrer Frage erwähnen, aktualisiert werden. Wenn Sie das Objekt umgehen, gibt es keine Schnittstellenänderungen.Das einzige Mal, dass Sie eine Methode schreiben sollten, die Felder für ein Objekt akzeptiert, ist das Schreiben einer Methode zum Abrufen des Objekts:
Zum Zeitpunkt des Aufrufs hat der aufrufende Code noch keinen Verweis auf das Objekt, da der Aufrufpunkt dieser Methode darin besteht, das Objekt abzurufen.
quelle
Otherthing
?" (1) Das wäre eine Verletzung des offenen / geschlossenen Prinzips. (2) Selbst wenn Sie das gesamte Objekt übergeben, greift der darin enthaltene Code auf die Mitglieder des Objekts zu (und wenn nicht, warum übergeben Sie das Objekt?), Und es würde trotzdem kaputt gehen ...Unter dem Gesichtspunkt der Wartbarkeit sollten Argumente klar voneinander unterscheidbar sein, vorzugsweise auf Compilerebene.
Das erste Design führt zur Früherkennung von Fehlern. Das zweite Design kann zu subtilen Laufzeitproblemen führen, die beim Testen nicht auftreten. Daher ist der erste Entwurf vorzuziehen.
quelle
Von den beiden ist meine Präferenz die erste Methode:
function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }
Der Grund dafür ist, dass Änderungen an beiden Objekten auf der Straße vorgenommen werden. Solange die Änderungen diese Getter bewahren, sodass die Änderung außerhalb des Objekts transparent ist, müssen Sie weniger Code ändern und testen und haben weniger Chancen, die App zu stören.
Dies ist nur mein Denkprozess, der hauptsächlich darauf beruht, wie ich solche Dinge gerne bearbeite und strukturiere und was sich auf lange Sicht als ziemlich überschaubar und wartbar herausstellt.
Ich werde nicht auf Namenskonventionen eingehen, sondern darauf hinweisen, dass dieser Speichermechanismus sich später ändern kann, obwohl in dieser Methode das Wort "Datenbank" vorkommt. Aus dem gezeigten Code geht hervor, dass die Funktion nicht an die verwendete Datenbankspeicherplattform gebunden ist - oder auch wenn es sich um eine Datenbank handelt. Wir haben nur angenommen, weil es im Namen ist. Unter der Annahme, dass diese Getter immer erhalten bleiben, wird es wiederum einfach sein, zu ändern, wie / wo diese Objekte gespeichert werden.
Ich würde die Funktion und die beiden Objekte jedoch überdenken, da Sie eine Funktion haben, die von zwei Objektstrukturen abhängig ist, insbesondere von den verwendeten Gettern. Es sieht auch so aus, als ob diese Funktion diese beiden Objekte zu einer kumulativen Sache zusammenfügt, die beibehalten wird. Mein Bauch sagt mir, dass ein dritter Gegenstand Sinn machen könnte. Ich müsste mehr über diese Objekte wissen und wie sie sich auf die Realität und die erwartete Roadmap beziehen. Aber mein Bauch neigt sich in diese Richtung.
Nach dem derzeitigen Stand des Codes lautet die Frage: "Wo würde oder sollte diese Funktion sein?" Gehört es zum Account oder zu OtherThing? Wohin geht es?
Ich denke, es gibt bereits eine dritte Objekt- "Datenbank", und ich neige dazu, diese Funktion in dieses Objekt einzufügen, und dann wird es zu einer Aufgabe dieses Objekts, in der Lage zu sein, mit einem Konto und einem anderen Objekt umzugehen, das Ergebnis umzuwandeln und es dann auch beizubehalten .
Umso besser, wenn Sie das dritte Objekt an ein ORM-Muster (Object Relational Mapping) anpassen. Das würde es für jeden, der mit dem Code arbeitet, sehr offensichtlich machen, zu verstehen, "Ah, hier werden Account und OtherThing zusammengeschlagen und bestehen".
Es könnte aber auch sinnvoll sein, ein viertes Objekt einzuführen, das die Aufgabe des Kombinierens und Transformierens eines Accounts und eines OtherThing übernimmt, jedoch nicht die Mechanismen des Bestehens. Das würde ich tun, wenn Sie mit viel mehr Interaktionen mit oder zwischen diesen beiden Objekten rechnen, da ich dann die Persistenzbits in ein Objekt zerlegen möchte, das nur die Persistenz verwaltet.
Ich würde dafür fotografieren, dass das Design so bleibt, dass das Konto, OtherThing oder das dritte ORM-Objekt geändert werden kann, ohne dass auch die anderen drei geändert werden müssen. Wenn es keinen guten Grund dafür gibt, möchte ich, dass Account und OtherThing unabhängig sind und nicht die inneren Abläufe und Strukturen voneinander kennen.
Wenn ich den vollständigen Kontext wüsste, könnte ich natürlich meine Ideen komplett ändern. Wiederum denke ich so, wenn ich solche Dinge sehe, und wie schlank.
quelle
Beide Ansätze haben ihre eigenen Vor- und Nachteile. Was in einem Szenario besser ist, hängt stark vom jeweiligen Anwendungsfall ab.
Pro Mehrere Parameter, Con Objektreferenz:
Pro Objekt Referenz:
Was und wann verwendet werden muss, hängt stark von den Anwendungsfällen ab
quelle
Auf der einen Seite haben Sie einen Account und ein anderes Objekt. Auf der anderen Seite können Sie einen Wert in eine Datenbank einfügen, wenn Sie die ID eines Kontos und die ID eines anderen Kontos angeben. Das sind die beiden gegebenen Dinge.
Sie können eine Methode schreiben, die Account und Otherthing als Argumente verwendet. Auf der Pro-Seite muss der Anrufer keine Details über Account und andere Dinge wissen. Negativ zu vermerken ist, dass der Angerufene Kenntnisse über die Methoden der Abrechnung und des Sonstigen haben muss. Außerdem gibt es keine Möglichkeit, etwas anderes als den Wert eines anderen Objekts in eine Datenbank einzufügen und diese Methode nicht zu verwenden, wenn Sie die ID eines Kontoobjekts, aber nicht das Objekt selbst haben.
Oder Sie können eine Methode schreiben, die zwei IDs und einen Wert als Argumente verwendet. Auf der negativen Seite muss der Anrufer die Details von Account und Otherthing kennen. Und es kann vorkommen, dass Sie tatsächlich mehr Details eines Kontos oder eines anderen Elements benötigen als nur die ID, die in die Datenbank eingefügt werden soll. In diesem Fall ist diese Lösung völlig nutzlos. Auf der anderen Seite sind hoffentlich keine Kenntnisse über Konto und Sonstiges erforderlich, und es gibt mehr Flexibilität.
Ihr Urteil lautet: Brauchen Sie mehr Flexibilität? Dies ist oft keine Frage eines einzigen Aufrufs, sondern ist in Ihrer gesamten Software konsistent: Entweder verwenden Sie die meiste Zeit IDs von Account oder Sie verwenden die Objekte. Wenn du es mischst, bekommst du das Schlimmste von beiden Welten.
In C ++ können Sie eine Methode haben, die zwei IDs plus Wert verwendet, und eine Inline-Methode, die Account und Otherthing berücksichtigt, sodass Sie beide Möglichkeiten mit Null-Overhead haben.
quelle