Pergunta sobre c#, readonly – design de classe readonly quando uma classe não-readonly já está em vigor

13

Eu tenho uma classe que, em construção, carrega informações de um banco de dados. As informações são todas modificáveis ​​e, em seguida, o desenvolvedor pode chamar Save () para salvar as informações no banco de dados.

Também estou criando uma classe que será carregada do banco de dados, mas não permitirá atualizações. (uma versão somente para leitura). Minha pergunta é, devo fazer uma classe separada e herdar, ou devo apenas atualizar o objeto existente para obter um parâmetro readonly no construtor, ou devo criar uma classe separada inteiramente?

A classe existente já é usada em muitos lugares no código.

Obrigado.

Atualizar:

Em primeiro lugar, há muitas ótimas respostas aqui. Seria difícil aceitar apenas um. Obrigado a todos.

Os principais problemas que parece são:

Atender às expectativas com base em nomes de classes e estruturas de herança.Evitando código duplicado desnecessário

Parece haver uma grande diferença entre Readable e ReadOnly. Uma classe Readonly provavelmente não deve ser herdada. Mas uma classe Readable sugere que também pode ganhar capacidade de escrita em algum momento.

Então, depois de pensar muito, aqui está o que estou pensando:

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

Infelizmente, eu ainda não sei como fazer isso com propriedades (veja a resposta da StriplingWarrior para isso feito com propriedades), mas eu tenho a sensação de que isso envolverá a palavra-chave protegida emodificadores de acesso a propriedades assimétricas.

Além disso, felizmente para mim, os dados que são carregados do banco de dados não precisam ser transformados em objetos de referência, ao contrário, eles são tipos simples. Isso significa que eu realmente não tenho que me preocupar com pessoas modificando os membros doReadOnlyPerson objeto.

Atualização 2:

Note, como StriplingWarrior sugeriu, downcasting pode levar a problemas, mas isso geralmente é verdade, já que mandar um Monkey para um Animal de volta para um Dog pode ser ruim. No entanto, parece que, embora a conversão seja permitida em tempo de compilação, ela não é realmente permitida em tempo de execução.

Uma classe wrapper também pode fazer o truque, mas eu gosto disso melhor porque evita o problema de ter que copiar profundamente o objeto passado / permitir que o objeto passado seja modificado, modificando assim a classe wrapper.

Sua resposta

5   a resposta
4

A pergunta é: "como você deseja transformar uma classe modificável em uma classe somente leitura herdando dela?" Com herança, você pode estender uma classe, mas não restringi-la. Fazer isso lançando exceções violaria oPrincípio da Substituição de Liskov (LSP)

O inverso, ou seja, derivar uma classe modificável de uma classe read-only seria OK deste ponto de vista; no entanto, como você deseja transformar uma propriedade somente leitura em uma propriedade de leitura / gravação? E, além disso, é desejável poder substituir um objeto modificável em que um objeto somente leitura é esperado?

No entanto, você pode fazer isso com interfaces

<code>interface IReadOnly
{
    int MyProperty { get; }
}

interface IModifiable : IReadOnly
{
    new int MyProperty { set; }
    void Save();
}
</code>

Esta classe é uma tarefa compatível com oIReadOnly interface também. Em contextos somente leitura, você pode acessá-lo através doIReadOnly interface.

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

ATUALIZAR

Fiz algumas investigações adicionais sobre o assunto.

No entanto, há uma ressalva para isso, eu tive que adicionar umnew palavra-chave emIModifiable e você só pode acessar o getter diretamente através doModifiableClass ou através doIReadOnly interface, mas não através doIModifiable interface.

Eu também tentei trabalhar com duas interfacesIReadOnly eIWriteOnly tendo apenas um getter ou um setter respectivamente. Você pode então declarar uma interface herdando de ambos e nãonew palavra-chave é necessária na frente da propriedade (como emIModifiable). No entanto, quando você tenta acessar a propriedade desse objeto, você obtém o erro do compiladorAmbiguity between 'IReadOnly.MyProperty' and 'IWriteOnly.MyProperty'.

