当一个非只读类已经到位时,只读类设计

use*_*667 13 c# readonly

我有一个类,在构建时,从数据库加载它的信息.信息都是可修改的,然后开发人员可以调用它上面的Save()使其将该信息保存回数据库.

我也在创建一个将从数据库加载的类,但不允许对它进行任何更新.(一个只读版本.)我的问题是,我应该创建一个单独的类并继承,还是应该只更新现有对象以在构造函数中获取readonly参数,还是应该完全创建一个单独的类?

现有的类已经在代码中的许多地方使用.

谢谢.

更新:

首先,这里有很多很棒的答案.很难接受只有一个.感谢大家.

看来主要的问题是:

  • 满足基于类名和继承结构的期望.
  • 防止不必要的重复代码

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,我还不知道如何使用属性执行此操作(请参阅StriplingWarrior对此属性的回答),但我感觉它将涉及受保护的关键字和非对称属性访问修饰符.

另外,幸运的是,从数据库加载的数据不必转换为引用对象,而是简单类型.这意味着我不必担心人们修改ReadOnlyPerson对象的成员.

更新2:

请注意,正如StriplingWarrior所暗示的那样,向下转换可能会导致问题,但这通常是正确的,因为将一只猴子施放到动物身上并将动物归还给狗可能会很糟糕.但是,似乎即使在编译时允许转换,实际上也不允许在运行时.

包装类也可以做到这一点,但我更喜欢这个,因为它避免了必须深层复制传入的对象/允许传入的对象被修改从而修改包装类的问题.

Str*_*ior 13

里氏替换原则说,你不应该让你的只读类从您的读写类继承,因为消费类必须知道,他们不能没有得到一个异常调用Save方法就可以了.

使可写类扩展可读类对我来说更有意义,只要可读类上没有任何内容表明它的对象永远不会被持久化.例如,我不会调用基类a ReadOnly[Whatever],因为如果你有一个方法以一个ReadOnlyPerson参数作为参数,那么该方法可以证明对它们对该对象所做的任何事情都不可能产生任何影响.数据库,如果实际实例是a,则不一定是真的WriteablePerson.

更新

我原本假设在你的只读课程中你只想阻止人们调用该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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

这确保了ReadOnlyPerson不能简单地将其转换为ModifiablePerson并进行修改.如果你愿意相信开发人员不会试图以这种方式下载论据,我更喜欢Steve和Olivier的答案中基于接口的方法.

另一个选择是让你ReadOnlyPerson只是一个Person对象的包装类.这将需要更多样板代码,但是当您无法更改基类时它会派上用场.

最后一点,因为您喜欢了解Liskov替换原则:通过让Person类负责将自己加载到数据库之外,您违反了单一责任原则.理想情况下,您的Person类将具有表示包含"Person"的数据的属性,并且将有一个不同的类(可能是a PersonRepository)负责从数据库生成Person或将Person保存到数据库.

更新2

