Pytanie w sprawie c#, readonly – projekt klasy tylko do odczytu, gdy klasa nie-readonly jest już na miejscu

13

Mam klasę, która po zbudowaniu ładuje informacje z bazy danych. Informacje są modyfikowalne, a następnie programista może wywołać w nim Save (), aby zapisać te informacje z powrotem do bazy danych.

Tworzę także klasę, która ładuje się z bazy danych, ale nie zezwala na żadne aktualizacje. (wersja tylko do odczytu.) Moje pytanie brzmi: czy powinienem utworzyć oddzielną klasę i dziedziczyć, czy też powinienem zaktualizować istniejący obiekt, aby w konstruktorze pobrać parametr tylko do odczytu, czy też powinienem utworzyć osobną klasę w całości?

Istniejąca klasa jest już używana w wielu miejscach kodu.

Dzięki.

Aktualizacja:

Po pierwsze, jest tu wiele świetnych odpowiedzi. Trudno byłoby zaakceptować tylko jedną. Dziękuję wszystkim.

Główne problemy wydają się:

Spełnianie oczekiwań w oparciu o nazwy klas i struktury dziedziczenia.Zapobieganie niepotrzebnemu duplikatowi kodu

Wydaje się, że istnieje duża różnica między Readable i ReadOnly. Klasa Readonly prawdopodobnie nie powinna być dziedziczona. Ale klasa czytelna sugeruje, że w pewnym momencie może również zyskać zdolność zapisu.

Więc po wielu przemyśleniach oto, o czym myślę:

<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>

Niestety, nie wiem jeszcze, jak to zrobić za pomocą właściwości (patrz odpowiedź StriplingWarrior na to, co zostało zrobione z właściwościami), ale mam wrażenie, że będzie to dotyczyło chronionego słowa kluczowego iasymetryczne modyfikatory dostępu do własności.

Na szczęście dla mnie dane, które są ładowane z bazy danych, nie muszą być przekształcane w obiekty referencyjne, są to raczej typy proste. Oznacza to, że tak naprawdę nie muszę się martwić o ludzi modyfikujących członkówReadOnlyPerson obiekt.

Aktualizacja 2:

Zauważ, jak zasugerował StriplingWarrior, downcasting może prowadzić do problemów, ale jest to na ogół prawdą, ponieważ rzucenie Małpy na i Zwierzę z powrotem na Psa może być złe. Wydaje się jednak, że mimo że casting jest dozwolony w czasie kompilacji, nie jest on faktycznie dozwolony w czasie wykonywania.

Klasa opakowująca może również załatwić sprawę, ale podoba mi się to lepiej, ponieważ pozwala uniknąć problemu głębokiego kopiowania przekazanego obiektu / umożliwia modyfikację przekazanego obiektu, modyfikując tym samym klasę opakowania.

Twoja odpowiedź

5   odpowiedzi
5

Szybką opcją może być utworzenieIReadablePerson Interfejs (etc), który zawiera tylko właściwości get i nie zawiera Save (). Następnie możesz zaimplementować ten interfejs w istniejącej klasie, a tam, gdzie potrzebujesz dostępu tylko do odczytu, użyj kodu odwołującego się do klasy poprzez ten interfejs.

Zgodnie ze wzorem, prawdopodobnie chcesz miećIReadWritePerson także interfejs, który zawierałby setery iSave().

Edytować Na dalszą myślIWriteablePerson powinien byćIReadWritePerson, ponieważ nie ma sensu mieć klasy tylko do zapisu.

Przykład:

<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>
Absolutnie. Zobacz przykład, który dodałem, lub zobacz także odpowiedź @Olivier Jacot-Descombes. Steve Czetty
+1 dla mnie lubię ten pomysł. Ale łatwo jest stworzyć taki interfejs z właściwościami? Jakby można było mieć Imię i drugie imię; }? user420667
6

Zdecydowanie nie należy dziedziczyć klasy tylko do odczytu z klasy zapisywalnej. Klasy pochodne powinny rozszerzać i modyfikować możliwości klasy bazowej; nigdy nie powinni zabierać możliwości.

