Autor Beitrag
danielf
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 1012
Erhaltene Danke: 24

Windows XP
C#, Visual Studio
BeitragVerfasst: Di 08.12.15 16:20 
Hallo,

gibt es ein Read-Only pattern für c#?

Meine Idee ist, dass ich ein Interface mit gettern und eins mit settern habe. Das ReadWrite object erbt von beiden.

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
    namespace Read
    {
        internal interface IName : IWriteable<ReadWrite.IName>
        {
            string Name { get; }
        }
    }

    namespace Write
    {
        internal interface IName : Test.Read.IName
        {
            string Name { set; }
        }
    }

    namespace ReadWrite
    {
        internal interface IName : Test.Read.IName, Test.Write.IName
        {
        }
    }


Wie gesehen implementiert das Read-Object noch IWriteable mit dem man das schreibbare Objekt bekommt:

ausblenden C#-Quelltext
1:
2:
3:
4:
    internal interface IWriteable<T>
    {
        T GetWritable();
    }


Damit kann ich nun eine Name-Klasse erstellen.

ausblenden C#-Quelltext
1:
2:
3:
4:
    class NameBase : Base<ReadWrite.IName>, ReadWrite.IName
    {
        public string Name { get; set; }
    }


Damit ich die GetWriteable nicht immer implementiert muss, wird sie in der Base-Klasse realisiert:

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
    internal abstract class Base<T> : IWriteable<T>
        where T : class
    {
        public T GetWritable()
        {
            return this as T;
        }
    }


Bei einer Test-Anwendungs verhalten sich die Klassen wie gewünscht:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
class ReadWriteTest
    {
        public static void Run()
        {
            Read.IName name = new NameBase();

            //  error CS0200: Property or indexer 'Test.IName.Name' cannot be assigned to -- it is read only
            // name.Name = "Test";

            var writeable = name.GetWritable();
            writeable.Name = "Test";

            name = writeable;
        }
    }


Nun zu meinem Problem mit diesem Lösungsansatz:

1) Verwendung von as-operator
Die Basisklasse verwendet den as-operator um das schreibbare (typisierte) Objekt zurück zugeben. Dies finde ich nicht schön (nicht compil-sicher)
2) "Redundante" angabe des IReadWrite.IName interface in der NameBase-Klasse
Könnte ich sicherstellen, dass Implementierungen von Base immer den generischen Type implementieren könnte ich beide Probleme umgehen:
ausblenden C#-Quelltext
1:
internal abstract class Base<T> : IWriteable<T>, T					

Das geht leider nicht: error CS0689: Cannot derive from 'T' because it is a type parameter

Kann mir hierbei jemand helfen? Gerne auch andere Vorschläge zur Umsetzung von Read-/Write interfaces.

Danke und Gruß
Daniel
Palladin007
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 1282
Erhaltene Danke: 182

Windows 11 x64 Pro
C# (Visual Studio Preview)
BeitragVerfasst: Di 08.12.15 19:29 
Wozu genau soll das gut sein? :D

Das sieht sehr wirr aus und ich gebe zu, es drei mal lesen zu müssen um halbwegs zu kapieren, was Du mit den ganzen Interfaces bezwecken willst
Abgesehen davon wird das nie funktionieren, Du kannst nicht mit einem Interface dem COmpiler mit teilen, dass irgendeine Property auf einmal doch writable ist.
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4700
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Di 08.12.15 21:27 
Ich versteh auch nicht wirklich wofür das gut sein soll ein Programmiersprache ist kein Rechtesystem und sobald ich einfach das eine Interface über das andere bekommen kann ist es nur ein wenig Extraaufwand im Code der nichts bewirkt sondern nur nervt.
Je nach Ziel hätte ich da folgende Ideen

a.) Proxyklassen.
Die eigentliche Datenklasse ist read/writeable aber internal und für einen Benutzer nicht erreichbar. Dazu gibt es zwei Proxy Klassen je eine Readonly und eine Read/Write Variante.
Eine Factory Klasse liefert dir je nach Anfrage eine der beiden Varianten (oder verhindert es wenn der Benutzer nicht die notwendigen Rechte hat) und wenn es nötig ist sollte diese Factory eben auch die Proxies für den Zugriff durch die jeweils andere Variante ersetzen können.