回应你的意见:

  • 虽然您可以在技术上回答您自己的问题,但StackOverflow主要是从其他人那里获得答案.这就是为什么在某个宽限期过去之前它不会让你接受自己的答案.我们鼓励您优化您的问题并回答评论和答案,直到有人为您的初始问题提出了充分的解决方案.
  • ReadablePerson上课abstract是因为你似乎只想创造一个只读或可写的人.尽管两个子类都可以被认为是一个ReadablePerson,但是new ReadablePerson()当你可以轻松地创建一个子类时,有什么意义new ReadOnlyPerson()呢?使类抽象化需要用户在实例化时选择两个子类中的一个.
  • PersonRepository有点像工厂,但"存储库"这个词表示你实际上是从一些数据源中提取人的信息,而不是凭空创造这个人.
  • 在我看来,Person类只是一个POCO,没有逻辑:只是属性.存储库将负责构建Person对象.而不是说:

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

    ...并允许Person对象转到数据库以填充其各种属性,您会说:

    // This is a better separation of concerns
    var p = _personRepository.GetById(personId);
    
    Run Code Online (Sandbox Code Playgroud)

    然后,PersonRepository将从数据库中获取适当的信息,并使用该数据构造Person.

    如果您想调用一个没有理由更改此人的方法,您可以通过将其转换为Readonly包装器来保护该人员免受更改(遵循.NET库跟随ReadonlyCollection<T>该类的模式).另一方面,需要可写对象的方法可以直接给出Person:

    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);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 使用接口的好处是您不会强制使用特定的类层次结构.这将为您提供更好的灵活性.例如,让我们说你现在写这样的方法:

    GetVoteCount(ReadablePerson p);
    UpdatePersonDataFromLdap(ReadWritePerson p);
    
    Run Code Online (Sandbox Code Playgroud)

    ...但是在两年后你决定改用包装器实现.突然ReadOnlyPerson不再是一个ReadablePerson,因为它是一个包装类而不是基类的扩展.是否要改变ReadablePersonReadOnlyPerson你的所有方法签名?

    或者说你决定简化一些事情并将所有类合并到一个Person类中:现在你必须改变你所有的方法来获取Person对象.另一方面,如果您已编程到接口:

    GetVoteCount(IReadablePerson p);
    UpdatePersonDataFromLdap(IReadWritePerson p);
    
    Run Code Online (Sandbox Code Playgroud)

    ...然后这些方法不关心你的对象层次结构是什么样的,只要你给它们的对象实现它们要求的接口.您可以随时更改实施层次结构,而无需更改这些方法.


Dav*_*son 6

绝对不要让只读类继承可写类.派生类应该扩展和修改基类的功能; 他们永远不应该把能力带走.

您可以使可写类继承自只读类,但您需要仔细执行.要问的关键问题是,只读类的任何消费者是否都依赖于它是只读的这一事实?如果消费者指望永远不会改变的值,但是传递了可写派生类型,然后更改了值,则该消费者可能会被破坏.

我知道很有可能认为因为两种类型的结构(即它们包含的数据)相似或相同,所以应该从另一种继承.但情况往往并非如此.如果它们是针对显着不同的用例而设计的,则它们可能需要是单独的类.


Ste*_*tty 5

一个快速选项可能是创建一个IReadablePerson(etc)接口,它只包含get属性,并且不包含Save().然后,您可以让现有的类实现该接口,并且您需要只读访问的位置,让消费代码通过该接口引用该类.

为了与模式保持一致,您可能希望拥有一个IReadWritePerson包含setter和的接口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() {}
}
Run Code Online (Sandbox Code Playgroud)


Oli*_*bes 5

问题是,“如何通过继承将可修改的类变成只读类?” 通过继承,您可以扩展类,但不能限制它。通过抛出异常来这样做会违反里氏替换原则(LSP)。

从这个角度来看,反过来,即从只读类派生可修改的类也是可以的;但是,如何将只读属性变成读写属性呢?此外,是否希望能够在需要只读对象的地方替换可修改的对象?

但是,您可以使用接口来做到这一点

interface IReadOnly
{
    int MyProperty { get; }
}

interface IModifiable : IReadOnly
{
    new int MyProperty { set; }
    void Save();
}
Run Code Online (Sandbox Code Playgroud)

该类的赋值IReadOnly也与接口兼容。在只读上下文中,您可以通过IReadOnly界面访问它。

class ModifiableClass : IModifiable
{
    public int MyProperty { get; set; }
    public void Save()
    {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

更新

我对这个问题做了一些进一步的调查。

但是,有一个警告,我必须添加一个new关键字IModifiable,并且您只能直接通过ModifiableClass或通过IReadOnly接口访问 getter,但不能通过IModifiable接口访问。

我还尝试使用两个接口IReadOnly,并且IWriteOnly分别只有一个 getter 或一个 setter。然后,您可以声明一个从这两个接口继承的接口,并且new属性前面不需要任何关键字(如IModifiable)。但是,当您尝试访问此类对象的属性时,您会收到编译器错误Ambiguity between 'IReadOnly.MyProperty' and 'IWriteOnly.MyProperty'

显然,正如我所预期的那样,不可能从单独的 getter 和 setter 合成一个属性。