Ist das Ändern eines durch Referenz übergebenen Objekts eine schlechte Praxis?

12

In der Vergangenheit habe ich in der Regel den größten Teil meiner Manipulation eines Objekts innerhalb der primären Methode durchgeführt, die erstellt / aktualisiert wird. In letzter Zeit habe ich jedoch einen anderen Ansatz gewählt und bin gespannt, ob dies eine schlechte Praxis ist.

Hier ist ein Beispiel. Angenommen, ich habe ein Repository, das eine UserEntität akzeptiert , aber vor dem Einfügen der Entität rufen wir einige Methoden auf, um sicherzustellen, dass alle Felder auf das eingestellt sind, was wir wollen. Anstatt Methoden aufzurufen und die Feldwerte innerhalb der Insert-Methode festzulegen, rufe ich jetzt eine Reihe von Vorbereitungsmethoden auf, die das Objekt vor dem Einfügen formen.

Alte Methode:

public void InsertUser(User user) {
    user.Username = GenerateUsername(user);
    user.Password = GeneratePassword(user);

    context.Users.Add(user);
}

Neue Methoden:

public void InsertUser(User user) {
    SetUsername(user);
    SetPassword(user);

    context.Users.Add(user);
}

private void SetUsername(User user) {
    var username = "random business logic";

    user.Username = username;
}

private void SetPassword(User user) {
    var password = "more business logic";

    user.Password = password;
}

Ist es grundsätzlich eine schlechte Praxis, den Wert einer Immobilie anhand einer anderen Methode festzulegen?

JD Davis
quelle
6
Nur damit es heißt ... Sie haben hier nichts als Referenz übergeben. Das Übergeben von Referenzen nach Wert ist überhaupt nicht dasselbe.
CHao
4
@cHao: Das ist eine Unterscheidung ohne Unterschied. Das Verhalten des Codes ist unabhängig davon gleich.
Robert Harvey
2
@JDDavis: Grundsätzlich besteht der einzige wirkliche Unterschied zwischen Ihren beiden Beispielen darin, dass Sie dem festgelegten Benutzernamen und den festgelegten Kennwortaktionen einen aussagekräftigen Namen gegeben haben.
Robert Harvey
1
Alle Ihre Anrufe beziehen sich hier. Sie müssen einen Werttyp in C # verwenden, um den Wert zu übergeben.
Frank Hileman
2
@FrankHileman: Alle Aufrufe sind hier nach Wert. Das ist die Standardeinstellung in C #. Vorbei an einer Referenz und vorbei durch Bezugnahme sind verschiedene Tiere und die Unterscheidung ist von Bedeutung. Wenn userder Code als Referenz übergeben würde, könnte er dem Anrufer aus den Händen gezogen und durch einfaches Sagen von beispielsweise ersetzt werden user = null;.
CHao

Antworten:

10

Das Problem hierbei ist, dass a Usertatsächlich zwei verschiedene Dinge enthalten kann:

  1. Eine vollständige Benutzerentität, die an Ihren Datenspeicher übergeben werden kann.

  2. Der Satz von Datenelementen, die vom Aufrufer benötigt werden, um mit dem Erstellen einer Benutzerentität zu beginnen. Das System muss einen Benutzernamen und ein Kennwort hinzufügen, bevor es wirklich ein gültiger Benutzer ist, wie in Nr. 1 oben beschrieben.

Dies umfasst eine undokumentierte Nuance Ihres Objektmodells, die in Ihrem Typsystem überhaupt nicht ausgedrückt wird. Sie müssen es nur als Entwickler "wissen". Das ist nicht großartig und führt zu seltsamen Codemustern wie dem, auf das Sie stoßen.

Ich würde vorschlagen, dass Sie zwei Entitäten benötigen, z. B. eine UserKlasse und eine EnrollRequestKlasse. Letzteres kann alles enthalten, was Sie wissen müssen, um einen Benutzer zu erstellen. Es würde wie Ihre Benutzerklasse aussehen, jedoch ohne den Benutzernamen und das Kennwort. Dann könnten Sie dies tun:

