Подтвердить что ты не робот

Readonly class design, когда класс, не являющийся readonly, уже установлен

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

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

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

Спасибо.

Update:

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

Основные проблемы:

  • Соответствие ожиданиям, основанным на именах классов и структурах наследования.
  • Предотвращение ненужного дублирующего кода

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

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

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

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

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

Обновление 2:

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

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

4b9b3361

Ответ 1

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

Создание класса, доступного для записи, для читаемого класса будет иметь для меня больше смысла, если в читаемом классе нет ничего, что указывает на то, что его объект никогда не будет сохранен. Например, я бы не назвал базовый класс a 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 не может просто быть изменен как модифицируемый и изменен. Если вы согласны с тем, что разработчики не будут пытаться сбрасывать аргументы таким образом, я предпочитаю интерфейсный подход в ответах Стива и Оливье.

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

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

Обновление 2

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

  • Пока вы можете технически ответить на свой собственный вопрос, StackOverflow в основном связан с получением ответов от других людей. Поэтому он не позволит вам принять ваш собственный ответ до тех пор, пока не пройдет определенный льготный период. Вам рекомендуется уточнить свой вопрос и ответить на комментарии и ответы, пока кто-то не придумает адекватное решение вашего начального вопроса.
  • Я сделал класс ReadablePerson abstract, потому что казалось, что вы только хотите создать человека, который доступен только для чтения, или тот, который можно записать. Несмотря на то, что оба дочерних класса можно считать ReadablePerson, какой смысл создавать new ReadablePerson(), когда вы могли бы так же легко создать new ReadOnlyPerson()? Создание абстрактного класса требует от пользователя выбора одного из двух дочерних классов при их создании.
  • PersonRepository будет выглядеть как factory, но слово "репозиторий" означает, что вы на самом деле вытаскиваете информацию человека из какого-то источника данных, а не создаете человека из воздуха.
  • На мой взгляд, класс Person будет просто POCO, без логики в нем: просто свойства. Репозиторий будет отвечать за создание объекта Person. Вместо того, чтобы говорить:

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

    ... и позволяя объекту Person перейти в базу данных, чтобы заполнить его различные свойства, вы бы сказали:

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

    PersonRepository затем получит соответствующую информацию из базы данных и построит Person с этими данными.

    Если вы хотите вызвать метод, у которого нет причин изменять человека, вы можете защитить этого человека от изменений, преобразовывая его в оболочку Readonly (следуя шаблону, который библиотеки .NET следуют с классом ReadonlyCollection<T>), С другой стороны, методы, которые требуют записываемого объекта, могут быть напрямую заданы Person:

    var person = _personRepository.GetById(personId);
    // Prevent GetVoteCount from changing any of the person 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);
    }
    
  • Преимущество использования интерфейсов заключается в том, что вы не заставляете определенную иерархию классов. Это даст вам лучшую гибкость в будущем. Например, скажем, что на данный момент вы пишете свои методы следующим образом:

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

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

    Или скажите, что вы решили упростить вещи и просто объединить все свои классы в один класс Person: теперь вам нужно изменить все свои методы, чтобы просто взять объекты Person. С другой стороны, если вы запрограммировали на интерфейсы:

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

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

Ответ 2

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

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

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

Ответ 3

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

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

Изменить. При дальнейшей мысли 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() {}
}

Ответ 4

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

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

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

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

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

Ответ 5

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

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

Пример:

//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 не может быть сохранен с помощью метода "нормального" сохранения в репозитории, вместо этого требуется вызвать перегрузку, которая также принимает IUser, который должен иметь достаточные разрешения).

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