Laden...

Observable- und ValidableObject als Basis für ViewModels, etc.

Erstellt von Palladin007 vor 7 Jahren Letzter Beitrag vor 7 Jahren 5.711 Views
Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 7 Jahren
Observable- und ValidableObject als Basis für ViewModels, etc.

Servus,

ich hab mir mal vorgenommen, eine Basis für Klassen zu schreiben, die INotifyPropertyChanged oder INotifyPropertyChanging benötigen. Das Ganze soll flexibel sein um z.B. auch Validierung zu unterstützen.
Anschließend habe ich auch gleich eine Basis für Klassen geschrieben, welche die Validierung über ValidationRules unterstützt.
Das Ganze soll flexibel sein, dass z.B. auch Abhängigkeiten unter Properties - auch zu Properties von externen Klassen - automatisch geregelt werden.

Das Projekt liegt als zip im Anhang. Visual Studio 2015, keine Extensions wie z.B. ReSharper sind notwendig.

Ich habe für die meisten Klassen ein Interface bereit gestellt und für die Interfaces IObservableObject und IValidableObject jeweils eine Service-Klasse implementiert, damit die Funktionalität auch implementiert werden kann, ohne dass eine Basisklasse notwendig ist.

Außerdem habe ich bewusst keine Kommentare geschrieben, da ich versucht habe, alles so zu benennen, dass es auch ohne Kommentare verständlich ist.

Die Library ist Teil einer Sammlung von Libraries, alles, was ich nicht nutze, habe ich aber aus dem Projekt entfernt.
Zusätzlich gibt es ein Test-Projekt, wo ich für alle Klassen UnitTests geschrieben habe, die eigene Funktionalität anbietet.

Ich würde mich freuen, wenn ihr da mal drüber schaut und besonders im Bezug auf Langlebigkeit bewertet, da ich das auch für weitere Projekte oder Biliotheken (z.B. BusinessObject-Framework) als Grundlage verwenden möchte 😃
Also Code-Qualität, Lesbarkeit, Wartbarkeit, UnitTests, etc.

1.040 Beiträge seit 2007
vor 7 Jahren

Die Delegaten für die EventHandler sind überflüssig, besser und einfacher:

EventHandler<MyEventArgs>

Im PropertyChangingExtEventHandlerManager wird das Invoke rekursiv aufgerufen.

=)

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 7 Jahren

Die Delegaten für die EventHandler sind überflüssig, besser und einfacher:

EventHandler<MyEventArgs>  

Guter Tipp, hätte mir auch selber auffallen können 😄
Hab ich direkt umgesetzt

Im PropertyChangingExtEventHandlerManager wird das Invoke rekursiv aufgerufen.

Das stimmt -.-
In dem Äquivalent dazu war mir der Fehler bereits aufgefallen.
Ich hab schon mal versucht, beide Manager-Klassen zusammen zu fassen, aber das wurde ein kleines Chaos 😄

Aber danke für den Tipp, ich nehme das auch gleich mit in die Tests auf ^^

Was hältst Du denn von dem Gesamtkonzept?

16.806 Beiträge seit 2008
vor 7 Jahren

Regel #1: Vermeide Projekte mit generischen Bezeichnungen wie "Common", "Manager", "General".
Nur eine Frage der Zeit, bis jeglicher Krams in sowas landet.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 7 Jahren

Naja - genau das hatte ich eigentlich vor 😄

Ich möchte in der Projektmappe sammeln, was ich so über die Zeit immer wieder brauche und versuche dabei in den einzelnen Projekten thematisch zu trennen. So zum Beispiel das ObservableObject, oder für WPF verschiedene Command-Implementierungen (Delegate, Async, ...)
Damit ich nicht für alles Mögliche kleine Projektmappen mit einem Projekt habe, habe ich das so zusammen gefasst.

Oder wie würdest Du eine solche Sammlung von häufig benötigten Dingen aufbauen?

16.806 Beiträge seit 2008
vor 7 Jahren