public User InsertUser(EnrollRequest request) {
    var userName = GenerateUserName();
    var password = GeneratePassword();

    //You might want to replace this with a factory call, but "new" works here as an example
    var newUser = new User
    (
        request.Name, 
        request.Email, 
        userName, 
        password
    );
    context.Users.Add(user);
    return newUser;
}

Der Anrufer beginnt nur mit Registrierungsinformationen und erhält den abgeschlossenen Benutzer nach dem Einfügen zurück. Auf diese Weise vermeiden Sie das Mutieren von Klassen und unterscheiden typsicher zwischen einem Benutzer, der eingefügt wird, und einem Benutzer, der nicht eingefügt ist.

John Wu
quelle
2
Eine Methode mit einem Namen wie InsertUserhat wahrscheinlich den Nebeneffekt des Einfügens. Ich bin mir nicht sicher, was "icky" in diesem Zusammenhang bedeutet. Bei der Verwendung eines "Benutzers", der nicht hinzugefügt wurde, scheinen Sie den Punkt verfehlt zu haben. Wenn es nicht hinzugefügt wurde, handelt es sich nicht um einen Benutzer, sondern nur um eine Anforderung zum Erstellen eines Benutzers. Natürlich können Sie mit der Anfrage arbeiten.
John Wu
@candiedorange Das sind keine Nebenwirkungen, sondern die gewünschten Effekte beim Aufrufen der Methode. Wenn Sie nicht sofort hinzufügen möchten, erstellen Sie eine andere Methode, die den Anwendungsfall erfüllt. Dies ist jedoch nicht der Anwendungsfall, der in der Frage behandelt wurde. Sagen wir also, dass Bedenken nicht wichtig sind.
Andy
@candiedorange Wenn die Kopplung den Client-Code einfacher macht, würde ich sagen, dass das in Ordnung ist. Was Entwickler oder Pos in der Zukunft denken könnten, ist irrelevant. In diesem Fall ändern Sie den Code. Nichts ist verschwenderischer als der Versuch, Code zu erstellen, der jeden möglichen zukünftigen Anwendungsfall bewältigen kann.
Andy
5
@CandiedOrange Ich schlage vor, dass Sie versuchen, das genau definierte Typsystem der Implementierung nicht eng mit den konzeptionellen, einfach englischen Einheiten des Unternehmens zu koppeln. Ein Geschäftskonzept von "Benutzer" kann sicherlich in zwei Klassen implementiert werden, wenn nicht mehr, um funktional signifikante Varianten darzustellen. Einem Benutzer, der nicht beibehalten wird, wird kein eindeutiger Benutzername garantiert, er hat keinen Primärschlüssel, an den Transaktionen gebunden werden könnten, und er kann sich nicht einmal anmelden. Daher würde ich sagen, dass es sich um eine signifikante Variante handelt. Für einen Laien sind natürlich sowohl die EnrollRequest als auch der Benutzer "Benutzer" in ihrer vagen Sprache.
John Wu
5

Nebenwirkungen sind in Ordnung, solange sie nicht unerwartet auftreten. Im Allgemeinen ist also nichts falsch, wenn ein Repository eine Methode hat, die einen Benutzer akzeptiert und den internen Status des Benutzers ändert. Aber meiner InsertUser Meinung nach kommuniziert ein Methodenname wie dies nicht klar , und das macht ihn fehleranfällig. Wenn Sie Ihr Repo verwenden, würde ich einen Anruf wie erwarten

 repo.InsertUser(user);

um den internen Status des Repositorys zu ändern, nicht den Status des Benutzerobjekts. Dieses Problem besteht in beiden Implementierungen. Wie InsertUserdies intern funktioniert, ist völlig irrelevant.

