MVVM und Dienstmuster

13

Ich erstelle eine WPF-Anwendung mit dem MVVM-Muster. Momentan rufen meine Ansichtsmodelle die Serviceebene auf, um Modelle abzurufen (was für das Ansichtsmodell nicht relevant ist) und sie in Ansichtsmodelle zu konvertieren. Ich verwende die Konstruktorinjektion, um den erforderlichen Dienst an das Ansichtsmodell zu übergeben.

Es ist leicht zu testen und funktioniert gut für Ansichtsmodelle mit wenigen Abhängigkeiten. Sobald ich jedoch versuche, Ansichtsmodelle für komplexe Modelle zu erstellen, wird in einen Konstruktor eine Menge Dienste eingefügt (einer zum Abrufen aller Abhängigkeiten und eine Liste aller verfügbaren Werte) um zum Beispiel an eine itemsSource zu binden). Ich frage mich, wie ich mit mehreren Diensten wie diesen umgehen soll und habe immer noch ein Ansichtsmodell, das ich problemlos testen kann.

Ich denke an ein paar Lösungen:

  1. Erstellen eines Services Singleton (IServices), der alle verfügbaren Services als Schnittstellen enthält. Beispiel: Services.Current.XXXService.Retrieve (), Services.Current.YYYService.Retrieve (). Auf diese Weise habe ich keinen riesigen Konstruktor mit einer Vielzahl von Dienstparametern.

  2. Erstellen einer Fassade für die vom viewModel verwendeten Dienste und Übergeben dieses Objekts in den ctor meines viewmodel. Aber dann muss ich für jedes meiner komplexen Ansichtsmodelle eine Fassade erstellen, und es könnte ein bisschen viel sein ...

Was ist Ihrer Meinung nach die "richtige" Art, diese Art von Architektur zu implementieren?

alfa-alfa
quelle
Ich denke, der "richtige" Weg, dies zu tun, besteht darin, eine separate Ebene zu erstellen, die die Dienste aufruft und das Casting ausführt, das zum Erstellen des ViewModel erforderlich ist. Ihre ViewModels sollten nicht dafür verantwortlich sein, sich selbst zu erstellen.
Amy Blankenship
@AmyBlankenship: Ansichtsmodelle müssen sich nicht selbst erstellen (oder müssen dies sogar können), sind jedoch manchmal für die Erstellung anderer Ansichtsmodelle verantwortlich . Ein IoC-Container mit automatischer Factory-Unterstützung ist hier eine große Hilfe.
Aaronaught
"Will times" und "should" sind zwei verschiedene Tiere;)
Amy Blankenship
@AmyBlankenship: Schlagen Sie vor, dass Ansichtsmodelle keine anderen Ansichtsmodelle erstellen sollten? Das ist schwer zu schlucken. Ich kann nachvollziehen, dass Ansichtsmodelle nicht newzum Erstellen anderer Ansichtsmodelle verwendet werden sollten, aber man denke an eine einfache MDI-Anwendung, bei der durch Klicken auf die Schaltfläche "Neues Dokument" oder das Menü ein neuer Tab hinzugefügt oder ein neues Fenster geöffnet wird. Die Shell / der Dirigent muss in der Lage sein, neue Instanzen von etwas zu erstellen , auch wenn es sich hinter einer oder mehreren Indirektionsebenen verbirgt.
Aaronaught
Nun, sicherlich muss es die Fähigkeit haben, zu verlangen, dass irgendwo ein View erstellt wird. Aber um es selbst zu machen? Nicht in meiner Welt :). Andererseits nennen wir in der Welt, in der ich lebe, VM "Präsentationsmodell".
Amy Blankenship

Antworten:

22

In der Tat sind diese beiden Lösungen schlecht.

Erstellen eines Services Singleton (IServices), der alle verfügbaren Services als Schnittstellen enthält. Beispiel: Services.Current.XXXService.Retrieve (), Services.Current.YYYService.Retrieve (). Auf diese Weise habe ich keinen riesigen Konstruktor mit einer Vielzahl von Dienstparametern.

Dies ist im Wesentlichen das Service Locator Pattern , bei dem es sich um ein Anti-Pattern handelt. Wenn Sie dies tun, können Sie nicht mehr verstehen, wovon das Ansichtsmodell tatsächlich abhängt, ohne auf seine private Implementierung zu achten, was das Testen oder Umgestalten sehr schwierig macht.