b.) Freezeable Klassen
Umschaltbarer Verhalten, getter/setter wären immer da. Es wird aber geprüft ob der Vorgang im aktuellen Objectzustand erlaubt ist.
Das hätte dann aber Einfluß auf Property Implementierungen (oder man müßte was mit einem AOP Framework wie PostSharp basteln).

c.) Jemand der der nur lesen können soll bekommt immer nur eine Kopie ~Original~ Objects. Das kann er dann ändern wie er will es hat keine Auswirkung.
Palladin007
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 1282
Erhaltene Danke: 182

Windows 11 x64 Pro
C# (Visual Studio Preview)
BeitragVerfasst: Di 08.12.15 21:53 
Ergänzend zum Gedanken a, Proxyklasse:

Ein Interface ist ungefähr genau das ^^
Ein Interface, das Lesen kann und eines, das schreiben kann. Die internal Klasse implementiert Beide.
Per Reflection kommt man zwar immer noch relativ leicht an die nicht erlaubten Komponenten ran, aber mit Reflection kann man sowieso beinahe alles umgehen.
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4700
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Di 08.12.15 22:07 
Zitat:
Ein Interface ist ungefähr genau das ^^


Aber eben nur fast ;) Wenn ich einen Proxy habe der nur das Read Interface implementiert und einen anderen der nur das Write Interface implementiert dann wird es schwer sich mehr zu erschleichen.
Da braucht man schon einen dritten (eine Ding aus der Factory/Servoc Pattern/DI Schublade) der einen das passende liefert und der dann sinnvoll ~steuern~ kann.
Sobald man Reflection immer mit einrechnet ist dann sowieso jegliches Sichtbarkeitenmodell überflüssig.
Palladin007
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 1282
Erhaltene Danke: 182

Windows 11 x64 Pro
C# (Visual Studio Preview)
BeitragVerfasst: Di 08.12.15 22:14 
Dennoch würde ich immer ein Interface verwenden, wenn es geht. Schon alleine deshalb, weil eine Klasse nur von einer anderen Klasse erben kann.
Ich beschwere mich gefühlt jeden Tag darüber, dass z.B. die MarkupExtension-Klasse von WPF kein Interface ist :D Oder ValidationRule ...


Aber Du hast Recht, wenn die Implementierung beide Interfaces implementiert, ist das Erschleichen durch simples Casten möglich, allerdings funktioniert das nur zur Design-Zeit.
Solche Aspekte würde ich daher nur einplanen, wenn es zwingend notwendig ist - also wenn es tatsächlich um sensible Daten geht und die Assembly von externen Entwicklern weiter verwendet wird.
In der eigenen Anwendung würde ich dem keine Bedeutung bei messen - und wenn es doch notwendig ist, eine entwprächende Aufteilung auf zwei Klassen ist nicht weiter schwer, die Interfaces ändern sich ja nicht ^^
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4700
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Di 08.12.15 22:36 
Zitat:
Dennoch würde ich immer ein Interface verwenden, wenn es geht. Schon alleine deshalb, weil eine Klasse nur von einer anderen Klasse erben kann.


Das was er tut würde ich vermutlich nie tun aber irgendwie muss man da ja weiterkommen ;) Proxyklassen halbwegs vernünftig implementiert funktionieren auch recht gut man denke nur an Nullable<T> (auch wenn da noch ein wenig Editormagie dranhängt)
Versuche mal das nur mit Interfaces ;)
Palladin007
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 1282
Erhaltene Danke: 182

Windows 11 x64 Pro
C# (Visual Studio Preview)
BeitragVerfasst: Di 08.12.15 23:34 
ausblenden volle Höhe C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
public interface INullable<T>
{
    bool HasValue { get; }
    T Value { get; }
}

