readonly class design when a non-readonly class is already in place

前端 未结 5 2411
借酒劲吻你
借酒劲吻你 2021-02-19 16:55

I have a class that upon construction, loads it\'s info from a database. The info is all modifiable, and then the developer can call Save() on it to make it Save that informati

相关标签:
5条回答
  • 2021-02-19 17:33

    A quick option might be to create an IReadablePerson (etc) interface, which contains only get properties, and does not include Save(). Then you can have your existing class implement that interface, and where you need Read-only access, have the consuming code reference the class through that interface.

    In keeping with the pattern, you probably want to have a IReadWritePerson interface, as well, which would contain the setters and Save().

    Edit On further thought, IWriteablePerson should probably be IReadWritePerson, since it wouldn't make much sense to have a write-only class.

    Example:

    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() {}
    }
    
    0 讨论(0)
  • 2021-02-19 17:34

    Definitely do not make the read-only class inherit from the writable class. Derived classes should extend and modify the capabilities of the base class; they should never take capabilities away.

    You may be able to make the writable class inherit from the read-only class, but you need to do it carefully. The key question to ask is, would any consumers of the read-only class rely on the fact that it is read-only? If a consumer is counting on the values never changing, but the writable derived type is passed in and then the values are changed, that consumer could be broken.

    I know it is tempting to think that because the structure of the two types (i.e. the data that they contain) is similar or identical, that one should inherit from the other. But that is often not the case. If they are being designed for significantly different use cases, they probably need to be separate classes.

    0 讨论(0)
  • 2021-02-19 17:42

    The Liskov Substitution Principle says that you shouldn't make your read-only class inherit from your read-write class, because consuming classes would have to be aware that they can't call the Save method on it without getting an exception.

    Making the writable class extend the readable class would make more sense to me, as long as there is nothing on the readable class that indicates its object can never be persisted. For example, I wouldn't call the base class a ReadOnly[Whatever], because if you have a method that takes a ReadOnlyPerson as an argument, that method would be justified in assuming that it would be impossible for anything they do to that object to have any impact on the database, which is not necessarily true if the actual instance is a WriteablePerson.

    Update

    I was originally assuming that in your read-only class you only wanted to prevent people calling the Save method. Based on what I'm seeing in your answer-response to your question (which should actually be an update on your question, by the way), here's a pattern you might want to follow:

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

    This ensures that a truly ReadOnlyPerson cannot simply be cast as a ModifiablePerson and modified. If you're willing to trust that developers won't try to down-cast arguments in this way, though, I prefer the interface-based approach in Steve and Olivier's answers.

    Another option would be to make your ReadOnlyPerson just be a wrapper class for a Person object. This would necessitate more boilerplate code, but it comes in handy when you can't change the base class.

    One last point, since you enjoyed learning about the Liskov Substitution Principle: By having the Person class be responsible for loading itself out of the database, you are breaking the Single-Responsibility Principle. Ideally, your Person class would have properties to represent the data that comprises a "Person," and there would be a different class (maybe a PersonRepository) that's responsible for producing a Person from the database or saving a Person to the database.

    Update 2

    Responding to your comments:

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

    0 讨论(0)
  • 2021-02-19 17:42

    I had the same problem to solve when creating an object for user security permissions, that in certain cases must be mutable to allow high-level users to modify security settings, but normally is read-only to store the currently logged-in user's permissions information without allowing code to modify those permissions on the fly.

    The pattern I came up with was to define an interface which the mutable object implements, that has read-only property getters. The mutable implementation of that interface can then be private, allowing code that directly deals with instantiating and hydrating the object to do so, but once the object is returned out of that code (as an instance of the interface) the setters are no longer accessible.

    Example:

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

    A pattern like this allows you to use code you've already created in a read-write fashion, while not allowing Joe Programmer to turn a read-only instance into a mutable one. There are a few more tricks in my actual implementation, mainly dealing with persistence of the editable object (since editing user records is a secured action, an EditableUser cannot be saved with the Repository's "normal" persistence method; it instead requires calling an overload that also takes an IUser which must have sufficient permissions).

    One thing you simply must understand; if it is possible for your program to edit the records in any scope, it is possible for that ability to be abused, whether intentionally or otherwise. Regular code reviews of any usage of the mutable or immutable forms of your object will be necessary to make sure other coders aren't doing anything "clever". This pattern also isn't enough to ensure that an application used by the general public is secure; if you can write an IUser implementation, so can an attacker, so you'll need some additional way to verify that your code and not an attacker's produced a particular IUser instance, and that the instance hasn't been tampered with in the interim.

    0 讨论(0)
  • 2021-02-19 17:47

    The question is, "how do you want to turn a modifiable class into a read-only class by inheriting from it?" With inheritance you can extend a class but not restrict it. Doing so by throwing exceptions would violate the Liskov Substitution Principle (LSP).

    The other way round, namely deriving a modifiable class from a read-only class would be OK from this point of view; however, how do you want to turn a read-only property into a read-write property? And, moreover, is it desirable to be able to substitute a modifiable object where a read-only object is expected?

    However, you can do this with interfaces

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

    This class is assignment compatible to the IReadOnly interface as well. In read-only contexts you can access it through the IReadOnly interface.

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

    UPDATE

    I did some further investigations on the subject.

    However, there is a caveat to this, I had to add a new keyword in IModifiable and you can only access the getter either directly through the ModifiableClass or through the IReadOnly interface, but not through the IModifiable interface.

    I also tried to work with two interfaces IReadOnly and IWriteOnly having only a getter or a setter respectively. You can then declare an interface inheriting from both of them and no new keyword is required in front of the property (as in IModifiable). However when you try to access the property of such an object you get the compiler error Ambiguity between 'IReadOnly.MyProperty' and 'IWriteOnly.MyProperty'.

    Obviously, it is not possible to synthesize a property from separate getters and setters, as I expected.

    0 讨论(0)
提交回复
热议问题