Mach Dein Projekt- bzw. Design so, dass Common nicht existieren kann.
Projekte, die Common heissen, werden in 99% der Fälle zugemüllt mit jedem - auf Deutsch - Scheiss und werden nach einiger Zeit auch unbrauchbar, da nicht mehr austauschbar.
Habe noch kein einziges Common-Projekt gesehen, das älter als ein paar Monate ist und funktioniert. Keines.

Aktuell ist das Projekt "Common" bei Dir noch übersichtlich und hat wenig Abhängigkeiten.
Aber jede Wette, dass das in ein paar Monaten anders aussieht.

Das Validation-Zeug kannst Du auch super auf DB Entitäten laufen lassen, um diese zu validieren. Heisst aber, dass Deine Entitäten eine "Common" referenzieren müssen, das Component-Zeug kennt.
Dat is halt genau das, was man vermeiden sollte.

Mach halt ein Projekt Palladin.SystemExtensions mit Type und Co, Palladin.PropertyNotifications mit dem ganzen Property Zeugs.
Palladin.Observerable mit diesem Zeugs.

So baust Du Dir gerade einen minimalistischen Monolith.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 7 Jahren

Du meinst, ich sollte mir 1. einen besseren möglichst eindeutigen Namen suchen und 2. pro "Feature" ein eigenes Projekt mit passendem Namespace anlegen?

  1. werde ich sowieso noch machen - ich hab nur aktuell keine gute Idee, die ich dann auch lange so lassen würde 😄
  2. Das tue ich (eigentlich) sowieso, hier hab ich wohl damit gebrochen, ohne es zu merken 😄 Den Namen ComponentModel hab ich mir von .NET abgeschaut, wenn ich aber so darüber nachdenke, ist das auch wieder etwas, das alles mögliche bedeuten kann.

Was hältst Du davon, wenn ich pro Implementierung eines häufig genutzten Problems ein neues Projekt mit eigenem Namespace anlege? Die würden dann immer noch unter Common.blabla bzw. dem Namen, den es noch zu finden gilt, laufen.
So würden die Observable- und Validable-Namespaces eigene Projekte bekommen und die Notification-Sachen ebenfalls, sodass das ComponentModel bis auf diese drei Namespaces leer ist.

Ich habe auch einige kleine Dinge (aus dem Projekt im Anhang entfernt), die sich nicht direkt einem Anwendungsfall zuordnen lassen, die ich aber häufig nutze und bisher immer kopiert habe. So nutze ich z.B. ganz gerne die GetOrAdd-Methode von ConcurrentDictionary, was IDictionary aber nicht hat. Dafür hab ich daher ein paar Erweiterungs-Methoden. Oder eine Enumerable-Implementierung, welche die Werte cached und beim Reset aus dem Cache liest.

Alles "kleine", was sich keinem Anwendungsbereich zuordnen lässt, würde ich dann in einem darüber liegenden Namespace belassen (der auch in einer eigenen DLL liegt). Da muss ich dann nur darauf achten, dass ich nicht zu viel dort ablege, denn Auslagern würde extreme Fleißarbeit in allen Projekten, die das nutzen, bedeuten.

16.806 Beiträge seit 2008
vor 7 Jahren

Die Trennung is auf alle Fälle pflicht in meinen Augen.
Wenn ich Reviews mach, zB, auf Anfrage und irgendwo Common seh, dann wird das ohne Gnade und gegen jede Argumentation rot markiert.
Da gibts wenig Diskussionsgrundlage, egal für welchen Code oder Codegröße, von meiner Seite. Ist - so sehe ich das - einfach ein absolutes NoGo in Sachen Projekt- und Codestruktur.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 7 Jahren

Ich finde das gut, dass Du ohne Gnade rot markierst, deshalb hab ich es ja hier rein gestellt 😄
Danke dafür 😃
Allerdings versuche ich auch, den für mich besten Weg zu finden, daher erkläre ich den Grund für mein Vorgehen und frage nach.