Erstellen einer Fassade für die vom viewModel verwendeten Dienste und Übergeben dieses Objekts in den ctor meines viewmodel. Aber dann muss ich für jedes meiner komplexen Ansichtsmodelle eine Fassade erstellen, und es könnte ein bisschen viel sein ...

Dies ist weniger ein Anti-Pattern, sondern ein Code-Geruch. Im Wesentlichen erstellen Sie ein Parameterobjekt , aber der Sinn des PO-Refactoring-Musters besteht darin, sich mit Parametersätzen zu befassen, die häufig und an vielen verschiedenen Orten verwendet werden , während dieser Parameter immer nur einmal verwendet werden würde. Wie Sie bereits erwähnt haben, würde dies viel Code aufblähen, ohne dass dies wirklich von Vorteil wäre, und würde nicht gut mit vielen IoC-Containern zusammenspielen.

Tatsächlich übersehen beide oben genannten Strategien das Gesamtproblem, nämlich dass die Kopplung zwischen Ansichtsmodellen und Diensten zu hoch ist . Das einfache Ausblenden dieser Abhängigkeiten in einem Service-Locator oder Parameterobjekt ändert nicht , von wie vielen anderen Objekten das Ansichtsmodell abhängt.

Überlegen Sie, wie Sie eines dieser Ansichtsmodelle einem Unit-Test unterziehen würden. Wie groß wird Ihr Setup-Code sein? Wie viele Dinge müssen initialisiert werden, damit es funktioniert?

Viele Leute, die mit MVVM anfangen, versuchen, Ansichtsmodelle für einen gesamten Bildschirm zu erstellen , was grundsätzlich der falsche Ansatz ist. In MVVM dreht sich alles um Komposition , und ein Bildschirm mit vielen Funktionen sollte aus mehreren unterschiedlichen Ansichtsmodellen bestehen, von denen jedes nur von einem oder wenigen internen Modellen / Diensten abhängt. Wenn sie miteinander kommunizieren müssen, geschieht dies über Pub / Sub (Nachrichtenbroker, Ereignisbus usw.).

Eigentlich müssen Sie Ihre Ansichtsmodelle umgestalten, damit sie weniger Abhängigkeiten aufweisen . Wenn Sie dann einen aggregierten "Bildschirm" benötigen, erstellen Sie ein anderes Ansichtsmodell, um die kleineren Ansichtsmodelle zu aggregieren. Dieses aggregierte Ansichtsmodell muss nicht viel für sich tun, daher ist es auch ziemlich einfach zu verstehen und zu testen.

Wenn Sie dies richtig gemacht haben, sollte es schon beim Betrachten des Codes offensichtlich sein, da Sie kurze, prägnante, spezifische und testbare Ansichtsmodelle haben.

Aaronaught
quelle
Ja, das werde ich wahrscheinlich am Ende tun! Vielen Dank Herr.
Alfa-Alfa
Nun, ich nahm bereits an, dass er dies bereits versucht hatte, aber es gelang ihm nicht. @ Alfa-Alfa
Euphoric
@Euphoric: Wie gelingt dir das "nicht"? Wie Yoda sagen würde: Tu oder nicht, es gibt keinen Versuch.
Aaronaught
@Aaronaught Zum Beispiel braucht er wirklich alle Daten in einem Ansichtsmodell. Vielleicht hat er Gitter und verschiedene Spalten kommen von verschiedenen Diensten. Mit Komposition geht das nicht.
Euphorischer
@Euphoric: Eigentlich Sie können das lösen mit der Zusammensetzung, aber das kann unter der Ansicht Modellebene durchgeführt werden. Es geht einfach darum, die richtigen Abstraktionen zu schaffen. In diesem Fall benötigen Sie nur einen Dienst, um die anfängliche Abfrage zu verarbeiten und eine Liste von IDs sowie eine Sequenz / Liste / ein Array von "Anreicherungselementen" zu erhalten, die mit ihren eigenen Informationen versehen sind. Machen Sie das Raster selbst zu einem eigenen Ansichtsmodell. Sie haben das Problem mit zwei Abhängigkeiten gelöst und es ist äußerst einfach zu testen.
Aaronaught
1

Ich könnte ein Buch darüber schreiben ... eigentlich bin ich es;)

Erstens gibt es keinen allgemein "richtigen" Weg, um Dinge zu tun. Sie müssen andere Faktoren berücksichtigen.

Möglicherweise sind Ihre Dienste zu feinkörnig. Eine bessere Lösung könnte darin bestehen, die Dienste mit Fassaden zu versehen, die die Schnittstelle bieten, die ein bestimmtes ViewModel oder sogar ein Cluster verwandter ViewModels verwenden würde.

