Como argumentar contra essa mentalidade "completamente pública" do design da classe de objeto de negócios

9

Estamos fazendo muitos testes de unidade e refatoração de nossos objetos de negócios, e eu pareço ter opiniões muito diferentes sobre o design de classe do que outros pares.

Um exemplo de classe da qual eu não sou fã:

public class Foo
{
    private string field1;
    private string field2;
    private string field3;
    private string field4;
    private string field5;

    public Foo() { }

    public Foo(string in1, string in2)
    {
        field1 = in1;
        field2 = in2;
    }

    public Foo(string in1, string in2, string in3, string in4)
    {
        field1 = in1;
        field2 = in2;
        field3 = in3;
    }

    public Prop1
    { get { return field1; } }
    { set { field1 = value; } }

    public Prop2
    { get { return field2; } }
    { set { field2 = value; } }

    public Prop3
    { get { return field3; } }
    { set { field3 = value; } }

    public Prop4
    { get { return field4; } }
    { set { field4 = value; } }

    public Prop5
    { get { return field5; } }
    { set { field5 = value; } }

}

Na classe "real", eles não são todos string, mas em alguns casos, temos 30 campos de apoio para propriedades completamente públicas.

Eu odeio essa aula e não sei se estou sendo exigente. Algumas coisas a serem observadas:

  • Campos de apoio privados, sem lógica nas propriedades, parecem desnecessários e incham a classe
  • Vários construtores (um pouco ok), mas juntamente com
  • todas as propriedades com um setter público, eu não sou fã.
  • Potencialmente, nenhuma propriedade receberia um valor devido ao construtor vazio; se um chamador não souber, você poderá obter algo muito indesejável e difícil de testar quanto ao comportamento.
  • São muitas propriedades! (no caso 30)

Acho muito mais difícil realmente saber em que estado o objeto Fooestá a qualquer momento, como implementador. O argumento foi formulado "talvez não tenhamos as informações necessárias para definir Prop5no momento da construção do objeto. Ok, acho que posso entender isso, mas se for esse o caso, torne apenas o Prop5setter público, nem sempre até 30 propriedades em uma classe.

Estou apenas sendo exigente e / ou louco por querer uma aula que seja "fácil de usar" em vez de ser "fácil de escrever (tudo público)"? Classes como a acima gritam para mim, eu não sei como isso será usado, então apenas tornarei tudo público para o caso .

Se não estou sendo muito exigente, quais são os bons argumentos para combater esse tipo de pensamento? Não sou muito bom em articular argumentos, pois fico muito frustrado ao tentar expressar meu ponto de vista (não intencionalmente, é claro).

Kritner
fonte
3
Um objeto deve sempre estar em um estado válido; ou seja, você não precisa ter objetos parcialmente instanciados . Algumas informações não fazem parte do estado central de um objeto enquanto outras informações são (por exemplo, uma tabela deve ter pernas, mas o pano é opcional); informações opcionais podem ser fornecidas após a instanciação; as informações principais devem ser fornecidas na instanciação. Se algumas informações são essenciais, mas não são conhecidas na instanciação, eu diria que as classes precisam ser refatoradas. É difícil dizer mais sem conhecer o contexto da classe.
Marvin
11
Dizer "talvez não tenhamos as informações necessárias para definir Prop5 no momento da construção do objeto" me faz perguntar "você está dizendo que haverá algumas vezes que você terá essas informações no momento da construção e outras em que não tem essa informação? " Isso me faz pensar se há realmente duas coisas diferentes que essa classe está tentando representar, ou talvez se essa classe deve ser dividida em classes menores. Mas se for feito "por precaução", isso também é ruim, IMO; isso me diz que não existe um design claro para o modo como os objetos são criados / preenchidos.
Aprendiz do Dr. Wily
3
Para os campos de apoio privados sem lógica nas propriedades, existe uma razão pela qual você não está usando propriedades automáticas ?
Carson63000
2
@Kritner todo o ponto de auto-propriedades é que se você precisa de lógica real lá no futuro, você pode facilmente adicioná-lo no lugar do auto getou set:-)
Carson63000

Respostas:

10

Classes completamente públicas têm uma justificativa para certas situações, bem como as outras classes extremas, com apenas um método público (e provavelmente muitos métodos particulares). E aulas com alguns públicos, alguns métodos privados também.

Tudo depende do tipo de abstração que você vai modelar com eles, quais camadas você possui em seu sistema, o grau de encapsulamento necessário nas diferentes camadas e (é claro) que escola de pensamento o autor da turma vem de. Você pode encontrar todos esses tipos no código SOLID.

Existem livros inteiros escritos sobre quando preferir que tipo de design, por isso não vou listar nenhuma regra aqui, o espaço nesta seção não seria suficiente. No entanto, se você tiver um exemplo do mundo real para uma abstração que você gosta de modelar com uma classe, tenho certeza de que a comunidade aqui terá todo o prazer em ajudá-lo a melhorar o design.

