Вопрос по c# – дизайн класса только для чтения, когда класс не для чтения уже существует

13

У меня есть класс, который при построении загружает информацию из базы данных. Вся информация может быть изменена, и тогда разработчик может вызвать функцию Save (), чтобы сохранить эту информацию обратно в базу данных.

Я также создаю класс, который будет загружаться из базы данных, но не разрешать какие-либо обновления для него. (версия только для чтения.) Мой вопрос заключается в том, должен ли я сделать отдельный класс и наследовать, или я должен просто обновить существующий объект, чтобы получить параметр только для чтения в конструкторе, или я должен полностью создать отдельный класс?

Существующий класс уже используется во многих местах кода.

Благодарю.

Update:

Во-первых, здесь много хороших ответов. Было бы трудно принять только один. Спасибо всем.

The main problems it seems are:

Meeting expectations based on class names and inheritance structures. Preventing unnecessary duplicate code

Кажется, есть большая разница между Readable и ReadOnly. Класс Readonly, вероятно, не должен наследоваться. Но класс Readable предполагает, что он может также получить возможность записи в некоторый момент.

Итак, после долгих раздумий вот что я думаю:

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

К сожалению, я пока не знаю, как это сделать со свойствами (см. Ответ StriplingWarrior об этом со свойствами), но у меня есть ощущение, что это будет включать защищенное ключевое слово иасимметричные модификаторы доступа к свойству.

Кроме того, к счастью для меня, данные, которые загружаются из базы данных, не нужно превращать в ссылочные объекты, они представляют собой простые типы. Это означает, что мне действительно не нужно беспокоиться о людях, изменяющих членовReadOnlyPerson объект.

Update 2:

Заметьте, что, как предположил StriplingWarrior, даункинг может привести к проблемам, но это, как правило, верно, поскольку приведение обезьяны к животному и обратно к собаке может быть плохим. Однако, похоже, что хотя приведение разрешено во время компиляции, оно фактически не разрешено во время выполнения.

Класс-обертка также может помочь, но мне это нравится больше, потому что он устраняет проблему необходимости глубокого копирования переданного объекта / позволяет изменять переданный объект, тем самым изменяя класс-обертку.

Ваш Ответ

5   ответов
0

лось решить ту же проблему, которая в некоторых случаях должна быть изменяемой, чтобы позволить пользователям высокого уровня изменять настройки безопасности, но обычно доступна только для чтения, чтобы сохранить вошедших в систему пользователей. информация о разрешениях, не позволяя коду изменять эти разрешения на лету.

Шаблон, который я придумал, должен был определить интерфейс, который реализует изменяемый объект, который имеет методы получения только для чтения. Изменяемая реализация этого интерфейса может тогда быть закрытой, что позволяет коду, который непосредственно связан с созданием экземпляра и гидратации объекта, делать это, но как только объект возвращается из этого кода (как экземпляр интерфейса), сеттеры больше не доступны ,

Пример:

//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);
   }

   ...
}

