Senden Sie im Allgemeinen Objekte oder deren Mitgliedsvariablen in Funktionen?


31

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?


5
Es würde ganz davon abhängen, wozu die Funktion dient und wie sie sich auf das betreffende Objekt bezieht (oder nicht bezieht).
MetaFight

Das ist das Problem. Ich kann nicht sagen, wann ich den einen oder anderen benutzen würde. Ich habe das Gefühl, ich könnte den Code jederzeit ändern, um beide Ansätze zu berücksichtigen.
AJJ

1
In API-Begriffen (und überhaupt nicht in Bezug auf die Implementierungen) ist der erstere abstrakt und domänenorientiert (was gut ist), während der letztere nicht ist (was schlecht ist).
Erik Eidt

1
Der erste Ansatz wäre mehr 3-Tier-OO. Dies sollte jedoch umso mehr der Fall sein, als die Wortdatenbank aus der Methode entfernt wird. Es sollte "Store" oder "Persist" sein und entweder Account oder Thing (nicht beides) ausführen. Als Kunde dieser Schicht sollten Sie das Speichermedium nicht kennen. Beim Abrufen eines Kontos müssen Sie jedoch die ID oder eine Kombination von Eigenschaftswerten (keine Feldwerte) übergeben, um das gewünschte Objekt zu identifizieren. Oder / und eine Aufzählungsmethode implementieren, die alle Konten übergibt.
Martin Maat

1
Typischerweise wäre beides falsch (oder eher weniger als optimal). Wie ein Objekt in die Datenbank serialisiert werden soll, sollte eine Eigenschaft (eine Member-Funktion) des Objekts sein, da dies normalerweise direkt von den Member-Variablen des Objekts abhängt. Wenn Sie Mitglieder des Objekts ändern, müssen Sie auch die Serialisierungsmethode ändern. Das funktioniert besser, wenn es Teil des Objekts ist
Tofro

Antworten:


24

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:

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}

vs

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(accountId, dataId, someValue);
}

In der ersten Version übergibt der UI-Code das dataObjekt blind und es liegt am Datenbankcode, die nützlichen Felder daraus zu extrahieren. In der zweiten Version teilt der UI-Code das dataObjekt in nützliche Felder auf, und der Datenbankcode empfängt sie direkt, ohne zu wissen, woher sie stammen. Die wichtigste Implikation ist, dass bei dataeiner Ä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 das dataObjekt enthält. In der Regel ist dies jedoch sehr offensichtlich. Zum Beispiel, wenndataIst eine vom Benutzer bereitgestellte Zeichenfolge wie "20/05/1999", sollte es dem UI-Code überlassen sein, diese Datevor der Weitergabe in einen geeigneten Typ zu konvertieren .


8

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.


5

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.


Warum ändern Sie nicht den Methodennamen in insertAccountIntoDatabase oder übergeben einen anderen Typ? Bei einer bestimmten Anzahl von Argumenten ist es einfach, obj zu verwenden, um den Code zu lesen. In Ihrem Fall würde ich eher darüber nachdenken, ob der Methodenname klar macht, was ich einfügen werde, anstatt wie ich es tun werde.
Laiv

3

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 EmailInviterund diese drei möglichen Implementierungen einer inviteMethode:

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

Das Eingeben einer Stelle, Stringan der Sie ein EmailAddressKennwort eingeben sollten, ist fehlerhaft, da nicht alle Zeichenfolgen E-Mail-Adressen sind. Die EmailAddressKlasse passt semantisch besser zum Verhalten der Methode. Die Weitergabe von a Userist jedoch auch fehlerhaft, denn warum in aller Welt sollte EmailInviterman 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 Stringoder verwenden, intaber 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.


0

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

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

ist ein besser lesbarer Anruf als

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );

kann da nicht zustimmen. Die beiden sind auch nicht. Das Erstellen von zwei neuen Objektinstanzen, nur um sie an eine Methode zu übergeben, ist nicht gut. Ich würde insertIntoDatabase (myAccount, myOtherthing) anstelle einer Ihrer Optionen verwenden.
jwenting

0

Ü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:

public User getUser(String primaryKey) {
  return ...;
}

Zum Zeitpunkt des Aufrufs hat der aufrufende Code noch keinen Verweis auf das Objekt, da der Aufrufpunkt dieser Methode darin besteht, das Objekt abzurufen.


1
"Was passiert, wenn Sie die Felder ändern 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 ...
David Arno,

@ DavidArno der Punkt meiner Antwort ist nicht, dass nichts brechen würde, aber weniger würde brechen. Vergessen Sie auch nicht den ersten Absatz: Unabhängig von den Unterbrechungen sollte der interne Zustand eines Objekts über die Schnittstelle des Objekts abstrahiert werden. Die Weitergabe seines internen Zustands verstößt gegen die OOP-Prinzipien.

0

Unter dem Gesichtspunkt der Wartbarkeit sollten Argumente klar voneinander unterscheidbar sein, vorzugsweise auf Compilerebene.

// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)

// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)

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.


0

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.


0

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:

  • Anrufer, der nicht an eine bestimmte Klasse gebunden ist , kann Werte aus verschiedenen Quellen übergeben
  • Der Objektstatus kann nicht unerwartet innerhalb der Methodenausführung geändert werden .

Pro Objekt Referenz:

  • Klare Schnittstelle , an die die Methode an den Objektreferenztyp gebunden ist, wodurch es schwierig wird, versehentlich nicht verwandte / ungültige Werte zu übergeben
  • Das Umbenennen eines Feldes / Getters erfordert Änderungen bei allen Aufrufen der Methode und nicht nur bei ihrer Implementierung
  • Wenn eine neue Eigenschaft hinzugefügt wird und übergeben werden muss, sind keine Änderungen an der Methodensignatur erforderlich
  • Methode kann Objektstatus ändern
  • Wenn zu viele Variablen mit ähnlichen primitiven Typen übergeben werden, wird der Aufrufer in Bezug auf die Reihenfolge verwirrt (Builder-Musterproblem).

Was und wann verwendet werden muss, hängt stark von den Anwendungsfällen ab

  1. Einzelne Parameter übergeben: Wenn die Methode nichts mit dem Objekttyp zu tun hat , ist es im Allgemeinen besser, die Liste der einzelnen Parameter zu übergeben, damit sie für ein breiteres Publikum anwendbar ist.
  2. Neues Modellobjekt einführen: Wenn die Liste der Parameter größer wird (mehr als 3), ist es besser, ein neues Modellobjekt einzuführen, das zur aufgerufenen API gehört (Builder-Muster bevorzugt).
  3. Objektreferenz übergeben: Wenn sich die Methode auf die Domänenobjekte bezieht, ist es aus Sicht der Wartbarkeit und Lesbarkeit besser, die Objektreferenzen zu übergeben.

0

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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.