13

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

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

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

谢谢。

更新:

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

主要问题似乎是:

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

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 所建议的那样,向下转换可能会导致问题,但这通常是正确的,因为将猴子转换为狗,将动物向下转换为狗可能很糟糕。然而,似乎即使在编译时允许强制转换,但在运行时实际上是不允许的。

包装器类也可以解决问题,但我更喜欢这个,因为它避免了必须深度复制传入的对象/允许修改传入的对象从而修改包装器类的问题。

4

5 回答 5

13

Liskov 替换原则说你不应该让你的只读类继承你的读写类,因为消费类必须知道他们不能在没有异常的情况下调用它的 Save 方法。

让可写类扩展可读类对我来说更有意义,只要可读类上没有任何内容表明其对象永远不会被持久化。例如,我不会调用基类 a ReadOnly[Whatever],因为如果您有一个将 aReadOnlyPerson作为参数的方法,那么该方法将有理由假设他们对该对象所做的任何事情都不可能对数据库,如果实际实例是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; }
    }
}

这确保了真正ReadOnlyPerson不能简单地转换为 ModifiablePerson 并进行修改。但是,如果您愿意相信开发人员不会尝试以这种方式降低参数,那么我更喜欢 Steve 和 Olivier 的答案中基于接口的方法。

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

最后一点,既然您喜欢学习 Liskov 替换原则:通过让 Person 类负责将自己从数据库中加载出来,您就违反了单一职责原则。理想情况下,您的 Person 类将具有表示包含“Person”的数据的属性,并且将有一个不同的类(可能是一个PersonRepository)负责从数据库中生成 Person 或将 Person 保存到数据库中。

更新 2

回复您的评论:

  • 虽然您可以在技术上回答自己的问题,但 StackOverflow 主要是关于从其他人那里获得答案。这就是为什么在某个宽限期过去之前,它不会让您接受自己的答案。我们鼓励您完善您的问题并回复评论和答案,直到有人对您的初始问题提出适当的解决方案。
  • 我开设这ReadablePerson门课abstract是因为您似乎只想创建一个只读或可写的人。即使两个子类都可以被认为是 ReadablePerson,但是new ReadablePerson()当您可以轻松地创建a 时,创建 a 的意义new ReadOnlyPerson()何在?使类抽象化要求用户在实例化它们时选择两个子类之一。
  • PersonRepository 有点像工厂,但“存储库”一词表示您实际上是从某个数据源中提取人员的信息,而不是凭空创建人员。
  • 在我看来,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'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);
    }
    
  • 使用接口的好处是您不会强制使用特定的类层次结构。这将在未来为您提供更好的灵活性。例如,假设您现在这样编写方法:

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

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

    或者说您决定简化事情并将所有类合并到一个Person类中:现在您必须更改所有方法以仅获取 Person 对象。另一方面,如果您已经对接口进行了编程:

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

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

于 2012-04-04T16:08:32.407 回答
6

绝对不要让只读类继承自可写类。派生类应该扩展和修改基类的能力;他们永远不应该剥夺能力。

您也许可以使可写类继承自只读类,但您需要谨慎行事。要问的关键问题是,只读类的任何消费者是否会依赖它是只读的这一事实?如果消费者指望值永远不会改变,但可写派生类型被传入,然后值被改变,那么消费者可能会被破坏。

我知道,由于这两种类型的结构(即它们包含的数据)相似或相同,因此很容易认为一种应该从另一种继承。但情况往往并非如此。如果它们是为明显不同的用例而设计的,它们可能需要是单独的类。

于 2012-04-04T16:15:53.463 回答
5

一个快速的选择可能是创建一个IReadablePerson(等)接口,它只包含 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() {}
}
于 2012-04-04T16:17:34.040 回答
4

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

反过来,从只读类派生一个可修改类从这个角度来看是可以的;但是,您想如何将只读属性转换为读写属性?而且,是否希望能够在需要只读对象的地方替换可修改对象?

但是,您可以使用接口执行此操作

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接口访问 getter,而不能通过IModifiable接口访问。

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

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

于 2012-04-04T16:38:22.033 回答
0

在为用户安全权限创建对象时我有同样的问题要解决,在某些情况下必须是可变的以允许高级用户修改安全设置,但通常是只读的以存储当前登录用户的权限信息不允许代码即时修改这些权限。

我想出的模式是定义一个可变对象实现的接口,它具有只读属性获取器。然后,该接口的可变实现可以是私有的,允许直接处理实例化和水合对象的代码这样做,但是一旦从该代码中返回对象(作为接口的实例),setter 将不再可访问.

例子:

//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 将只读实例转换为可变实例。在我的实际实现中还有一些技巧,主要是处理可编辑对象的持久性(由于编辑用户记录是一项安全操作,无法使用 Repository 的“正常”持久性方法保存 EditableUser;它需要调用一个重载还需要一个必须具有足够权限的 IUser)。

您必须了解的一件事;如果您的程序可以在任何范围内编辑记录,那么无论是有意还是无意,这种能力都可能被滥用。有必要对对象的可变或不可变形式的任何用法进行定期代码审查,以确保其他编码人员没有做任何“聪明”的事情。这种模式也不足以确保公众使用的应用程序是安全的;如果您可以编写 IUser 实现,那么攻击者也可以,因此您需要一些额外的方法来验证您的代码而不是攻击者生成了特定的 IUser 实例,并且该实例在此期间没有被篡改。

于 2012-04-04T17:23:30.373 回答