Możesz sprawić, że zapisywalna klasa dziedziczy z klasy tylko do odczytu, ale musisz to zrobić ostrożnie. Kluczowym pytaniem, które należy zadać, jest to, czy jakikolwiek konsument klasy tylko do odczytu będzie polegał na tym, że jest tylko do odczytu? Jeśli konsument liczy na to, że wartości nigdy się nie zmienią, ale zapisany typ pochodny zostanie przekazany, a następnie zmienione, konsument może zostać uszkodzony.

Wiem, że kuszące jest myślenie, że ponieważ struktura tych dwóch typów (tj. Danych, które zawierają) jest podobna lub identyczna, należy odziedziczyć po drugiej. Ale często tak nie jest. Jeśli są one zaprojektowane dla znacznie różnych przypadków użycia, prawdopodobnie muszą być oddzielnymi klasami.

Kochanie, to bardzo uczciwa i denerwująca troska. user420667
Zakładam, że właściwym podejściem byłoby posiadanie abstrakcyjnej „czytelnej” klasy (lub interfejsu), która mogłaby mieć zarówno zapisywalne, jak i niezmienne klasy pochodne (lub interfejsy). Podczas gdy niektórzy użytkownicy klasy kolekcji mogą polegać na tym, że są niezmienni, a inni mogą polegać na tym, że są niezmienne, straszna ilość nie będzie się tym przejmować, pod warunkiem, że będą mogli ją przeczytać. Nie ma powodu, dla którego metody, które nie mają znaczenia, czy dana klasa jest zmienna, nie powinny działać w ten sam sposób na zmiennych i niezmiennych. supercat
Nie można przesłonić operatorów rzutowania dla typów w tej samej hierarchii (a nawet gdyby można było, byłoby to okropnym pomysłem). Typ może być zawsze bezbłędnie przekonwertowany na typ bazowy. David Nelson
Dziękuję za eksperyment gedanken. Być może oznacza to, że powinienem zastąpić niejawne i jawne operatory rzutowania, aby zapobiec rzutowaniu? Ale w takim razie pokonuje cel dziedziczenia, prawda? Cerować. Musi być lepszy sposób. user420667
0

Miałem ten sam problem do rozwiązania przy tworzeniu obiektu dla uprawnień użytkownika, który w pewnych przypadkach musi być zmienny, aby umożliwić użytkownikom wysokiego poziomu modyfikowanie ustawień zabezpieczeń, ale normalnie jest tylko do odczytu, aby przechowywać informacje o uprawnieniach aktualnie zalogowanego użytkownika bez zezwolenia na modyfikację tych uprawnień w locie.

Wzór, który wymyśliłem, polegał na zdefiniowaniu interfejsu, który implementuje obiekt mutable, który ma gettery właściwości tylko do odczytu. Zmienna implementacja tego interfejsu może być wtedy prywatna, umożliwiając kodowi, który bezpośrednio zajmuje się tworzeniem i nawadnianiem obiektu, aby to zrobić, ale po zwróceniu obiektu z tego kodu (jako instancji interfejsu) setery nie są już dostępne .

Przykład:

<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>

Wzorzec taki jak ten pozwala na użycie kodu, który już utworzyłeś w trybie do odczytu i zapisu, nie pozwalając programistom Joe na przekształcenie instancji tylko do odczytu w zmienną. W mojej rzeczywistej implementacji jest jeszcze kilka sztuczek, głównie zajmujących się trwałością edytowalnego obiektu (ponieważ edytowanie rekordów użytkowników jest zabezpieczoną akcją, nie można zapisać EditableUser za pomocą „normalnej” metody trwałości Repozytorium; wymaga to wywołania przeciążenia, które bierze również IUser, który musi mieć wystarczające uprawnienia).

