MVVM mit Unity und Unit Testing Architekturentwurf

8

Ich erstelle eine Visual Studio-ähnliche Anwendung in WPF, und ich habe einige Probleme, die beste architektonische Entwurfsorganisation meiner Komponenten zu identifizieren. Ich plane, Unity als meine Abhängigkeit-Injektion Container und Visual Studio Unit Testing-Framework und wahrscheinlich Moq zum Mocking Bibliothek.

Ich werde zuerst die Struktur meiner Lösung beschreiben, dann meine Fragen:

Ich habe ein WPF-Projekt , das Folgendes enthält:

  • Meine Unity-Container-Initialisierung (Bootstrapper) beim Start der Anwendung (in App.xaml.cs)
  • Alle meine Anwendungsansichten (XAML).

Ein anderes Projekt namens ViewModel enthält Folgendes:

  • Alle meine Anwendung ViewModels. Alle meine ViewModels erben von einer ViewModelBase, die eine ILogger-Eigenschaft
  • verfügbar macht

Meine Initialisierungslogik lautet wie folgt:

  1. Anwendungsstart
  2. Erstellen und Registrieren von Unity-Containern von Typen: MainView und MainViewModel
  3. Beheben Sie meine MainView und zeigen Sie sie an.

var window = Container.Resolve<MainView>();

window.Show();

Mein MainView-Konstruktor erhält ein MainViewModel-Objekt in seinem Konstruktor:

%Vor%
  1. Mein MainViewModel hat ein Child ViewModel für jedes Panel:

    %Vor%

Und ich plane, eine InitializePanels () -Methode zu erstellen, die jedes der Panels initialisiert.

Jetzt hier meine Fragen: Wie kann meine MainViewModel.InitializePanels () alle diese Panels initialisieren? die folgenden Optionen gegeben:

Option 1: Initialisieren Sie die ViewModels manuell:

%Vor%

Nachteile:

  • Ich verwende den Unity-Container nicht, damit meine Abhängigkeiten (z. B. ILogger) nicht automatisch aufgelöst werden.

Option 2: Verwenden Sie die Setter-Injektion, indem Sie meine Eigenschaften mit Anmerkungen versehen:

%Vor%

Nachteile:

  • Ich habe gelesen, dass Unity Setter-Abhängigkeiten vermieden werden sollten, da sie in diesem Fall eine Abhängigkeit mit Unity erzeugen
  • Ich habe auch gelesen, dass Sie Unity für Komponententests vermeiden sollten. Wie kann ich diese Abhängigkeit in Unit-Tests verdeutlichen? Es kann ein Alptraum sein, viele abhängige Eigenschaften zu haben.

Option 3: Verwenden Sie die Unity Constructor-Injection, um ALLE meine Panel ViewModels an den MainViewModel-Konstruktor zu übergeben, damit sie automatisch vom Unity-Container aufgelöst werden:

%Vor%

Vorteile:

  • Die Abhängigkeit wäre zum Zeitpunkt der Erstellung offensichtlich und klar, was beim Aufbau meiner ViewModel UnitTests helfen könnte.

Nachteile:

  • So viele Konstruktor-Parameter können ziemlich schnell hässlich werden

Option 4: Registrieren aller VM-Typen bei Containeraufbau. Übergeben Sie dann die UnityContainer-Instanz über die Konstruktorinjektion an mein MainViewModel:

%Vor%

So könnte ich etwas tun wie:

%Vor%

Nachteile:

  • Wenn ich mich entscheide, UNITY für meine UnitTests nicht zu verwenden, wie viele Leute vorschlagen, kann ich meine Panel ViewModels nicht auflösen und initialisieren.

Angesichts dieser langwierigen Erklärung, was ist der beste Ansatz, damit ich einen Dependency-Injektionscontainer nutzen kann und am Ende mit einer Unit-Testable-Lösung enden kann?

Vielen Dank im Voraus,

    
Adolfo Perez 22.06.2012, 14:17
quelle

3 Antworten

5

Das Wichtigste zuerst ... Wie Sie festgestellt haben, ist Ihre aktuelle Konfiguration möglicherweise problematisch beim Testen von Einheiten (komplexe VM-Initialisierung). Allerdings macht das DI-Prinzip , hängt von Abstraktionen ab, nicht von Konkretionen , macht dies Problem gehen sofort weg. Wenn Ihre Ansichtsmodelle Schnittstellen implementieren und Abhängigkeiten über Schnittstellen realisiert werden, wird jede komplexe Initialisierung irrelevant, da Sie im Test einfach Mocks verwenden.

Als Nächstes besteht das Problem bei annotierten Eigenschaften darin, dass Sie hohe Kopplung zwischen Ihrem Ansichtsmodell und Unity erstellen (daher ist es höchstwahrscheinlich falsch). Idealerweise sollten Registrierungen an einem einzelnen Punkt auf der obersten Ebene behandelt werden (was in Ihrem Fall der Bootstrapper ist), so dass der Container in keiner Weise an das Objekt gebunden ist, das er bereitstellt. Ihre Optionen # 3 und # 4 sind die häufigsten Lösungen für dieses Problem mit wenigen Anmerkungen:

  • zu # 3 : Zu viele Konstruktorabhängigkeiten werden normalerweise gemildert, indem allgemeine Funktionen in Fassadenklassen (aber 4 ist nicht so viele immerhin). In der Regel hat ordnungsgemäß entworfener Code dieses Problem nicht. Beachten Sie, dass abhängig von Ihrem MainViewModel möglicherweise nur die Abhängigkeit von Liste der untergeordneten Ansichtsmodelle erforderlich ist, keine konkreten.
  • bis # 4 : In Komponententests sollten Sie keinen IoC-Container verwenden. Sie erstellen einfach Ihre MainViewModel (via Ctor) manuell und injizieren Mocks von Hand .

