Frage an c#, readonly – schreibgeschütztes Klassendesign, wenn bereits eine nicht schreibgeschützte Klasse vorhanden ist

13

Ich habe eine Klasse, die beim Erstellen Informationen aus einer Datenbank lädt. Die Informationen können alle geändert werden. Anschließend kann der Entwickler Save () aufrufen, um sie in die Datenbank zurückzuspeichern.

Ich erstelle auch eine Klasse, die aus der Datenbank geladen wird, aber keine Aktualisierungen zulässt. (Eine schreibgeschützte Version.) Meine Frage ist, sollte ich eine separate Klasse erstellen und erben, oder sollte ich einfach das vorhandene Objekt aktualisieren, um einen schreibgeschützten Parameter im Konstruktor zu übernehmen, oder sollte ich eine separate Klasse vollständig erstellen?

Die vorhandene Klasse wird bereits an vielen Stellen im Code verwendet.

Vielen Dank.

Aktualisieren:

Erstens gibt es hier viele gute Antworten. Es wäre schwer, nur einen zu akzeptieren. Vielen Dank an alle.

Die Hauptprobleme scheinen zu sein:

Erfüllen von Erwartungen basierend auf Klassennamen und Vererbungsstrukturen.Vermeiden Sie unnötigen doppelten Code

Es scheint einen großen Unterschied zwischen Readable und ReadOnly zu geben. Eine Readonly-Klasse sollte wahrscheinlich nicht vererbt werden. Eine Readable-Klasse deutet jedoch darauf hin, dass sie möglicherweise irgendwann auch wieder beschreibbar wird.

Nach langen Überlegungen überlege ich mir Folgendes:

<code>public class PersonTestClass
{
    public static void Test()
    {

        ModifiablePerson mp = new ModifiablePerson();
        mp.SetName("value");
        ReadOnlyPerson rop = new ReadOnlyPerson();
        rop.GetName();
        //ReadOnlyPerson ropFmp = (ReadOnlyPerson)mp;  // not allowed.
        ReadOnlyPerson ropFmp = (ReadOnlyPerson)(ReadablePerson)mp; 
          // above is allowed at compile time (bad), not at runtime (good).
        ReadablePerson rp = mp;
    }
}

public class ReadablePerson
{
    protected string name;
    public string GetName()
    {
        return name;
    }        
}
public sealed class ReadOnlyPerson : ReadablePerson
{
}
public class ModifiablePerson : ReadablePerson
{
    public void SetName(string value)
    {
        name = value;
    }
}
</code>

Leider weiß ich noch nicht, wie man dies mit Eigenschaften macht (siehe die Antwort von StriplingWarrior für diese mit Eigenschaften), aber ich habe das Gefühl, dass es sich um das geschützte Schlüsselwort und handeltasymmetrische Eigenschaftszugriffsmodifikatoren.

Zum Glück müssen die aus der Datenbank geladenen Daten nicht in Referenzobjekte umgewandelt werden, sondern es handelt sich um einfache Typen. Das heißt, ich muss mich nicht wirklich darum kümmern, dass Leute die Mitglieder der ändernReadOnlyPerson Objekt.

Update 2:

Beachten Sie, wie StriplingWarrior angedeutet hat, dass Downcasting zu Problemen führen kann. Dies gilt jedoch im Allgemeinen, da es schlecht sein kann, einen Affen und ein Tier zurück zu einem Hund zu werfen. Es scheint jedoch, dass das Casting zur Kompilierungszeit zwar zulässig, zur Laufzeit jedoch nicht zulässig ist.

Eine Wrapper-Klasse kann auch den Trick ausführen, aber das gefällt mir besser, weil es das Problem vermeidet, das übergebene Objekt tief zu kopieren / das übergebene Objekt modifizieren zu lassen, wodurch die Wrapper-Klasse modifiziert wird.

Deine Antwort

5   die antwort
5

IReadablePerson (etc) interface, whic, h enthält nur get-Eigenschaften und nicht Save (). Anschließend können Sie diese Schnittstelle von Ihrer vorhandenen Klasse implementieren lassen. Wenn Sie nur Lesezugriff benötigen, muss der konsumierende Code die Klasse über diese Schnittstelle referenzieren.