Jedna rzecz, którą po prostu musisz zrozumieć; jeśli Twój program może edytować rekordy w jakimkolwiek zakresie, możliwe jest nadużywanie tej zdolności, celowo lub w inny sposób. Konieczne będą regularne przeglądy kodu dotyczące użycia zmiennych lub niezmiennych form obiektu, aby upewnić się, że inni kodery nie robią niczego „mądrego”. Ten wzorzec również nie wystarcza, aby zapewnić bezpieczeństwo aplikacji używanej przez ogół społeczeństwa; jeśli możesz napisać implementację IUser, może to zrobić atakujący, więc będziesz potrzebował dodatkowego sposobu na sprawdzenie, czy Twój kod, a nie atakujący, wytworzył konkretną instancję IUser, a instancja nie została w międzyczasie zmodyfikowana.

Ciekawy przykład. Jednak co powstrzymuje mnie przed tworzeniem AuthUser z edytorem, a następnie zmianą nazwy użytkownika edytora, zmieniając w ten sposób nazwę authuser, która nie powinna być dozwolona, ​​biorąc pod uwagę intencje interfejsu IUser, prawda? user420667
W każdym razie powiedziałem, że zdolność do przechodzenia między zmiennymi i niezmiennymi instancjami obiektu musi być ściśle monitorowana w każdej sytuacji, która może mieć reperkusje bezpieczeństwa. Niektóre z tych innych sugestii są nie mniej wrażliwe. KeithS
W tym przypadku powstrzymuje cię to, że nie możesz uzyskać dostępu do AuthUser; jest to prywatna klasa zagnieżdżona niedostępna z zewnątrz obiektu, który wykonuje uwierzytelnianie logowania. Joe Programmer nadal może napisać własną implementację IUser, ale jeśli jestem starszym debiutantem lub architektem nad Joe, mógłbym użyć ReSharpera lub sprytnego testu jednostkowego do przeszukiwania bazy kodu dla implementacji IUnit, których nie autoryzowałem. Gdyby Joe był atakującym, rozważyłbym dodanie wartości skrótu, wygenerowanej przez mój (zapieczętowany i zaciemniony) uwierzytelniacz logowania, która mogłaby zostać użyta do szybkiego sprawdzenia, czy mój kod zrobił lub nie wygenerował użytkownika IUser. KeithS
13

TheZasada substytucji Liskowa mówi, że nie powinieneś dziedziczyć klasy tylko do odczytu z klasy do odczytu i zapisu, ponieważ używanie klas musiałoby być świadome, że nie mogą wywołać na niej metody Save bez uzyskania wyjątku.

Zwiększenie klasy do zapisu do klasy czytelnej byłoby dla mnie bardziej sensowne, o ile w klasie czytelnej nie ma niczego, co wskazuje, że jej obiekt nigdy nie może zostać utrwalony. Na przykład nie nazwałbym klasy bazowej aReadOnly[Whatever], ponieważ jeśli masz metodę, która wymagaReadOnlyPerson jako argument, metoda ta byłaby uzasadniona przy założeniu, że byłoby niemożliwe, aby cokolwiek, co robią temu obiektowi, miało jakikolwiek wpływ na bazę danych, co niekoniecznie jest prawdą, jeśli rzeczywistą instancją jestWriteablePerson.

Aktualizacja

Początkowo zakładałem, że w klasie tylko do odczytu chciałeś tylko uniemożliwić osobom dzwoniącymSave metoda. Na podstawie tego, co widzę w Twojej odpowiedzi-odpowiedzi na twoje pytanie (a tak na marginesie, co powinno być aktualizacją twojego pytania), oto wzór, który możesz chcieć śledzić:

<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>

Zapewnia to prawdziwieReadOnlyPerson nie można po prostu rzucić jako osoby modyfikowanej i zmodyfikować. Jeśli jednak chcesz zaufać, że programiści nie będą próbowali odrzucać argumentów w ten sposób, wolę podejście oparte na interfejsie w odpowiedziach Steve'a i Oliviera.

Inną opcją byłoby wykonanieReadOnlyPerson po prostu bądź klasą opakowującą dla aPerson obiekt. Wymagałoby to większej liczby standardowych kodów, ale przydaje się, gdy nie można zmienić klasy bazowej.