Para abordar seus outros pontos:

  • "campos de apoio privados sem lógica nas propriedades": Sim, você está certo. Para getters e setters triviais, isso é apenas um "ruído" desnecessário. Para evitar esse tipo de "inchaço", o C # possui uma sintaxe de atalho para os métodos get / set da propriedade:

Então, ao invés de

   private string field1;
   public string Prop1
   { get { return field1; } }
   { set { field1 = value; } }

Escreva

   public string Prop1 { get;set;}

ou

   public string Prop1 { get;private set;}
  • "Vários construtores": isso não é um problema em si. Ocorre um problema quando há duplicação de código desnecessária, como mostrado no seu exemplo, ou a hierarquia de chamada é complicada. Isso pode ser facilmente resolvido refatorando partes comuns em uma função separada e organizando a cadeia de construtores de maneira unidirecional

  • "Potencialmente, nenhuma propriedade teria um valor atribuído ao construtor vazio": em C #, todo tipo de dados tem um valor padrão claramente definido. Se as propriedades não forem inicializadas explicitamente em um construtor, elas receberão esse valor padrão atribuído. Se isso for usado intencionalmente , está perfeitamente correto - portanto, um construtor vazio pode estar correto se o autor souber o que está fazendo.

  • "São muitas propriedades! (No caso 30)": sim, se você é livre para projetar essa classe de maneira greenfield, 30 são demais, eu concordo. No entanto, nem todos nós temos esse luxo (você não escreveu no comentário abaixo, é um sistema legado?). Às vezes, você precisa mapear registros de um banco de dados, arquivo ou dados existente de uma API de terceiros para o seu sistema. Portanto, nesses casos, 30 atributos podem ser algo com que se deve conviver.

Doc Brown
fonte
Obrigado, sim, eu sei que o POCO / POJOs tem o seu lugar, e meu exemplo é bastante abstrato. Eu não acho que poderia ser mais específico sem entrar em um romance, no entanto.
precisa saber é
@Kritner: veja minha edição
Doc Brown
Sim, normalmente, quando estou criando uma nova classe, vou pela rota da propriedade automática. Ter os campos de apoio privados "apenas porque" é (na minha opinião) desnecessário e até causa problemas. Quando você tem ~ 30 propriedades para uma classe e mais de 100 classes, não é fácil dizer que haverá alguma configuração de propriedades ou obtenção do campo de apoio incorreto ... Eu já encontrei isso várias vezes na refatoração. :)
Kritner
obrigado, o valor padrão para string nullé não é? portanto, se uma das propriedades realmente usada em a .ToUpper(), mas um valor nunca for definido para ela, seria lançada uma exceção de tempo de execução. Este é outro exemplo bom porque os necessários dados para a classe deve ter para ser definido durante a construção objeto. Não basta deixar para o usuário. THanks
Kritner
Além de muitas propriedades não definidas, o principal problema dessa classe é que ela possui lógica zero (ZRP) e é o arquétipo de um modelo anêmico. Sem uma necessidade disso que possa ser expressa em palavras, como um DTO, é um design ruim.
user949300
2
  1. Ao dizer que "Campos de apoio privados sem lógica nas propriedades parecem desnecessários e incham a classe", você já tem um argumento decente.

  2. Não parece que vários construtores sejam o problema aqui.

  3. Quanto a ter todas as propriedades como públicas ... Talvez pense dessa maneira, se você quiser sincronizar o acesso a todas essas propriedades entre os threads, será muito difícil, pois poderá escrever código de sincronização em qualquer lugar em que esteja usava. No entanto, se todas as propriedades fossem incluídas por getters / setters, você poderia facilmente criar a sincronização na classe.

  4. Acho que quando você diz "difícil testar o comportamento", o argumento fala por si. Seu teste pode ter que verificar se não é nulo ou algo parecido (em todos os locais em que a propriedade é usada). Realmente não sei porque não sei como é seu teste / aplicativo.

  5. Você está certo em muitas propriedades. Você poderia tentar usar herança (se as propriedades forem semelhantes o suficiente) e depois ter uma lista / matriz usando genéricos? Em seguida, você pode escrever getters / setters para acessar as informações e simplificar a maneira como você interagirá com todas as propriedades.