Entsprechend dem Muster möchten Sie wahrscheinlich eine habenIReadWritePerson Schnittstelle, die die Setter und enthalten würdeSave().

Bearbeiten Bei weiterem NachdenkenIWriteablePerson sollte wohl seinIReadWritePerson, da es nicht viel Sinn macht, eine reine Schreibklasse zu haben.

Beispiel:

<code>public interface IReadablePerson
{
    string Name { get; }
}

public interface IReadWritePerson : IReadablePerson
{
    new string Name { get; set; }
    void Save();
}

public class Person : IReadWritePerson
{
    public string Name { get; set; }
    public void Save() {}
}
</code>
+1 für mich gefällt diese Idee. Aber ist es einfach, eine solche Schnittstelle mit Eigenschaften zu erstellen? Als ob man den Namen {get;} und den anderen Namen {set; }? user420667
Absolut. Siehe das Beispiel, das ich hinzugefügt habe, oder siehe auch die Antwort von @Olivier Jacot-Descombes. Steve Czetty
4

Wie möchten Sie eine modifizierbare Klasse in eine schreibgeschützte Klasse umwandeln, indem Sie von ihr erben?" Mit der Vererbung können Sie eine Klasse erweitern, aber nicht einschränken. Wenn Sie dies tun, indem Sie Ausnahmen auslösen, würde dies die Richtlinie verletzenLiskov-Substitutionsprinzip (LSP).

Das Ableiten einer modifizierbaren Klasse von einer schreibgeschützten Klasse wäre in dieser Hinsicht in Ordnung. Wie möchten Sie jedoch eine schreibgeschützte Eigenschaft in eine schreibgeschützte Eigenschaft umwandeln? Und ist es darüber hinaus wünschenswert, ein modifizierbares Objekt ersetzen zu können, wenn ein schreibgeschütztes Objekt erwartet wird?

Sie können dies jedoch mit Schnittstellen tun

<code>interface IReadOnly
{
    int MyProperty { get; }
}

interface IModifiable : IReadOnly
{
    new int MyProperty { set; }
    void Save();
}
</code>

Diese Klasse ist zuweisungskompatibel zuIReadOnly Schnittstelle als auch. In schreibgeschützten Kontexten können Sie über dieIReadOnly Schnittstelle.

<code>class ModifiableClass : IModifiable
{
    public int MyProperty { get; set; }
    public void Save()
    {
        ...
    }
}
</code>

AKTUALISIEREN

Ich habe weitere Untersuchungen zu diesem Thema durchgeführt.

Allerdings gibt es eine Einschränkung, ich musste eine hinzufügennew Stichwort inIModifiable und Sie können den Getter nur entweder direkt über denModifiableClass oder durch dieIReadOnly Schnittstelle, aber nicht durch dieIModifiable Schnittstelle.

Ich habe auch versucht, mit zwei Schnittstellen zu arbeitenIReadOnly undIWriteOnly nur einen Getter bzw. einen Setter haben. Sie können dann eine Schnittstelle deklarieren, die von beiden erbt, und nonew Das Schlüsselwort ist vor der Eigenschaft erforderlich (wie inIModifiable). Wenn Sie jedoch versuchen, auf die Eigenschaft eines solchen Objekts zuzugreifen, wird der Compilerfehler angezeigtAmbiguity between 'IReadOnly.MyProperty' and 'IWriteOnly.MyProperty'.

Offensichtlich ist es nicht möglich, eine Eigenschaft aus getrennten Gettern und Setzern zu synthetisieren, wie ich erwartet hatte.

Sie müssen keine zwei separaten Klassen implementieren, solange Sie die IReadOnly-Schnittstelle verwenden, wenn Sie die Klasse in einem schreibgeschützten Kontext verwenden möchten. Steve Czetty
6

eibbaren Klasse erben. Abgeleitete Klassen sollten die Funktionen der Basisklasse erweitern und ändern. Sie sollten niemals Fähigkeiten wegnehmen.