Ich möchte noch einen weiteren Punkt ansprechen. Überlegen Sie, was passiert, wenn Ihr Projekt wächst. Das all Packen von Modellen in ein einzelnes Projekt ist möglicherweise keine gute Idee. Jedes View-Modell hat seine eigenen Abhängigkeiten (oft nicht verwandt mit anderen), und all diese Dinge müssen zusammensitzen. Dies könnte sich schnell als schwierig erweisen. Denken Sie stattdessen darüber nach, ob Sie einige allgemeine Funktionen (z. B. messaging , tools ) extrahieren und in separaten Projektgruppen (wiederum in M-VM-V-Projekte) aufteilen können ).

Außerdem ist es viel einfacher, Ansichten auszutauschen, wenn Sie eine funktionsbezogene Gruppierung haben. Wenn die Projektstruktur so aussieht:

%Vor%

Wenn Sie eine andere Ansicht für das Bearbeitungsfenster des Benutzers ausprobieren, müssen Sie eine einzelne Assembly erneut kompilieren und austauschen ( User.Views ). Mit all in one bag Ansatz müssen Sie viel größere Teile der Anwendung neu erstellen, auch wenn sich die Mehrheit nicht geändert hat.

Bearbeiten: Denken Sie daran, dass Ändern der bestehenden Struktur des Projekts (auch winzig) ist in der Regel ein sehr teurer Prozess mit kleineren / keine Geschäftsergebnisse. Es ist Ihnen vielleicht nicht erlaubt oder einfach nicht möglich, dies zu tun. Verwendung -basierte (DAL, BLL, BO usw.) Struktur funktioniert , es wird mit der Zeit immer schwerer. Sie können auch den gemischten Modus verwenden, mit Kernfunktionen gruppiert nach ihrer Verwendung und einfach neue Funktionen hinzufügen, die einen modularen Ansatz verwenden.

    
k.m 22.06.2012, 14:30
quelle
2

Zunächst möchten Sie vielleicht eher Interfaces als konkrete Klassen verwenden, damit Sie beim Unit-Testen die Mock-Objekte übergeben können, also IToolboxViewModel anstelle von ToolboxViewModel , etc.

Davon abgesehen würde ich die dritte Option - Konstruktorinjektion - empfehlen. Dies ist am sinnvollsten, da Sie sonst var mainVM = new MainViewModel() aufrufen und mit einem nicht-funktionalen View-Modell enden könnten. Dadurch machen Sie es auch sehr einfach zu verstehen, was die Abhängigkeiten Ihres Ansichtsmodells sind, was es einfacher macht, Komponententests zu schreiben.

Ich würde diesen Link überprüfen, da er für Ihre Frage relevant ist.

>     
Adi Lester 22.06.2012 14:27
quelle
1

Ich stimme Lesters Punkten zu, möchte aber einige weitere Optionen und Meinungen hinzufügen.

Wo Sie das ViewModel über den Konstruktor an die View übergeben, ist das etwas unkonventionell, da die Bindungsfunktion von WPF Ihnen ermöglicht, das ViewModel vollständig von der View zu entkoppeln, indem Sie es an das DataContext-Objekt binden. In dem von Ihnen beschriebenen Design ist die View an eine konkrete Implementierung gekoppelt und begrenzt die Wiederverwendung.

Während eine Service-Fassade Option 3 vereinfacht, ist es nicht ungewöhnlich (wie Sie bereits beschrieben haben), dass ViewModels der obersten Ebene viele Verantwortlichkeiten haben. Ein anderes Muster, das Sie berücksichtigen können, ist ein Controller- oder Factory-Muster, das das Ansichtsmodell zusammenfügt. Die Fabrik kann durch den Container unterstützt werden, um die Arbeit zu erledigen, aber der Container wird vom Anrufer abstrahiert. Ein Hauptziel beim Erstellen containergesteuerter Apps besteht darin, die Anzahl der Klassen zu begrenzen, die verstehen, wie das System zusammengesetzt ist.

Ein weiterer Punkt betrifft die Anzahl der Verantwortlichkeiten und Objektbeziehungen, die zum Viewmodell auf oberster Ebene gehören. Wenn Sie sich Prism ansehen (ein guter Kandidat für WPF + Unity), wird das Konzept der "Regionen" eingeführt, die von Modulen bevölkert werden. Eine Region kann eine Symbolleiste darstellen, die von mehreren Modulen ausgefüllt wird. Bei einem solchen Entwurf hat das Top-Level-Viewmodel weniger Verantwortlichkeiten (und Abhängigkeiten!) Und jedes Modul enthält Unit-testbare DI-Komponenten. Große Verschiebung des Denkens von dem Beispiel, das Sie zur Verfügung gestellt haben.

In Bezug auf Option 4, bei der der Container durch den Konstruktor übergeben wird, ist eine technische Abhängigkeitsinversion, jedoch in Form eines Servicestandorts anstelle einer Abhängigkeitsinjektion. Nachdem ich diesen Weg gegangen bin, bevor ich dir sagen kann, dass es ein sehr rutschiger Abhang ist (mehr wie eine Klippe): Abhängigkeiten sind in Klassen versteckt und dein Code wird zu einem Netz von "just in time" -Wahnsinn - völlig unberechenbar, absolut untestabil.

    
bryanbcook 23.06.2012 16:12
quelle