Noch einfacher wäre es, die Services in einer einzigen Fassade zusammenzufassen, die von allen Ansichtsmodellen verwendet wird. Natürlich kann dies möglicherweise eine sehr große Schnittstelle mit vielen unnötigen Funktionen für das durchschnittliche Szenario sein. Aber ich würde sagen, es ist nicht anders als ein Nachrichtenrouter, der jede Nachricht in Ihrem System verarbeitet.

Tatsächlich habe ich gesehen, dass sich eine Menge Architekturen letztendlich zu einem Nachrichtenbus entwickelt haben, der um etwas wie das Muster des Ereignisaggregators herum aufgebaut ist. Dort ist das Testen einfach, da Ihre Testklasse nur einen Listener beim EA registriert und das entsprechende Ereignis als Antwort auslöst. Aber das ist ein fortgeschrittenes Szenario, das Zeit braucht, um sich zu entwickeln. Ich sage, fang mit der einheitlichen Fassade an und gehe von dort aus.

Michael Brown
quelle
Eine massive Service Fassade ist sehr verschieden von einem Message - Broker. Es ist fast am entgegengesetzten Ende des Abhängigkeitsspektrums. Ein Kennzeichen dieser Architektur ist, dass der Absender nichts über den Empfänger weiß und es möglicherweise viele Empfänger gibt (Pub / Sub oder Multicast). Vielleicht verwechseln Sie es mit RPC-ähnlichem "Remoting", bei dem nur ein herkömmlicher Dienst über ein Remote-Protokoll verfügbar gemacht wird und der Absender weiterhin sowohl physisch (Endpunktadresse) als auch logisch (Rückgabewert) mit dem Empfang verbunden ist.
Aaronaught
Die Ähnlichkeit besteht darin, dass die Fassade sich wie ein Router verhält und der Anrufer nicht weiß, welcher Dienst / welche Dienste den Anruf bearbeiten, genau wie ein Client, der eine Nachricht sendet, nicht weiß, wer die Nachricht bearbeitet.
Michael Brown
Ja, aber die Fassade ist dann ein Gott-Objekt . Es hat alle Abhängigkeiten, die die Ansichtsmodelle haben, wahrscheinlich mehr, weil es von mehreren geteilt wird. Tatsächlich haben Sie die Vorteile der losen Kopplung, für die Sie sich so viel Mühe gegeben haben, beseitigt, weil Sie jetzt, wenn etwas die Megafassade berührt, keine Ahnung haben, von welcher Funktionalität es wirklich abhängt. Stellen Sie sich vor, Sie schreiben einen Komponententest für eine Klasse, die die Fassade verwendet. Sie erstellen ein Mock für die Fassade. Welche Methoden verspotten Sie? Wie sieht Ihr Setup-Code aus?
Aaronaught
Dies unterscheidet sich stark von einem Nachrichtenbroker, da der Broker auch nichts über die Implementierung der Nachrichtenhandler weiß . Es verwendet IoC unter der Haube. Die Fassade weiß alles über die Empfänger, weil sie Anrufe an sie weiterleiten muss. Der Bus hat keine Kopplung; die fassade weist eine unglaublich hohe efferenz auf. Fast alles, was Sie ändern, wirkt sich auch auf die Fassade aus.
Aaronaught
Ich denke, ein Teil der Verwirrung hier - und ich sehe das ziemlich oft - ist, was eine Abhängigkeit bedeutet. Wenn Sie eine Klasse haben, die von einer anderen Klasse abhängt, aber 4 Methoden dieser Klasse aufruft, hat sie 4 Abhängigkeiten, nicht 1. Wenn Sie alles hinter eine Fassade stellen, ändert sich die Anzahl der Abhängigkeiten nicht, sie sind nur schwerer zu verstehen .
Aaronaught
0

Warum nicht beides kombinieren?

Erstellen Sie eine Fassade und stellen Sie alle Dienste bereit, die Ihre Ansichtsmodelle verwenden. Dann können Sie eine einzige Fassade für alle Ihre Ansichtsmodelle ohne das falsche S-Wort haben.

Oder Sie können die Eigenschaftsinjektion anstelle der Konstruktorinjektion verwenden. Aber dann müssen Sie sicherstellen, dass diese richtig injiziert werden.

Euphorisch
quelle
Dies wäre eine bessere Antwort, wenn Sie ein Beispiel in Pseudo-C # angeben würden.
Robert Harvey