Sie können die beschreibbare Klasse möglicherweise von der schreibgeschützten Klasse erben lassen, müssen dies jedoch sorgfältig tun. Die zentrale Frage ist, ob sich Verbraucher der schreibgeschützten Klasse darauf verlassen können, dass sie schreibgeschützt sind. Wenn ein Verbraucher damit rechnet, dass sich die Werte nie ändern, der beschreibbare abgeleitete Typ jedoch übergeben wird und die Werte dann geändert werden, kann dies zu Fehlern führen.

Ich weiß, es ist verlockend zu glauben, dass, da die Struktur der beiden Typen (d. H. Der darin enthaltenen Daten) ähnlich oder identisch ist, einer von dem anderen erben sollte. Das ist aber oft nicht der Fall. Wenn sie für erheblich unterschiedliche Anwendungsfälle entwickelt wurden, müssen sie wahrscheinlich separate Klassen sein.

Sie können keine Umsetzungsoperatoren für Typen in derselben Hierarchie überschreiben (und selbst wenn Sie dies könnten, wäre dies eine schreckliche Idee). Ein Typ kann immer spontan in seinen Basistyp konvertiert werden. David Nelson
Verdammt, das ist eine sehr faire und nervige Angelegenheit. user420667
Danke für das gedanken Experiment. Vielleicht bedeutet das nur, dass ich die impliziten und expliziten Casting-Operatoren überschreiben sollte, um das Casting zu verhindern? Aber diese Art der Niederlage hat doch den Zweck der Vererbung, oder? Verdammt. Es muss einen besseren Weg geben. user420667
Ich würde annehmen, dass der richtige Ansatz darin besteht, eine abstrakte "lesbare" Klasse (oder Schnittstelle) zu haben, die dann sowohl beschreibbare als auch unveränderbare abgeleitete Klassen (oder Schnittstellen) haben könnte. Während einige Benutzer einer Auflistungsklasse sich darauf verlassen können, dass sie unveränderlich ist, und andere sich darauf verlassen können, dass sie unveränderlich ist, kümmert es eine ganze Menge nicht, ob sie es auf die eine oder andere Weise lesen können. Es gibt keinen Grund, warum es Methoden egal ist, ob eine Klasse veränderbar ist oder nicht. supercat
13

Liskov-Substitutionsprinzip gibt an, dass Sie Ihre schreibgeschützte Klasse nicht von Ihrer schreibgeschützten Klasse erben lassen sollten, da konsumierende Klassen wissen müssen, dass sie die Save-Methode nicht ohne Ausnahme aufrufen können.

Wenn Sie die beschreibbare Klasse erweitern, ist die lesbare Klasse für mich sinnvoller, solange die lesbare Klasse nichts enthält, was darauf hinweist, dass ihr Objekt niemals beibehalten werden kann. Zum Beispiel würde ich die Basisklasse a nicht nennenReadOnly[Whatever], denn wenn du eine Methode hast, die aReadOnlyPerson Als Argument wäre diese Methode gerechtfertigt, wenn man annimmt, dass nichts, was sie mit diesem Objekt anstellen, Auswirkungen auf die Datenbank haben kann, was nicht unbedingt der Fall sein muss, wenn es sich bei der tatsächlichen Instanz um eine handeltWriteablePerson.

Aktualisieren

Ich war ursprünglich davon ausgegangen, dass Sie in Ihrer schreibgeschützten Klasse nur verhindern wollten, dass Leute die anrufenSave Methode. Basierend auf dem, was ich in Ihrer Antwort auf Ihre Frage sehe (die eigentlich eine Aktualisierung Ihrer Frage sein sollte), ist hier ein Muster, dem Sie möglicherweise folgen möchten:

<code>public abstract class ReadablePerson
{

    public ReadablePerson(string name)
    {
        Name = name;
    }

    public string Name { get; protected set; }

}

public sealed class ReadOnlyPerson : ReadablePerson
{
    public ReadOnlyPerson(string name) : base(name)
    {
    }
}