Подобный шаблон позволяет вам использовать код, который вы уже создали, в режиме чтения-записи, в то же время не позволяя Joe Programmer превращать экземпляр только для чтения в изменяемый. В моей фактической реализации есть еще несколько хитростей, в основном связанных с сохранением редактируемого объекта (поскольку редактирование записей пользователя является защищенным действием, EditableUser не может быть сохранен с помощью метода постоянства Repository 's & quot; normal & quot; он вместо этого требует вызова перегрузка, которая также принимает IUser, который должен иметь достаточные разрешения).

Одна вещь, которую вы просто должны понять; если ваша программа может редактировать записи в любой области, то возможно злоупотребление этой возможностью, преднамеренно или иным образом. Регулярные проверки кода любого использования изменяемых или неизменяемых форм вашего объекта будут необходимы, чтобы удостовериться, что другие кодировщики не делают ничего «умного». Этого шаблона также недостаточно для обеспечения безопасности приложения, используемого широкой публикой; если вы можете написать реализацию IUser, то же самое может сделать и злоумышленник, так что вам понадобится какой-то дополнительный способ проверить, что ваш код, а не злоумышленник, создал конкретный экземпляр IUser и что этот экземпляр не был подделан в промежуточный.

Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded user420667
13

Принцип замещения Лискова говорит, что вы не должны делать свой класс только для чтения наследуемым от вашего класса чтения-записи, потому что потребляющие классы должны знать, что они не могут вызывать метод Save для него без получения исключения.

Расширение доступного для записи класса для читаемого класса имело бы для меня больше смысла, если в читаемом классе нет ничего, что указывало бы на то, что его объект никогда не будет сохранен. Например, я бы не назвал базовый классReadOnly[Whatever]потому что если у вас есть метод, который принимаетReadOnlyPerson в качестве аргумента, этот метод будет оправдан, если предположить, что что-либо, что они делают с этим объектом, не окажет никакого влияния на базу данных, что не всегда верно, если фактический экземпляр являетсяWriteablePerson.

Update

Первоначально я предполагал, что в вашем классе, предназначенном только для чтения, вы хотели только запретить людям звонитьSave метод. Исходя из того, что я вижу в вашем ответе-ответе на ваш вопрос (кстати, это должно быть обновление вашего вопроса), вот схема, которой вы, возможно, захотите следовать:

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

Это гарантирует, что действительноReadOnlyPerson не может быть просто приведен как ModifiablePerson и изменен. Если вы хотите верить, что разработчики не будут пытаться опровергнуть аргументы таким образом, я предпочитаю основанный на интерфейсе подход в ответах Стива и Оливье.

Другим вариантом будет сделать вашReadOnlyPerson просто быть классом-оберткой дляPerson объект. Это потребовало бы большего количества стандартного кода, но это пригодится, когда вы не можете изменить базовый класс.

И последнее замечание: вам понравилось изучать принцип подстановки Лискова: если ответственность за загрузку себя из базы данных возлагается на класс Person, вы нарушаете принцип единой ответственности. В идеале, ваш класс Person должен иметь свойства для представления данных, которые содержат "Person", quot; и там будет другой класс (может быть,PersonRepository) который отвечает за создание Лица из базы данных или сохранение Лица в базе данных.

Update 2

Отвечая на ваши комментарии:

While you can technically answer your own question, StackOverflow is largely about getting answers from other people. That's why it won't let you accept your own answer until a certain grace period has passed. You are encouraged to refine your question and respond to comments and answers until someone has come up with an adequate solution to your initial question. I made the ReadablePerson class abstract because it seemed like you'd only ever want to create a person that is read-only or one that is writeable. Even though both of the child classes could be considered to be a ReadablePerson, what would be the point of creating a new ReadablePerson() when you could just as easily create a new ReadOnlyPerson()? Making the class abstract requires the user to choose one of the two child classes when instantiating them. A PersonRepository would sort of be like a factory, but the word "repository" indicates that you're actually pulling the person's information from some data source, rather than creating the person out of thin air.

In my mind, the Person class would just be a POCO, with no logic in it: just properties. The repository would be responsible for building the Person object. Rather than saying:

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

... and allowing the Person object to go to the database to populate its various properties, you would say:

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

The PersonRepository would then get the appropriate information out of the database and construct the Person with that data.

If you wanted to call a method that has no reason to change the person, you could protect that person from changes by converting it to a Readonly wrapper (following the pattern that the .NET libraries follow with the ReadonlyCollection<T> class). On the other hand, methods that require a writeable object could be given the Person directly:

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);
}

The benefit of using interfaces is that you're not forcing a specific class hierarchy. This will give you better flexibility in the future. For example, let's say that for the moment you write your methods like this:

GetVoteCount(ReadablePerson p);
UpdatePersonDataFromLdap(ReadWritePerson p);

... but then in two years you decide to change to the wrapper implementation. Suddenly ReadOnlyPerson is no longer a ReadablePerson, because it's a wrapper class instead of an extension of a base class. Do you change ReadablePerson to ReadOnlyPerson in all your method signatures?