Um dies zu lösen, könnten Sie entweder

  • Trennen Sie die Initialisierung vom Einfügen (daher muss der Aufrufer von InsertUserein vollständig initialisiertes UserObjekt bereitstellen , oder

  • Bauen Sie die Initialisierung in den Konstruktionsprozess des Benutzerobjekts ein (wie in einigen anderen Antworten vorgeschlagen), oder

  • Versuchen Sie einfach , einen besseren Namen für die Methode zu finden, der klarer ausdrückt, was sie tut.

So wählen Sie einen Methodennamen wie PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPasswordoder was auch immer Sie bevorzugen die Nebenwirkung mehr deutlicher zu machen.

Natürlich weist ein so langer Methodenname darauf hin, dass die Methode möglicherweise "zu viel" tut (SRP-Verletzung), aber manchmal möchten oder können Sie dies nicht einfach beheben, und dies ist die am wenigsten aufdringliche, pragmatische Lösung.

Doc Brown
quelle
3

Ich schaue mir die beiden Optionen an, die Sie ausgewählt haben, und ich muss sagen, dass ich die alte Methode bei weitem der vorgeschlagenen neuen Methode vorziehe. Es gibt einige Gründe dafür, obwohl sie im Grunde das Gleiche tun.

In beiden Fällen stellen Sie das user.UserNameund ein user.Password. Ich habe Vorbehalte gegen das Passwortelement, aber diese Vorbehalte sind für das jeweilige Thema nicht relevant.

Auswirkungen der Änderung von Referenzobjekten

  • Dies erschwert die gleichzeitige Programmierung - aber nicht alle Anwendungen sind Multithread-Anwendungen
  • Diese Änderungen können überraschend sein, insbesondere wenn nichts an der Methode darauf hindeutet, dass dies passieren würde
  • Diese Überraschungen können die Wartung erschweren

Alte Methode gegen neue Methode

Die alte Methode erleichterte das Testen:

  • GenerateUserName()ist unabhängig testbar. Sie können Tests gegen diese Methode schreiben und sicherstellen, dass die Namen korrekt generiert werden
  • Wenn für den Namen Informationen vom Benutzerobjekt erforderlich sind, können Sie die Signatur ändern GenerateUserName(User user)und diese Testbarkeit beibehalten

Die neue Methode verbirgt die Mutationen:

  • Sie wissen nicht, dass sich das UserObjekt ändert, bis Sie 2 Ebenen tief gehen
  • Die Änderungen am UserObjekt sind in diesem Fall überraschender
  • SetUserName()kann mehr als nur einen Benutzernamen festlegen. Das ist in der Werbung nicht der Fall, was es neuen Entwicklern erschwert, herauszufinden, wie die Dinge in Ihrer Anwendung funktionieren
Berin Loritsch
quelle
1

Ich stimme der Antwort von John Wu voll und ganz zu. Sein Vorschlag ist gut. Aber es fehlt etwas Ihre direkte Frage.

Ist es grundsätzlich eine schlechte Praxis, den Wert einer Immobilie anhand einer anderen Methode festzulegen?

Nicht von Natur aus.

Sie können dies nicht zu weit gehen, da Sie auf unerwartetes Verhalten stoßen. ZB PrintName(myPerson)sollte das Personenobjekt nicht geändert werden, da die Methode impliziert, dass es nur daran interessiert ist , die vorhandenen Werte zu lesen . Dies ist jedoch ein anderes Argument als in Ihrem Fall, da dies SetUsername(user)stark impliziert, dass Werte festgelegt werden.


Dies ist eigentlich ein Ansatz, den ich häufig für Unit- / Integrationstests verwende, bei denen ich eine Methode speziell zum Ändern eines Objekts erstelle, um seine Werte auf eine bestimmte Situation festzulegen, die ich testen möchte.

Beispielsweise:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

Ich erwarte ausdrücklich, dass die ArrrangeContractDeletedStatusMethode den Status des myContractObjekts ändert .

Der Hauptvorteil besteht darin, dass ich mit dieser Methode das Löschen von Verträgen mit verschiedenen Erstverträgen testen kann. zB ein Vertrag mit einem langen Statusverlauf oder ein Vertrag ohne vorherigen Statusverlauf, ein Vertrag mit einem absichtlich fehlerhaften Statusverlauf, ein Vertrag, den mein Testbenutzer nicht löschen darf.

Wenn ich verschmolzen CreateEmptyContractund ArrrangeContractDeletedStatusin einem einzigen Verfahren; Ich müsste mehrere Varianten dieser Methode für jeden Vertrag erstellen, den ich in einem gelöschten Zustand testen möchte.

Und während ich so etwas tun könnte wie:

myContract = ArrrangeContractDeletedStatus(myContract);

Dies ist entweder redundant (da ich das Objekt sowieso ändere) oder ich zwinge mich jetzt, einen tiefen Klon des myContractObjekts zu erstellen. Das ist übermäßig schwierig, wenn Sie jeden Fall abdecken möchten (stellen Sie sich vor, ich möchte Navigationseigenschaften für mehrere Ebenen. Muss ich alle klonen? Nur die Entität der obersten Ebene? ... So viele Fragen, so viele implizite Erwartungen )

Das Ändern des Objekts ist der einfachste Weg, um das zu erreichen, was ich möchte, ohne mehr Arbeit leisten zu müssen, nur um zu vermeiden, dass das Objekt grundsätzlich nicht geändert wird.


Die direkte Antwort auf Ihre Frage lautet also, dass dies keine inhärent schlechte Praxis ist, solange Sie nicht verschleiern, dass die Methode das übergebene Objekt ändern kann. Für Ihre aktuelle Situation wird dies durch den Methodennamen sehr deutlich.

Flater
quelle
0

Ich finde sowohl das Alte als auch das Neue problematisch.

Alter Weg:

1) InsertUser(User user)