public sealed class ModifiablePerson : ReadablePerson
{
    public ModifiablePerson(string name) : base(name)
    {
    }
    public new string Name { 
        get {return base.Name;}
        set {base.Name = value; }
    }
}
</code>

Dies stellt sicher, dass ein wirklichReadOnlyPerson kann nicht einfach als ModifiablePerson umgewandelt und modifiziert werden. Wenn Sie jedoch darauf vertrauen möchten, dass Entwickler nicht versuchen, Argumente auf diese Weise herunterzuschreiben, bevorzuge ich den schnittstellenbasierten Ansatz in den Antworten von Steve und Olivier.

Eine andere Möglichkeit wäre es, IhreReadOnlyPerson Sei einfach eine Wrapper-Klasse für aPerson Objekt. Dies würde mehr Boilerplate-Code erfordern, aber es ist nützlich, wenn Sie die Basisklasse nicht ändern können.

Ein letzter Punkt, da Sie gerne das Liskov-Substitutionsprinzip kennengelernt haben: Indem die Person-Klasse dafür verantwortlich ist, sich selbst aus der Datenbank zu laden, brechen Sie das Prinzip der einfachen Verantwortung. Im Idealfall verfügt Ihre Personenklasse über Eigenschaften zur Darstellung der Daten, die eine "Person" umfassen, und es gibt eine andere Klasse (möglicherweise einePersonRepository), der dafür verantwortlich ist, eine Person aus der Datenbank zu erzeugen oder eine Person in der Datenbank zu speichern.

Update 2

Auf Ihre Kommentare antworten:

Während Sie Ihre eigene Frage technisch beantworten können, geht es bei StackOverflow hauptsächlich darum, Antworten von anderen Personen zu erhalten. ist der Grund, warum Sie Ihre eigene Antwort erst akzeptieren können, wenn eine bestimmte Nachfrist abgelaufen ist. Sie werden aufgefordert, Ihre Frage zu verfeinern und auf Kommentare und Antworten zu antworten, bis jemand eine angemessene Lösung für Ihre ursprüngliche Frage gefunden hat.Ich habe das gemachtReadablePerson Klasseabstract weil es so aussah, als wollten Sie immer nur eine Person erstellen, die schreibgeschützt oder beschreibbar ist. Auch wenn beide untergeordneten Klassen als lesbare Person betrachtet werden könnten, was wäre der Sinn der Erstellung einernew ReadablePerson() wenn man genauso einfach einen erstellen könntenew ReadOnlyPerson()? Damit die Klasse abstrakt wird, muss der Benutzer beim Instanziieren eine der beiden untergeordneten Klassen auswählen.Ein PersonRepository ähnelt einer Fabrik, aber das Wort "Repository" zeigt an, dass Sie die Informationen einer Person tatsächlich aus einer Datenquelle ziehen, anstatt die Person aus dem Nichts zu schaffen.

In meinen Augen wäre die Person-Klasse nur ein POCO ohne Logik: nur Eigenschaften. Repository ist für die Erstellung des Personenobjekts verantwortlich. Anstatt zu sagen:

<code>// This is what I think you had in mind originally
var p = new Person(personId);
</code>

... und dem Person-Objekt erlauben, in die Datenbank zu gelangen, um die verschiedenen Eigenschaften zu füllen, würden Sie sagen:

<code>// This is a better separation of concerns
var p = _personRepository.GetById(personId);
</code>

PersonRepository würde dann die entsprechenden Informationen aus der Datenbank abrufen und die Person mit diesen Daten erstellen.

Wenn Sie eine Methode aufrufen möchten, für die es keinen Grund gibt, die Person zu ändern, können Sie diese Person vor Änderungen schützen, indem Sie sie in einen schreibgeschützten Wrapper konvertieren (entsprechend dem Muster, das die .NET-Bibliotheken mit dem bezeichnen)ReadonlyCollection<T> Klasse). Auf der anderen Seite könnten Methoden, die ein beschreibbares Objekt erfordern, mit dem folgenden Attribut versehen werden:Person direkt:

<code>var person = _personRepository.GetById(personId);
// Prevent GetVoteCount from changing any of the person's information
int currentVoteCount = GetVoteCount(person.AsReadOnly()); 
// This is allowed to modify the person. If it does, save the changes.
if(UpdatePersonDataFromLdap(person))
{
     _personRepository.Save(person);
}
</code>

Der Vorteil der Verwendung von Schnittstellen besteht darin, dass Sie keine bestimmte Klassenhierarchie erzwingen. Dies gibt Ihnen in Zukunft eine bessere Flexibilität. Nehmen wir zum Beispiel an, Sie schreiben Ihre Methoden für den Moment wie folgt:

<code>GetVoteCount(ReadablePerson p);
UpdatePersonDataFromLdap(ReadWritePerson p);
</code>

... aber dann entscheiden Sie sich in zwei Jahren, auf die Wrapper-Implementierung umzusteigen. PlötzlichReadOnlyPerson ist nicht mehr aReadablePerson, weil es eine Wrapper-Klasse anstelle einer Erweiterung einer Basisklasse ist. Veränderst du dich?ReadablePerson zuReadOnlyPerson in all Ihren Methodensignaturen?

Oder sagen Sie, Sie möchten die Dinge vereinfachen und alle Ihre Klassen zu einer einzigen zusammenfassenPerson Klasse: Jetzt müssen Sie alle Ihre Methoden ändern, um nur Personenobjekte zu nehmen. Andererseits, wenn Sie auf Schnittstellen programmiert hatten:

<code>GetVoteCount(IReadablePerson p);
UpdatePersonDataFromLdap(IReadWritePerson p);
</code>

... dann ist es diesen Methoden egal, wie Ihre Objekthierarchie aussieht, solange die Objekte, die Sie ihnen geben, die Schnittstellen implementieren, nach denen sie fragen. Sie können Ihre Implementierungshierarchie jederzeit ändern, ohne diese Methoden ändern zu müssen.

Aber ich sehe die Schnittstellen immer noch nicht alsbesser Option, obwohl sie eine Option sind. 1) Selbst wenn ich ReadonlyPerson als Wrapper ändern würde, würde ich die abgeleitete Natur nicht ändern. 2) Würden die Argumente nicht genauso gut zutreffen, wenn ich die Schnittstellenstruktur konsolidieren / ändern wollte? Ich habe das Gefühl, dass diese Diskussion vielleicht woanders hingehört, vielleicht Chat / eine separate Frage / E-Mail. Ich finde es aber gut. user420667
danke für die ausführlichen antworten. Ich muss ein bisschen darüber nachdenken. user420667
@ user420667: Ich würde es vorziehenReadWritePerson, ModifiablePersonoder auch nurPerson. Ich habe meine Antwort auch mit weiteren Hinweisen aktualisiert. Lassen Sie mich wissen, wenn Sie weitere Fragen haben. StriplingWarrior
Könnten Sie auch erklären, was am Schnittstellenansatz vorzuziehen ist? user420667
0

rsicherheitsberechtigungen zu lösen, das in bestimmten Fällen veränderlich sein muss, damit Benutzer auf hoher Ebene Sicherheitseinstellungen ändern können. Normalerweise ist es jedoch schreibgeschützt, um die Berechtigungsinformationen des derzeit angemeldeten Benutzers zu speichern ohne dass der Code diese Berechtigungen im laufenden Betrieb ändern kann.

Das Muster, das ich mir ausgedacht habe, bestand darin, eine Schnittstelle zu definieren, die das veränderbare Objekt implementiert und die schreibgeschützte Eigenschaften erhält. Die veränderbare Implementierung dieser Schnittstelle kann dann privat sein, sodass Code, der sich direkt mit der Instanziierung und Hydratisierung des Objekts befasst, dies tun kann. Sobald das Objekt jedoch aus diesem Code (als Instanz der Schnittstelle) zurückgegeben wird, sind die Setter nicht mehr verfügbar .

Beispiel:

<code>//this is what "ordinary" code uses for read-only access to user info.
public interface IUser
{
   string UserName {get;}
   IEnumerable<string> PermissionStrongNames {get;}

   ...
}

