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?
quelle
user
der Code als Referenz übergeben würde, könnte er dem Anrufer aus den Händen gezogen und durch einfaches Sagen von beispielsweise ersetzt werdenuser = null;
.Antworten:
Das Problem hierbei ist, dass a
User
tatsächlich zwei verschiedene Dinge enthalten kann:Eine vollständige Benutzerentität, die an Ihren Datenspeicher übergeben werden kann.
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
User
Klasse und eineEnrollRequest
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: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.
quelle
InsertUser
hat 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.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 erwartenum den internen Status des Repositorys zu ändern, nicht den Status des Benutzerobjekts. Dieses Problem besteht in beiden Implementierungen. Wie
InsertUser
dies 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
InsertUser
ein vollständig initialisiertesUser
Objekt bereitstellen , oderBauen 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
,InsertUserWithNewNameAndPassword
oder 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.
quelle
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.UserName
und einuser.Password
. Ich habe Vorbehalte gegen das Passwortelement, aber diese Vorbehalte sind für das jeweilige Thema nicht relevant.Auswirkungen der Änderung von Referenzobjekten
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 werdenGenerateUserName(User user)
und diese Testbarkeit beibehaltenDie neue Methode verbirgt die Mutationen:
User
Objekt ändert, bis Sie 2 Ebenen tief gehenUser
Objekt sind in diesem Fall überraschenderSetUserName()
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 funktionierenquelle
Ich stimme der Antwort von John Wu voll und ganz zu. Sein Vorschlag ist gut. Aber es fehlt etwas Ihre direkte Frage.
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 diesSetUsername(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:
Ich erwarte ausdrücklich, dass die
ArrrangeContractDeletedStatus
Methode den Status desmyContract
Objekts ä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
CreateEmptyContract
undArrrangeContractDeletedStatus
in 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:
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 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.
quelle
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
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
User
und ihm eine gegebenname
und eine gesetztpassword
, 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
Besser:
SetRandomName
oder etwas, was die Absicht widerspiegelt.3)
SetPassword(User user)
Frage
Besser:
SetRandomPassword
oder etwas, was die Absicht widerspiegelt.Vorschlag:
Was ich lieber lesen würde, wäre so etwas wie:
Zu Ihrer ersten Frage:
Nein, es ist eher Geschmackssache.
quelle