Wenn ich diesen Methodennamen lese, der in meiner IDE auftaucht, fällt mir als Erstes ein

Wo wird der Benutzer eingefügt?

Der Methodenname sollte lauten AddUserToContext. Zumindest ist dies das, was die Methode am Ende tut.

2) InsertUser(User user)

Dies verstößt eindeutig gegen das Prinzip der geringsten Überraschung. Sagen wir, ich habe meine Arbeit bisher gemacht und eine neue Instanz von a erstellt Userund ihm eine gegeben nameund eine gesetzt password, ich wäre überrascht:

a) Dies fügt nicht nur einen Benutzer in etwas ein

b) es untergräbt auch meine Absicht, den Benutzer so einzufügen, wie er ist; Der Name und das Passwort wurden überschrieben.

c) bedeutet dies, dass es sich auch um einen Verstoß gegen das Prinzip der Einzelverantwortung handelt.

Neuer Weg:

1) InsertUser(User user)

Immer noch Verstoß gegen SRP und Prinzip der geringsten Überraschung

2) SetUsername(User user)

Frage

Benutzername festlegen? Zu was?

Besser: SetRandomNameoder etwas, was die Absicht widerspiegelt.

3) SetPassword(User user)

Frage

Passwort festlegen? Zu was?

Besser: SetRandomPasswordoder etwas, was die Absicht widerspiegelt.


Vorschlag:

Was ich lieber lesen würde, wäre so etwas wie:

public User GenerateRandomUser(UserFactory Uf){
    User u = Uf.GetUser();
    u.Name = GenerateRandomUserName();
    u.Password = GenerateRandomUserPassword();
    return u;
}

...

public void AddUserToContext(User u){
    this.context.Users.Add(u);
}

Zu Ihrer ersten Frage:

Ist das Ändern eines durch Referenz übergebenen Objekts eine schlechte Praxis?

Nein, es ist eher Geschmackssache.

Thomas Junk
quelle