public struct MyNullable<T> : INullable<T>
{
    private readonly T _value;

    public bool HasValue { get; }
    public T Value
    {
        get
        {
            if (!HasValue)
                throw new InvalidOperationException();

            return _value;
        }
    }

    public MyNullable() // geht nicht, ich weiß -_-
    {
        _value = default(T);
        HasValue = false;
    }
    public MyNullable(T value)
    {
        _value = value;
        HasValue = true;
    }
}


Ok, ich gebe mich geschlagen :D
Gegen das Beispiel Nullable lässt sich nichts sagen, das ist mit einem Interface einfach sinnlos :D


Dennoch sehe ich Proxy-Klassen als riskantes Spiel.
Nicht sauber umgesetzt stellt sie schnell eine Abhängigkeit dar, die sich mal eben überall durch zieht und nur sehr schwer wieder heraus zu bekommen ist.



Mich würde aber schon interessieren, wozu? :D
Für ein Rechtesystem könnte ich es mir vorstellen, allerdings würde ich eher irgendwo die Info hinterlassen, ob nun gelesen oder geschrieben werden darf und bei falschem Zugriff eine eigens dafür erstellte Exception (oder die UnauthorizedAccessException?) werfen, auf die ich dann gesondert horche. Das dürfte die einfachste Lösung sein, bedeutet wenig Mehraufwand und eine geringe Komplexität.
danielf Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 1012
Erhaltene Danke: 24

Windows XP
C#, Visual Studio
BeitragVerfasst: Mi 09.12.15 09:09 
Hallo und Danke für die Anregungen.

Es soll kein Rechtesystem implementiert werden, sondern es soll sichergestellt werden, dass

Schreibzugriffe nur innerhalb einer gewissen Umfang - nennen wir es Transaction - abläuft und somit parallele Schreibzugriffe koordiniert werden können.
Dieser Mechanismus soll durch Interfaces abgesichert werden, dass der Mechanismus nicht "aus versehen" vergessen wird.

Gruß
Daniel

Bezug auf: Palladin007, BeitragVerfasst: Di 08.12.15 18:29

"Das sieht sehr wirr aus und ich gebe zu, es drei mal lesen zu müssen um halbwegs zu kapieren, was Du mit den ganzen Interfaces bezwecken willst
Abgesehen davon wird das nie funktionieren, Du kannst nicht mit einem Interface dem COmpiler mit teilen, dass irgendeine Property auf einmal doch writable ist."

Dann solltest du es nochmal lesen, dass du die andere Hälfte auch verstehst. Das Beispiel funktioniert. Wie gesagt unschön finde ich nur die Type Konvertiertung mit as und die Tatsache, dass man das writeable -Interface doppelt angeben muss.

Bezug auf: Ralf Jansen BeitragVerfasst: Di 08.12.15 20:27

a) Proxyklasse
Finde ich gut. Hinsichtlich Datenverwaltung und dem (minimal) zusätzlichen Speichverbrauch wohl zu overlay.

b) Freezeable
Die Resonanz zur Laufzeit ist mir zu spät. Der Entwickler soll durch den compiler unterstützt werden.

c) Kopie
Wie bei b) gibt es dafür dann keine compiler unterstützung und noch schlimmer finde ich, dass man nicht mal zur Laufzeit feedback bekommt, dass man das Objekt nicht verändert hat. Ich weiß, dass dies ein Pattern ist, aber wie gesagt ich suche eher einen Mechanismus um während der Entwicklung Änderungen zu verhindern. Und mit verhindern ist nicht gemeint, dass man es gar nicht kann (über reflection oder cast), sondern wenn man mit den Interfaces arbeitet. Type-Konvertierungen und Reflection halte ich für schlechten Style - auch wenn es manchmal nicht anders geht.
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4700
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Mi 09.12.15 10:55 
Zitat:
Schreibzugriffe nur innerhalb einer gewissen Umfang - nennen wir es Transaction - abläuft und somit parallele Schreibzugriffe koordiniert werden können.