Obviamente, não é possível sintetizar uma propriedade de getters e setters separados, como eu esperava.

Não há necessidade de implementar duas classes separadas, contanto que você use a interface IReadOnly quando quiser usar a classe em um contexto somente leitura. Steve Czetty
13

oPrincípio da Substituição de Liskov diz que você não deve tornar sua classe somente leitura herdada de sua classe de leitura / gravação, pois as classes consumidoras precisam estar cientes de que não podem chamar o método Save sem obter uma exceção.

Fazer a classe gravável estender a classe legível faria mais sentido para mim, desde que não haja nada na classe legível que indique que seu objeto nunca pode ser persistido. Por exemplo, eu não chamaria a classe base deReadOnly[Whatever], porque se você tem um método que leva umReadOnlyPerson como um argumento, esse método seria justificado ao assumir que seria impossível que qualquer coisa que fizessem a esse objeto tivesse algum impacto no banco de dados, o que não é necessariamente verdadeiro se a instância real for umaWriteablePerson.

Atualizar

Eu estava originalmente assumindo que na sua aula somente de leitura você só queria impedir que as pessoasSave método. Com base no que estou vendo na sua resposta de resposta à sua pergunta (que, na verdade, deve ser uma atualização da sua pergunta), aqui está um padrão que você pode querer seguir:

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

Isso garante que um verdadeiroReadOnlyPerson não pode simplesmente ser convertido como um ModifiablePerson e modificado. Se você está disposto a confiar que os desenvolvedores não tentarão diminuir os argumentos dessa maneira, prefiro a abordagem baseada em interface nas respostas de Steve e Olivier.

Outra opção seria fazer o seuReadOnlyPerson basta ser uma classe wrapper para umPerson objeto. Isso exigiria mais código clichê, mas é útil quando você não pode alterar a classe base.

Um último ponto, uma vez que você gostou de aprender sobre o Princípio de Substituição de Liskov: Ao ter a classe Person responsável por sair do banco de dados, você está quebrando o Princípio de Responsabilidade Única. Idealmente, sua classe Person teria propriedades para representar os dados que compõem uma "Person" e haveria uma classe diferente (talvez umPersonRepository) é responsável por produzir uma Pessoa a partir do banco de dados ou salvar uma Pessoa no banco de dados.

Atualização 2

Respondendo aos seus comentários:

Embora você possa responder tecnicamente à sua própria pergunta, o StackOverflow é basicamente sobre como obter respostas de outras pessoas. É por isso que você não aceita sua própria resposta até que um certo período de tolerância tenha passado. Você é encorajado a refinar sua pergunta e responder a comentários e respostas até que alguém encontre uma solução adequada para sua pergunta inicial.Eu fiz oReadablePerson classeabstract porque parece que você só quer criar uma pessoa que seja somente de leitura ou uma que seja gravável. Embora ambas as classes filhas possam ser consideradas como ReadablePerson, qual seria o objetivo de criar umanew ReadablePerson() quando você poderia facilmente criar umnew ReadOnlyPerson()? Fazer o resumo da classe requer que o usuário escolha uma das duas classes filhas ao instanciá-las.Um PersonRepository seria como uma fábrica, mas a palavra "repositório" indica que você está realmente extraindo as informações da pessoa de alguma fonte de dados, em vez de criar a pessoa fora do ar.

Na minha opinião, a classe Person seria apenas um POCO, sem lógica: apenas propriedades. O repositório seria responsável pela construção do objeto Person. Em vez de dizer:

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

... e permitindo que o objeto Person vá ao banco de dados para preencher suas várias propriedades, você diria:

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

O PersonRepository então obterá as informações apropriadas do banco de dados e construirá a Pessoa com esses dados.

Se você quisesse chamar um método que não tivesse motivos para alterar a pessoa, poderia protegê-la de alterações convertendo-a em um wrapper Readonly (seguindo o padrão que as bibliotecas .NET seguem com oReadonlyCollection<T> classe). Por outro lado, os métodos que requerem um objeto gravável podem receber oPerson diretamente:

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

