it-swarm.com.de

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

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 User -Entitä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?

12
JD Davis

Das Problem hierbei ist, dass ein User tatsächlich zwei verschiedene Dinge enthalten kann:

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

  2. Der Satz von Datenelementen, der vom Aufrufer benötigt wird, 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. eine User Klasse und eine EnrollRequest Klasse. 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.

10
John Wu

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 IMHO ein Methodenname wie InsertUserkommuniziert dies nicht klar, und das ist es, was es fehleranfällig macht. 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 Ihrer Implementierungen. Wie InsertUser dies intern tut, 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 InsertUser ein vollständig initialisiertes User -Objekt bereitstellen, oder,

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

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

Wählen Sie also einen Methodennamen wie PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPassword oder was auch immer Sie bevorzugen, um den Nebeneffekt 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.

5
Doc Brown

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 setzen Sie user.UserName Und user.Password. Ich habe Vorbehalte gegen das Passwort, 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 vs. 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 der Name Informationen vom Benutzerobjekt erfordert, können Sie die Signatur in GenerateUserName(User user) ändern und diese Testbarkeit beibehalten

Die neue Methode verbirgt die Mutationen:

  • Sie wissen nicht, dass sich das User -Objekt ändert, bis Sie 2 Ebenen tief gehen
  • Die Änderungen am User -Objekt 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
3
Berin Loritsch

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. Z.B. PrintName(myPerson) sollte das Personenobjekt nicht ändern, da die Methode impliziert, dass sie nur daran interessiert ist , die vorhandenen Werte zu lesen . Dies ist jedoch ein anderes Argument als in Ihrem Fall, da 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.

Zum Beispiel:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

Ich erwarte ausdrücklich, dass die Methode ArrrangeContractDeletedStatus den Status des Objekts myContract ändert.

Der Hauptvorteil besteht darin, dass ich mit dieser Methode das Löschen von Verträgen mit verschiedenen Erstverträgen testen kann. z.B. 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 CreateEmptyContract und ArrrangeContractDeletedStatus zu einer einzigen Methode zusammengeführt hätte; Ich müsste für jeden Vertrag, den ich in einem gelöschten Zustand testen möchte, mehrere Varianten dieser Methode erstellen.

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

myContract = ArrrangeContractDeletedStatus(myContract);

Dies ist entweder redundant (da ich das Objekt sowieso ändere) oder ich zwinge mich jetzt, einen tiefen Klon des myContract -Objekts zu erstellen. Das ist übermäßig schwierig, wenn Sie jeden Fall abdecken möchten (stellen Sie sich vor, ich möchte Navigationseigenschaften auf mehreren 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 zu leisten, nur um zu vermeiden, dass das Objekt grundsätzlich nicht geändert wird.


Die direkte Antwort auf Ihre Frage lautet also, dass es 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.

1
Flater

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 AddUserToContext lauten. 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 eines User erstellt und ihm ein name gegeben und ein password gesetzt. 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: SetRandomName oder etwas, das die Absicht widerspiegelt.

3) SetPassword(User user)

Frage

Passwort festlegen? Zu was?

Besser: SetRandomPassword oder etwas, das 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.

0
Thomas Junk