Du meinst, ich soll dann eher Projekte anlegen, die z.B. "Observable", "Validable", "PropertyNotification" heißen (mit irgendeinem Namen davor)? Das Common würde überall rausfliegen, sodass ich dann die Namespaces "MyName.Observable", "MyName.Validable" und "MyName.PropertyNotifications" habe? Aus Common würde ich dann das "Expressions"-Namespace in ein eigenes Projekt "MyName.ExpressionAnalysis" auslagern.
Wenn ich so darüber nachdenke, ist das schon sinnvoll, aber was mache ich, wenn ich einige Erweiterungsmethoden habe, die sich ohne Zweifel an zig verschiedenen Stellen sinnvoll nutzen lassen? Sollte ich dafür dann ein Projekt "Extensions" anlegen?
Oder kleine Klassen, wie die CachedEnumerable-Implementierung, die mit den zwei Klassen irgendwie ein bisschen traurig in einem eigenen Projekt aussehen würde? 😄

16.806 Beiträge seit 2008
vor 7 Jahren

Ganz so granular wiederum mach ich es persönlich nicht.
Ich habe für einige Klassen einfach Shared-Projekte (zB. Validation), sodass ich mir die zusätzliche DLL/Namespace spare aber trotzdem eine logische Trennung habe.

3.003 Beiträge seit 2006
vor 7 Jahren

Kurze Frage - wieso implementierst du in den Managern Multicast-Delegaten nach, wenn wir schon events haben?

Ich hab' noch nicht alles durchgeschaut (die Validierungen links liegen lassen), aber es wirkt ziemlich over-engineered dafür, einer Familie von Klassen mehr oder minder zwei Ereignisse und ein Grundgerüst für die Auslösung dieser Ereignisse mitzugeben.

Für mich mehr auf der Hand gelegen hätte eine Grundimplementation von den beiden Ereignissen, und ein Attribut für Properties, das, wenn gesetzt, dafür sorgt, dass beim Setzen das Ereignis ausgelöst wird. So presst du auch die Properties der Klassen in ein Korsett - nicht nur muss die Klasse selbst ein interface implementieren, sondern auch ihre Eigenschaften. Ganz schön enge Vorgabe.