O benefício de usar interfaces é que você não está forçando uma hierarquia de classes específica. Isso lhe dará uma melhor flexibilidade no futuro. Por exemplo, digamos que, no momento em que você escreve seus métodos assim:

<code>GetVoteCount(ReadablePerson p);
UpdatePersonDataFromLdap(ReadWritePerson p);
</code>

... mas depois de dois anos você decide mudar para a implementação do wrapper. De repenteReadOnlyPerson não é mais umReadablePerson, porque é uma classe wrapper em vez de uma extensão de uma classe base. Você mudaReadablePerson paraReadOnlyPerson em todas as assinaturas do seu método?

Ou diga que você decide simplificar as coisas e apenas consolidar todas as suas aulas em um únicoPerson class: agora você tem que mudar todos os seus métodos para pegar objetos Person. Por outro lado, se você tivesse programado para interfaces:

<code>GetVoteCount(IReadablePerson p);
UpdatePersonDataFromLdap(IReadWritePerson p);
</code>

... então esses métodos não se importam com a aparência da sua hierarquia de objetos, contanto que os objetos que você fornece implementem as interfaces que eles pedem. Você pode alterar sua hierarquia de implementação a qualquer momento sem precisar alterar esses métodos.

obrigado pelas respostas detalhadas. Eu terei que pensar um pouco sobre eles. user420667
Eu acho que a discussão ficou um pouco fora do tópico, embora eu tenha gostado e acho que toca em alguns dos grandes temas do design. O único ressentimento que eu tenho com a ideia do wrapper é que me permite alterar o objeto que passei para ser empacotado, que requer uma cópia profunda do objeto. Isso significa que eu prefiro minha / nossa abordagem para o msdn :-P. Eu também gosto da ideia de repositório, pois fornece boas camadas e não faz sentido chamar Person.Save (). Talvez alguém possa ter uma interface IObjectRepository. user420667
Além disso, você poderia explicar o que é preferível sobre a abordagem de interface? user420667
Se o objeto que você passa é modificado, então o wrapper (que neste caso deveria ser somente leitura) é modificado. Isto é, a menos que você faça um construtor que passe todos os campos (basicamente uma cópia profunda) ou que seu construtor pegue o objeto a ser empacotado, faça a cópia em si. user420667
0

Eu tive o mesmo problema para resolver ao criar um objeto para permissões de segurança do usuário, que em certos casos deve ser mutável para permitir que usuários de alto nível modifiquem as configurações de segurança, mas normalmente é somente leitura para armazenar as informações de permissões do usuário conectado no momento. sem permitir que o código modifique essas permissões na hora.

O padrão que criei foi definir uma interface que o objeto mutável implemente, que tenha getters de propriedade somente leitura. A implementação mutável dessa interface pode ser privada, permitindo que o código lide diretamente com a instanciação e a hidratação do objeto, mas quando o objeto é retornado desse código (como uma instância da interface), os configuradores não estão mais acessíveis. .

Exemplo:

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

   ...
}
</code>

Um padrão como esse permite usar o código que você já criou de maneira read-write, sem permitir que Joe Programmer transforme uma instância somente leitura em mutável. Existem mais alguns truques em minha implementação real, lidando principalmente com a persistência do objeto editável (já que editar registros de usuário é uma ação segura, um EditableUser não pode ser salvo com o método de persistência "normal" do Repository; também leva um IUser que deve ter permissões suficientes).

Uma coisa você simplesmente precisa entender; Se for possível para o seu programa editar os registros em qualquer escopo, é possível que essa habilidade seja abusada, intencionalmente ou não. Revisões de código regulares de qualquer uso das formas mutáveis ​​ou imutáveis ​​do seu objeto serão necessárias para garantir que outros codificadores não estejam fazendo nada "inteligente". Esse padrão também não é suficiente para garantir que um aplicativo usado pelo público em geral seja seguro; Se você pode escrever uma implementação de IUser, o invasor também pode precisar de alguma maneira adicional para verificar se seu código, e não de um invasor, produziu uma determinada ocorrência de IUser e se a instância não foi adulterada nesse ínterim.