Unabhängig von deiner technischen Frage würden mir da jetzt ReaderWriterLocks einfallen.

Zitat:
Dieser Mechanismus soll durch Interfaces abgesichert werden, dass der Mechanismus nicht "aus versehen" vergessen wird.


Ok, also 2 Interfaces und man erstellt den betreffenden Code jeweils gegen das jeweils notwendige Interface (die konkrete Klasse wird nie verwendet). Der Sinn des ganzen Klassenunterbaus erschließt sich mir aber dann immer noch nicht. Wenn dein Code funktioniert wie du sagst was ist der Vorteil von var writeable = name.GetWritable(); gegenüber var writeable = (Write.IName)name. Letzteres wäre einfach zu erreichen indem das ~write~ Interface explicit implementiert wird ohne irgendwelchen Basisklassenunterbau.
Palladin007
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 1282
Erhaltene Danke: 182

Windows 11 x64 Pro
C# (Visual Studio Preview)
BeitragVerfasst: Mi 09.12.15 13:00 
Als kleiner Gedankenanstoß zur Idee, eine Kopie zu erstellen:
Excel macht es vermutlich ähnlich: Es wird eine Kopie angelegt und der Versuch, die zu speichern, führt dazu, dass nach einem neuen Speicherort gefragt wird.
Dafür könntest Du dann das ICloneable-Interface implementieren, das setzt intern dann ein Flag, was mit teilt, ob es sich um eine Kopie handelt oder nicht. So weißt Du immer, wenn es eine Kopie ist.

Der Weg mit ReaderWriterLocks (Kannte ich bisher auch noch nicht, danke dafür) oder die Variante, das Objekt zu kopieren, finde ich am besten.
danielf Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 1012
Erhaltene Danke: 24

Windows XP
C#, Visual Studio
BeitragVerfasst: Mi 09.12.15 14:46 
ReaderWriteLocks habe ich mir angeschaut. Von der Performance bin ich aber nicht angetan :)

Der Plan ist für Read-Operationen keine synchronisation vorzunehmen. Die Schreibzugriffe soll über einen Thread synchronisiert werden.

Wenn man das Interface direkt casted muss man a) genau das inteface angeben und b) kann kein Code ausgeführt werden.
ausblenden C#-Quelltext
1:
var writeable = (Write.IName)name;					


Verlagert man die Implementierung in eine Methode
ausblenden C#-Quelltext
1:
var writeable = name.GetWritable()					

So kann in GetWritable beliebiger Code ausgeführt werden. z.B. eine Copy angelegt werden, NotifyPropertyChanged-events abonniert werden usw. Damit könnte man zum Beispiel feststellen, wenn jemand anderes das Element bearbeitet, wärend ich ein client es schreiben will. Durch ein Event könnte man dann darauf reagieren -> z.b. autmatischer merge, andere Änderungen verwerfen usw.

Wie gesagt, hautpsächlich stört mich bei dem Ansatz die Typ-Umwandlung und die redundante Angabe des Write-Interfaces.

Trotzdem bin ich offen für Vorschläge wie der ReaderWriterLock.

Danke und Gruß
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4700
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Mi 09.12.15 15:03 
Zitat:
Der Plan ist für Read-Operationen keine synchronisation vorzunehmen. Die Schreibzugriffe soll über einen Thread synchronisiert werden.


Das würde ich im allgemeinen Fall als problematisch ansehen. Das du write immer als atomare Aktion hinbekommst ist eher unwahrscheinlich. Um also während des writes keine halbgaren falschen reads zu bekommen sollten während einem write(exclusiver Vorgang) die reads gelockt sein (Eben das was ein ReadWriterLock tut).
Bezüglich der Performance die Slim Version der ReaderWriteLocks ist eigentlich ganz ok.

Als weitere Möglichkeit um das ganze lockfrei aber threadsicher hinzubekommen wäre dann noch alle Klassen prinzipiell immutable zu machen und nicht zwischen read/write oder original/Copy zu unterscheiden. Das ist als Pattern am ehesten beherschbar.