Ostatnia kwestia, ponieważ podobało ci się zapoznanie się z zasadą podstawienia Liskowa: Dzięki temu, że klasa osób jest odpowiedzialna za wyładowanie się z bazy danych, łamiesz zasadę pojedynczej odpowiedzialności. Najlepiej byłoby, gdyby twoja klasa Person miała właściwości reprezentujące dane, które zawierają „Osobę”, i istniałaby inna klasa (być możePersonRepository) który jest odpowiedzialny za wyprodukowanie Osoby z bazy danych lub zapisanie Osoby do bazy danych.

Aktualizacja 2

Odpowiadając na twoje komentarze:

Chociaż technicznie możesz odpowiedzieć na własne pytanie, StackOverflow polega głównie na uzyskiwaniu odpowiedzi od innych osób. Dlatego nie pozwoli ci zaakceptować własnej odpowiedzi, dopóki nie minie pewien okres karencji. Zachęcamy do dopracowania pytania i odpowiedzi na komentarze i odpowiedzi, dopóki ktoś nie znajdzie odpowiedniego rozwiązania początkowego pytania.ZrobiłemReadablePerson klasaabstract ponieważ wydawało się, że chcesz stworzyć osobę, która jest tylko do odczytu lub może być zapisywalna. Nawet jeśli obie klasy potomne mogłyby być uważane za osobę do odczytu, to jaki byłby sens utworzenianew ReadablePerson() kiedy równie łatwo możesz stworzyćnew ReadOnlyPerson()? Dokonanie abstrakcji klasy wymaga od użytkownika wybrania jednej z dwóch klas podrzędnych podczas tworzenia instancji.Składnik PersonRepository przypominałby fabrykę, ale słowo „repozytorium” wskazuje, że faktycznie wyciągasz informacje o osobie z jakiegoś źródła danych, zamiast tworzyć osobę z powietrza.

Moim zdaniem klasa Person byłaby po prostu POCO, bez logiki: tylko właściwości. Repozytorium będzie odpowiedzialne za budowanie obiektu Person. Zamiast mówić:

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

... i pozwalając obiektowi Person przejść do bazy danych, aby wypełnić jej różne właściwości, można by powiedzieć:

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

PersonRepository pobiera następnie odpowiednie informacje z bazy danych i konstruuje Person za pomocą tych danych.

Jeśli chcesz wywołać metodę, która nie ma powodu do zmiany osoby, możesz ochronić tę osobę przed zmianami, konwertując ją na opakowanie Readonly (zgodnie ze wzorem, który biblioteki .NET podążają zaReadonlyCollection<T> klasa). Z drugiej strony, metodom, które wymagają obiektu do zapisu, można podaćPerson bezpośrednio:

<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>

Zaletą korzystania z interfejsów jest to, że nie wymusza się określonej hierarchii klas. Zapewni to lepszą elastyczność w przyszłości. Na przykład powiedzmy, że na razie piszesz swoje metody w ten sposób:

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

... ale potem za dwa lata zdecydujesz się na implementację opakowania. NagleReadOnlyPerson nie jest jużReadablePerson, ponieważ jest to klasa opakowująca zamiast rozszerzenia klasy bazowej. Czy się zmieniasz?ReadablePerson doReadOnlyPerson we wszystkich podpisach metod?

Lub powiedz, że zdecydujesz się uprościć rzeczy i po prostu skonsolidować wszystkie swoje klasy w jedenPerson klasa: teraz musisz zmienić wszystkie metody, aby po prostu pobierać obiekty Person. Z drugiej strony, jeśli zaprogramowałeś interfejsy:

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

... wtedy te metody nie dbają o to, jak wygląda hierarchia obiektów, o ile obiekty, które im dajesz, implementują interfejsy, o które proszą. Możesz zmienić hierarchię implementacji w dowolnym momencie bez konieczności zmiany tych metod.