De qualquer forma, eu disse que a capacidade de ir entre instâncias mutáveis ​​e imutáveis ​​de um objeto deve ser monitorada de perto em qualquer situação que possa ter repercussões de segurança. Algumas dessas outras sugestões não são menos vulneráveis. KeithS
O que o impede neste caso é que você não pode acessar o AuthUser; é uma classe aninhada privada não acessível de fora do objeto que realiza autenticação de login. Joe Programmer ainda poderia escrever sua própria implementação de IUser, mas se eu fosse o desenvolvedor sênior ou arquiteto de Joe, eu poderia usar o ReSharper ou um teste de unidade inteligente para pesquisar na base de código por implementações de IUnit que eu não autorizei. Se Joe fosse um invasor, eu consideraria adicionar um valor de hash, gerado pelo meu autenticador de logon (selado e ofuscado), que poderia ser usado para verificar rapidamente se meu código gerou ou não um IUser. KeithS
Exemplo interessante. No entanto, o que está me impedindo de fazer um AuthUser com um usuário editable e, em seguida, alterar o nome do editableuser, alterando assim o nome do autor, o que não deve ser permitido, dada a intenção da interface IUser, certo? user420667
5

Uma opção rápida pode ser criar umIReadablePerson A interface (etc), que contém as propriedades get, e não inclui Save (). Em seguida, você pode fazer com que sua classe existente implemente essa interface e, onde você precisa de acesso somente leitura, faça com que o código consumidor faça referência à classe por meio dessa interface.

De acordo com o padrão, você provavelmente quer ter umIReadWritePerson interface, bem como, que conteria os setters eSave().

Editar Pensando melhor,IWriteablePerson deve ser provavelmenteIReadWritePerson, já que não faria muito sentido ter uma aula somente para gravação.

Exemplo:

<code>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() {}
}
</code>
Absolutamente. Veja o exemplo que eu adicionei, ou veja também a resposta de @Olivier Jacot-Descombes. Steve Czetty
+1 para eu gostar dessa ideia. Mas é fácil fazer essa interface com propriedades? Como se alguém pudesse ter Name {get;} e o outro Name {set; } user420667
6

Definitivamente, não faça a classe somente leitura herdar da classe gravável. Classes derivadas devem estender e modificar os recursos da classe base; eles nunca devem tirar recursos.

Você pode tornar a classe gravável herdada da classe somente leitura, mas é necessário fazer isso com cuidado. A pergunta-chave a ser feita é se algum consumidor da classe read-only confiaria no fato de ser somente leitura? Se um consumidor está contando com os valores nunca mudando, mas o tipo derivado gravável é passado e, em seguida, os valores são alterados, esse consumidor pode ser quebrado.

Eu sei que é tentador pensar que, porque a estrutura dos dois tipos (ou seja, os dados que eles contêm) é semelhante ou idêntica, que um deve herdar do outro. Mas isso muitas vezes não é o caso. Se eles estão sendo projetados para casos de uso significativamente diferentes, eles provavelmente precisam ser classes separadas.

Droga, essa é uma preocupação muito justa e chata. user420667
Você não pode substituir os operadores de cast para tipos na mesma hierarquia (e mesmo se você pudesse, seria uma idéia terrível). Um tipo sempre pode ser impilmente convertido em seu tipo base. David Nelson
Obrigado pelo experimento gedanken. Talvez isso apenas signifique que eu deveria substituir os operadores de casting implícitos e explícitos para evitar o lançamento? Mas então esse tipo de derrota o propósito da herança, não é? Maldito. Deve haver um caminho melhor. user420667
Eu diria que a abordagem apropriada seria ter uma classe "legível" (ou interface) abstrata, que poderia ter classes derivadas graváveis ​​e imutáveis ​​(ou interfaces). Enquanto alguns usuários de uma classe de coleção podem confiar que ela é imutável, e outros podem confiar que ela é imutável, muita coisa não vai se importar de uma forma ou outra, desde que possam lê-la. Não há nenhuma razão para os métodos que não se importam se uma classe é mutável não poder operar da mesma maneira em mutáveis ​​e imutáveis. supercat

Perguntas relacionadas