Or say you decide to simplify things and just consolidate all your classes into a single Person class: now you have to change all your methods to just take Person objects. On the other hand, if you had programmed to interfaces:

GetVoteCount(IReadablePerson p);
UpdatePersonDataFromLdap(IReadWritePerson p);

... then these methods don't care what your object hierarchy looks like, as long as the objects you give them implement the interfaces they ask for. You can change your implementation hierarchy at any time without having to change these methods at all.

Error: User Rate Limit Exceeded user420667
Error: User Rate Limit Exceeded user420667
Error: User Rate Limit ExceededReadWritePerson, ModifiablePersonError: User Rate Limit ExceededPersonError: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded user420667
5

IReadablePerson (и т. д.), который содержит только свойства get и не включает Save (). Затем вы можете сделать так, чтобы ваш существующий класс реализовал этот интерфейс, и там, где вам нужен доступ только для чтения, чтобы код потребления использовал ссылку на класс через этот интерфейс.

В соответствии с шаблоном, вы, вероятно, хотите иметьIReadWritePerson интерфейс, который будет содержать сеттеры иSave().

Edit На дальнейшие мысли,IWriteablePerson должно бытьIReadWritePerson, поскольку было бы бессмысленно иметь класс только для записи.

Пример:

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() {}
}
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded user420667
4

«Как вы хотите превратить модифицируемый класс в класс только для чтения, наследуя его?» С наследованием вы можете расширять класс, но не ограничивать его. Это может привести к нарушениюПринцип замещения Лискова (ЛСП).

Обратный путь, а именно получение модифицируемого класса из класса только для чтения, будет в порядке с этой точки зрения; однако, как вы хотите превратить свойство только для чтения в свойство чтения-записи? И, кроме того, желательно ли иметь возможность заменить модифицируемый объект там, где ожидается объект только для чтения?

Тем не менее, вы можете сделать это с помощью интерфейсов

interface IReadOnly
{
    int MyProperty { get; }
}

interface IModifiable : IReadOnly
{
    new int MyProperty { set; }
    void Save();
}

Этот класс соответствует назначениюIReadOnly интерфейс также. В контексте только для чтения вы можете получить к нему доступ черезIReadOnly интерфейс.

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

ОБНОВИТЬ

Я провел дальнейшие исследования по этому вопросу.

Тем не менее, есть предостережение, я должен был добавитьnew ключевое слово вIModifiable и вы можете получить доступ только к получателю непосредственно черезModifiableClass или черезIReadOnly интерфейс, но не черезIModifiable интерфейс.

Я также пытался работать с двумя интерфейсамиIReadOnly а такжеIWriteOnly имея только геттер или сеттер соответственно. Затем вы можете объявить интерфейс, наследующий от них обоих, и нетnew Ключевое слово требуется перед собственностью (как вIModifiable). Однако, когда вы пытаетесь получить доступ к свойству такого объекта, вы получаете ошибку компилятора.Ambiguity between 'IReadOnly.MyProperty' and 'IWriteOnly.MyProperty'.

Очевидно, что, как я и ожидал, невозможно синтезировать свойство из отдельных методов получения и установки.

Error: User Rate Limit Exceeded
6

не делайте класс, доступный только для чтения, от класса, доступного для записи. Производные классы должны расширять и модифицировать возможности базового класса; они никогда не должны отнимать способности.

Возможно, вы сможете сделать доступный для записи класс наследуемым от класса только для чтения, но вам нужно сделать это осторожно. Ключевой вопрос, который нужно задать, - будут ли потребители класса «только для чтения» полагаться на тот факт, что он предназначен только для чтения? Если потребитель рассчитывает на значения, которые никогда не меняются, но передается доступный для записи производный тип, а затем значения меняются, этот потребитель может быть поврежден.

Я знаю, что соблазнительно думать, что поскольку структура двух типов (то есть данных, которые они содержат) схожи или идентичны, то один должен наследоваться от другого. Но это часто не так. Если они разрабатываются для существенно разных вариантов использования, они, вероятно, должны быть отдельными классами.

Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded user420667
Error: User Rate Limit Exceeded user420667

Похожие вопросы