Jeśli przekazany obiekt zostanie zmodyfikowany, modyfikowany jest opakowanie (które w tym przypadku miało być tylko do odczytu). Dopóki nie stworzysz konstruktora, który przekaże wszystkie pola (w zasadzie głęboką kopię) lub nie masz swojego konstruktora, który zabiera obiekt do zawinięcia, wykonaj samą głęboką kopię. user420667
Myślę, że dyskusja stała się nieco nie na temat, chociaż podobało mi się i myślę, że dotyka ona niektórych wielkich tematów w projektowaniu. Jedyne co mam z pomysłem opakowania to to, że pozwala mi zmienić obiekt, który przekazałem, aby był zawinięty, co wymaga głębokiej kopii obiektu. Oznacza to, że wolę moje / nasze podejście do msdn :-P. Podoba mi się także pomysł na repozytorium, ponieważ zapewnia dobre warstwowanie i nie ma sensu dzwonić do Person.Save (). Być może można nawet mieć interfejs IObjectRepository. user420667
Ale nadal nie widzę interfejsów jakolepszy opcja, chociaż są opcją. 1) Nawet jeśli zmieniłbym ReadonlyPerson na wrapper, nie zmieniłbym jego pochodnej natury. 2) Czy argumenty nie obowiązywałyby tak łatwo, gdybym chciał skonsolidować / zmienić strukturę interfejsu? Czuję, że ta dyskusja może być gdzie indziej, być może czat / oddzielne pytanie / e-mail. Myślę, że to dobry pomysł. user420667
@ user420667: Myślę, że to zależy od tego, co próbujesz zapobiec. Jeśli chcesz, aby metoda mogła zakładać, że obiekt Person nie może zostać zmieniony w innym wątku podczas jego działania, masz rację. Jeśli chcesz, aby metoda wywołująca wiedziała, że ​​metoda nie będzie kolidować z obiektem Person, gdy jego umowa mówi, że planuje tylkoczytać Osoba, wtedy klasa opakowująca byłaby wystarczająca. StriplingWarrior
4

Pytanie brzmi: „jak chcesz zmienić klasę modyfikowalną w klasę tylko do odczytu, dziedzicząc z niej?” Dzięki dziedziczeniu możesz rozszerzyć klasę, ale jej nie ograniczać. Robiąc tak, rzucając wyjątki, naruszyłoby toZasada substytucji Liskowa (LSP).

Na odwrót, mianowicie uzyskanie klasy modyfikowalnej z klasy tylko do odczytu byłoby z tego punktu widzenia OK; jak jednak chcesz zmienić właściwość tylko do odczytu w właściwość do odczytu i zapisu? Co więcej, czy pożądane jest, aby można było zastąpić obiekt modyfikowalny, w którym oczekiwany jest obiekt tylko do odczytu?

Możesz to jednak zrobić za pomocą interfejsów

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

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

Ta klasa jest zgodna zIReadOnly interfejs. W kontekstach tylko do odczytu można uzyskać do niego dostęp za pośrednictwemIReadOnly berło.

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

AKTUALIZACJA

Przeprowadziłem dalsze badania na ten temat.

Istnieje jednak pewne zastrzeżenie, musiałem dodaćnew słowo kluczowe wIModifiable i możesz uzyskać dostęp do gettera tylko bezpośrednio przezModifiableClass lub przezIReadOnly interfejs, ale nie przezIModifiable berło.

Próbowałem też pracować z dwoma interfejsamiIReadOnly iIWriteOnly mając tylko gettera lub setera. Następnie możesz zadeklarować interfejs dziedziczący z obu i nienew słowo kluczowe jest wymagane przed nieruchomością (jak wIModifiable). Jednak podczas próby uzyskania dostępu do właściwości takiego obiektu pojawia się błąd kompilatoraAmbiguity between 'IReadOnly.MyProperty' and 'IWriteOnly.MyProperty'.

Oczywiście nie można zsyntetyzować właściwości z oddzielnych modułów pobierających i ustawiających, jak się spodziewałem.

Nie ma potrzeby implementowania dwóch oddzielnych klas, o ile używasz interfejsu IReadOnly, gdy chcesz użyć klasy w kontekście tylko do odczytu. Steve Czetty

Powiązane pytania