Snoop
fonte
11
Obrigado - # 1 e # 4 Eu definitivamente me sinto mais à vontade em argumentar. Não tenho muita certeza do que você quer dizer no # 3. A maioria das propriedades das classes compõe a chave primária no banco de dados. Usamos chaves competitivas, às vezes até 8 colunas - mas isso é outra questão. Eu estava pensando em tentar colocar as partes que compõem a chave em sua própria classe / estrutura, o que eliminaria muitas propriedades (pelo menos nessa classe) - mas esse é um aplicativo legado com milhares de linhas de código com vários chamadores. Então eu acho que eu tenho um monte de pensar sobre :)
Kritner
Eh. Se a classe fosse imutável, não haveria razão para escrever qualquer código de sincronização dentro da classe.
precisa
@RubberDuck Esta classe é imutável? O que você quer dizer?
Snoop
3
Definitivamente não é imutável @StevieV. Qualquer classe com setters de propriedades é mutável.
precisa
11
@Kritner Como as propriedades são usadas como chaves primárias? Presumivelmente, isso requer alguma lógica (verificações nulas) ou regras (por exemplo, NAME tem prioridade sobre AGE). De acordo com o OOP, esse código pertence a essa classe. Você poderia usar isso como argumento?
user949300
2

Como você me disse, Fooé um objeto de negócios. Ele deve encapsular algum comportamento nos métodos de acordo com o seu domínio e seus dados devem permanecer o mais encapsulados possível.

No exemplo que você mostra, Fooparece mais um DTO e vai contra todos os princípios de POO (consulte Modelo de domínio anêmico )!

Como melhorias, eu sugeriria:

  • Torne esta classe o mais imutável possível. Como você disse, você deseja fazer alguns testes de unidade. Uma capacidade obrigatória para o teste de unidade é o determinismo , e o determinismo pode ser alcançado através da imutabilidade, porque resolve os problemas dos efeitos colaterais .
  • Divida essa classe divina em várias classes que fazem apenas 1 coisa cada (consulte SRP ). Teste de unidade em uma classe de 30 atributos em realmente uma PITA .
  • Remova a redundância do construtor tendo apenas 1 construtor principal e os outros chamando o construtor principal.
  • Remova getters desnecessários, duvido seriamente que todos os getters sejam realmente úteis.
  • Trazer de volta a lógica de negócios nas classes de negócios, é aqui que ele pertence!

Esta poderia ser uma classe refatorada em potencial com estas observações:

public sealed class Foo
{
    private readonly string field1;
    private readonly string field2;
    private readonly string field3;
    private readonly string field4;

    public Foo() : this("default1", "default2")
    { }

    public Foo(string in1, string in2) : this(in1, in2, "default3", "default4")
    { }

    public Foo(string in1, string in2, string in3, string in4)
    {
        field1 = in1;
        field2 = in2;
        field3 = in3;
        field4 = in4;
    }

    //Methods with business logic

    //Really needed ?
    public Prop1
    { get; }

    //Really needed ?
    public Prop2
    { get; }

    //Really needed ?
    public Prop3
    { get; }

    //Really needed ?
    public Prop4
    { get; }

    //Really needed ?
    public Prop5
    { get; }
}
Visto
fonte
2

Como argumentar contra essa mentalidade "completamente pública" do design da classe de objeto de negócios

  • "Existem vários métodos espalhados na (s) classe (s) cliente (s) que fazem mais do que 'mero ​​dever de DTO'. Eles pertencem um ao outro."
  • "Pode ser um 'DTO', mas tem identidade comercial - precisamos substituir os iguais".
  • "Precisamos classificar isso - precisamos implementar o IComparable"
  • "Estamos reunindo várias dessas propriedades. Vamos substituir o ToString para que todos os clientes não precisem escrever esse código."
  • "Temos vários clientes fazendo o mesmo com essa classe. Precisamos secar o código".
  • "O cliente está manipulando uma coleção dessas coisas. É óbvio que deve ser uma classe de coleção personalizada; e muitos dos pontos anteriores tornarão essa coleção personalizada mais funcional do que atualmente".
  • "Veja toda a manipulação tediosa e exposta de strings! Encapsulando que em métodos relevantes para os negócios aumentará nossa produtividade de codificação".
  • "Reunir esses métodos na classe os tornará testáveis ​​porque não precisamos lidar com a classe cliente mais complexa".
  • "Agora podemos testar unitariamente esses métodos refatorados. Como é, por razões x, y, z, o cliente não pode ser testado.

  • Na medida em que qualquer um dos argumentos acima possa ser apresentado, agregado:

    • A funcionalidade se torna reutilizável.
    • Está seco
    • É dissociado dos clientes.
    • codificar contra a classe é mais rápido e menos propenso a erros.
  • "Uma classe bem feita oculta o estado e expõe a funcionalidade. Essa é a proposta inicial para o design de classes OO".
  • "O resultado final faz um trabalho muito melhor de expressar o domínio comercial; as entidades distintas e sua interação explícita".
  • "Essa funcionalidade 'aérea' (ou seja, requisitos funcionais não explícitos, mas precisa ser feita) se destaca claramente e segue o Princípio de Responsabilidade Única.
radarbob
fonte