//This class is used for editing user information.
//It does not implement the interface, and so while editable it cannot be 
//easily used to "fake" an IUser for authorization
public sealed class EditableUser 
{
   public string UserName{get;set;}
   List<SecurityGroup> Groups {get;set;}

   ...
}

...

//this class is nested within the class responsible for login authentication,
//which returns instances as IUsers once successfully authenticated
private sealed class AuthUser:IUser
{
   private readonly EditableUser user;

   public AuthUser(EditableUser mutableUser) { user = mutableUser; }

   public string UserName {get{return user.UserName;}}

   public IEnumerable<string> PermissionNames 
   {
       //GetPermissions is an extension method that traverses the list of nestable Groups.
       get {return user.Groups.GetPermissions().Select(p=>p.StrongName);
   }

   ...
}
</code>

Mit einem solchen Muster können Sie Code verwenden, den Sie bereits mit Lese- und Schreibzugriff erstellt haben, ohne dass Joe Programmer aus einer schreibgeschützten Instanz eine veränderbare machen kann. In meiner eigentlichen Implementierung gibt es einige weitere Tricks, die sich hauptsächlich mit der Persistenz des bearbeitbaren Objekts befassen (da das Bearbeiten von Benutzerdatensätzen eine gesicherte Aktion ist, kann ein EditableUser nicht mit der "normalen" Persistenzmethode des Repository gespeichert werden, sondern erfordert stattdessen den Aufruf einer Überladung, die nimmt auch einen IUser (der ausreichende Berechtigungen haben muss).

Eine Sache, die Sie einfach verstehen müssen; Wenn es Ihrem Programm möglich ist, die Datensätze in einem beliebigen Bereich zu bearbeiten, kann diese Fähigkeit absichtlich oder auf andere Weise missbraucht werden. Regelmäßige Codeüberprüfungen der Verwendung der veränderlichen oder unveränderlichen Formen Ihres Objekts sind erforderlich, um sicherzustellen, dass andere Codierer nichts "Kluges" tun. Dieses Muster reicht auch nicht aus, um sicherzustellen, dass eine von der Öffentlichkeit verwendete Anwendung sicher ist. Wenn Sie wie ein Angreifer eine IUser-Implementierung schreiben können, müssen Sie zusätzlich überprüfen, ob Ihr Code und nicht ein Angreifer eine bestimmte IUser-Instanz erstellt hat und ob die Instanz in der Zwischenzeit nicht manipuliert wurde.

Was Sie in diesem Fall daran hindert, ist, dass Sie nicht auf AuthUser zugreifen können. Es ist eine private verschachtelte Klasse, auf die von außerhalb des Objekts, das die Anmeldeauthentifizierung durchführt, nicht zugegriffen werden kann. Joe Programmer könnte immer noch seine eigene IUser-Implementierung schreiben, aber wenn ich der Senior-Entwickler oder Architekt von Joe bin, könnte ich ReSharper oder einen cleveren Komponententest verwenden, um die Codebasis nach IUnit-Implementierungen zu durchsuchen, die ich nicht autorisiert habe. Wenn Joe ein Angreifer wäre, würde ich in Betracht ziehen, einen von meinem (versiegelten und verschleierten) Anmeldeauthentifizierer generierten Hashwert hinzuzufügen, mit dem schnell überprüft werden kann, ob mein Code einen IUser generiert hat oder nicht. KeithS
Interessantes Beispiel. Was hindert mich jedoch daran, einen AuthUser mit einem bearbeitbaren Benutzer zu erstellen und dann den Namen des bearbeitbaren Benutzers zu ändern, wodurch der Name des Authusers geändert wird, was angesichts der Absicht der IUser-Benutzeroberfläche nicht zulässig sein sollte, oder? user420667
Auf jeden Fall habe ich gesagt, dass die Fähigkeit, zwischen veränderlichen und unveränderlichen Instanzen eines Objekts zu wechseln, in jeder Situation, die Auswirkungen auf die Sicherheit haben könnte, genau überwacht werden muss. Einige dieser anderen Vorschläge sind nicht weniger anfällig. KeithS

Verwandte Fragen