LaTino
(der sich das morgen nochmal in Ruhe reinzieht und sich 'n Klassendiagramm malt^^)

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 7 Jahren

Ich habe für einige Klassen einfach Shared-Projekte (zB. Validation), sodass ich mir die zusätzliche DLL/Namespace spare aber trotzdem eine logische Trennung habe.

Was macht das denn für einen Unterschied zu normalen Projekten, außer dass es nicht in eine eigene DLL kompiliert wird?
Bisher habe ich den Vorteil von SharedProjects eher bei Projekten für verschiedenen Platformen gesehen. Dass der Großteil des Codes in SharedProjects liegt und pro Platform referenziert wird. Also wie es z.B. MVVMLight macht.

Kurze Frage - wieso implementierst du in den Managern Multicast-Delegaten nach, wenn wir schon events haben?

Weil ich zwei verschiedene Changing bzw. Changed Handler habe.
Ich kann zwar die .NET-Variante nehmen und in einem neuen Delegaten, der für meine erweiterte Variante ist, aufrufen, allerdings kann ich diesen Handler dann nicht mehr entfernen, da nicht das selbe Delegate-Objekt im Event registriert ist.

Ich hab' noch nicht alles durchgeschaut (die Validierungen links liegen lassen), aber es wirkt ziemlich over-engineered dafür, einer Familie von Klassen mehr oder minder zwei Ereignisse und ein Grundgerüst für die Auslösung dieser Ereignisse mitzugeben.

Für mich mehr auf der Hand gelegen hätte eine Grundimplementation von den beiden Ereignissen, und ein Attribut für Properties, das, wenn gesetzt, dafür sorgt, dass beim Setzen das Ereignis ausgelöst wird. So presst du auch die Properties der Klassen in ein Korsett - nicht nur muss die Klasse selbst ein interface implementieren, sondern auch ihre Eigenschaften. Ganz schön enge Vorgabe.

Ich verstehe deinen Einwand. Dieses Korsett ist auch der Grund, weshalb ich da selber sehr streng mit mir sein will, das erste Mal (Asche auf mein Habut 😄) UnitTests für private Projekte geschrieben habe und es hier gepostet habe.
Flexibel ist ja schön und gut, macht das Ganze aber auch fehleranfälliger.

Ich habe das aber bewusst so gemacht, da das später auch als Grundlage für ein BusinessObject-Framework dienen soll.
Außerdem habe ich so die Möglichkeit, komplexere Berechnungen für Properties in eigene Klassen auszulagern (ähnlich den Commands von WPF) und so die eigentliche Klasse übersichtlicher zu gestalten.
Für einfache automatische Properties habe ich eine FieldPropertyInfo-Implementierung, die nur den Wert hält und nichts weiter tut.

Attribute mag ich eigentlich gesagt nicht 😄
Ich finde die ziemlich hässlich und wenn es doch mal notwendig ist, den durch ein Attribut gesetzten Wert zur Laufzeit zu bearbeiten, dann ist das entweder sehr dirty und der falsche Weg, oder es wird richtig lustig, alles umzubauen. Vermutlich aber Beides 😄
Aber ich könnte eine Attribut-Unterstützung einbauen, die genutzt werden kann, aber nicht muss und leicht ausgetauscht werden kann. Das wäre nur eine PropertyInfo-Implementierung, welche die Reflection-Daten bekommt und sich daraus dann die Attribute raus sucht und interpretiert.

3.003 Beiträge seit 2006
vor 7 Jahren

Das Ding ist, dass du deinen Objekten eine Struktur aufzwingst, die ihnen neben ihrer eigentlichen Aufgabe eine weitere Fähigkeit beibringt. Single Responsibility und so. Attribute injizieren zusätzliche Funktionalität vertikal zur eigentlichen Aufgabe eines Objekts. Weniger hochgestochen: man kriegt eine zusätzliche, exakt definierte Funktionalität an beliebige Objekte gepappt, (im Idealfall) ohne die Objekte anzufassen. Nimm als Beispiel das Serializable- und verwandte Attribute. Wenn man das nicht hätte, müsste man jeder Klasse beibringen, wie es sich serialisiert darzustellen hat. Das könnte man auch durch eine generische Superklasse abbilden, aber dann hätte jedes abgeleitete Objekt jederzeit auch eine Implementierung seiner Serialisierung, und ehrlich gesagt, geht ein Objekt überhaupt nicht an, wie ich es zu serialisieren gedenke. Übertrag das auf den Anwendungsfall hier, dann wird hoffentlich deutlicher, was ich meine. Und ja, man startet im Normalfall tatsächlich mit einer Basisklasse, die IPNC implementiert, und leitet alles davon ab, was das eben können soll. Aber wenn man immer mehr Vorgaben und Interfaces einfügt, um immer mehr Funktionalität für an sich nichts miteinander zu tun habende Klassen zu bekommen, dann muss man sich langsam Gedanken machen, wie man das so löst, dass nicht ein komplexes, miteinander verwobenes, voneinander abhängiges Gebilde entsteht, das irgendwann kaum noch zu managen ist.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 7 Jahren

Single Resposibility kenne ich, aber ich muss gesetehen, mir ist nicht ganz klar, was Du meinst.

Meinst Du, dass eine Klasse, die von ObservableObject ableitet, diese Funktionalität dann auch den Child-Klassen aufzwingt?
Das ist nur ein Wrapper um eine ObservableObjectService-Klasse, damit keine Basis-Klasse vorgeschrieben wird, die Funktionalität aber trotzdem genutzt werden kann.

Oder meinst Du, dass jede IObservablePropertyInfo-Implementierung nicht wirklich die Informationen der Property bereit stellt, sondern mehr tut, nämlich Wert abfragen, schreiben und über Änderungen informieren?
Das stimmt, wirklich glücklich bin ich damit auch nicht, aber mir fiel kein besserer Weg sein, das anders zu lösen ohne dabei die Möglichkeit zu unterdrücken, externe Properties als Quelle zu verwenden.