Métodos estáticos vs. estáticos

11

Aqui está um problema com que me deparo com frequência: que haja um projeto de loja virtual que tenha uma classe de produto. Desejo adicionar um recurso que permita aos usuários publicar comentários em um produto. Então, eu tenho uma classe Review que faz referência a um produto. Agora eu preciso de um método que lista todas as revisões de um produto. Existem duas possibilidades:

(UMA)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

Olhando para o código, eu escolheria (A): não é estático e não precisa de um parâmetro. No entanto, sinto que (A) viola o Princípio da Responsabilidade Única (SRP) e o Princípio Aberto-Fechado (OCP), enquanto (B) não:

  • (SRP) Quando quero alterar a maneira como as revisões são coletadas para um produto, preciso alterar a classe do produto. Mas deve haver apenas um motivo para alterar a classe do produto. E isso certamente não são os comentários. Se eu compilar todos os recursos que têm algo a ver com produtos no Produto, em breve eles estarão espalhados.

  • (OCP) Preciso alterar a classe do produto para estendê-la com esse recurso. Penso que isto viola a parte do princípio "Fechado para mudança". Antes de receber a solicitação do cliente para implementar as revisões, considerei o Produto concluído e o "fechei".

O que é mais importante: seguir os princípios do SOLID ou ter uma interface mais simples?

Ou estou fazendo algo errado aqui?

Resultado

Uau, obrigado por todas as suas ótimas respostas! É difícil escolher uma como resposta oficial.

Deixe-me resumir os principais argumentos das respostas:

  • pro (A): OCP não é uma lei e a legibilidade do código também é importante.
  • pro (A): o relacionamento da entidade deve ser navegável. Ambas as classes podem conhecer esse relacionamento.
  • pro (A) + (B): faça as duas coisas e delegue em (A) a (B) para que o produto tenha menos probabilidade de ser alterado novamente.
  • pro (C): coloque métodos finder na terceira classe (serviço) onde não é estático.
  • contra (B): impede zombaria nos testes.

Algumas coisas adicionais que minhas faculdades no trabalho contribuíram:

  • pro (B): nossa estrutura ORM pode gerar automaticamente o código para (B).
  • pro (A): por razões técnicas da nossa estrutura ORM, será necessário alterar a entidade "fechada" em alguns casos, independentemente de onde o localizador for. Portanto, nem sempre eu posso continuar com o SOLID.
  • contra (C): muito barulho ;-)

Conclusão

Estou usando os dois (A) + (B) com delegação para o meu projeto atual. Em um ambiente orientado a serviços, no entanto, irei com (C).

Wolfgang
fonte
2
Desde que não seja uma variável estática, tudo é legal. Os métodos estáticos são simples de testar e simples de rastrear.
Coder
3
Por que não apenas ter uma classe ProductsReviews? Em seguida, Produto e Revisão permanecem os mesmos. Ou talvez eu entenda mal.
ElGringoGrande
2
@ Codificador "Métodos estáticos são simples de testar", sério? Eles não podem ser ridicularizados, consulte: googletesting.blogspot.com/2008/12/… para obter mais detalhes.
StuperUser
1
@ SuperUser: Não há nada para zombar. Assert(5 = Math.Abs(-5));
15/12
2
Testar Abs()não é o problema, testar algo que depende disso. Você não tem uma costura para isolar o CUT (Code-Under-Test) dependente para usar uma simulação. Isso significa que você não pode testá-lo como uma unidade atômica e todos os seus testes se tornam testes de integração que testam a lógica da unidade. Uma falha em um teste pode estar na CUT ou no Abs()(ou seu código dependente) e remove os benefícios de diagnóstico dos testes de unidade.
StuperUser

Respostas:

7

O que é mais importante: seguir os princípios do SOLID ou ter uma interface mais simples?

Interface com o SOLID

Eles não são mutuamente exclusivos. A interface deve expressar a natureza do seu modelo de negócios idealmente em termos de modelo de negócios. Os princípios do SOLID são um Koan para maximizar a manutenção do código orientado a objetos (e eu quero dizer "manutenção" no sentido mais amplo). O primeiro suporta o uso e a manipulação do seu modelo de negócios e o segundo otimiza a manutenção do código.

Princípio Aberto / Fechado

"não toque nisso!" é uma interpretação simplista demais. E assumindo que queremos dizer "a classe" é arbitrária e não necessariamente correta. Em vez disso, o OCP significa que você projetou seu código de forma que modificar seu comportamento não exige (não deve) que você modifique diretamente o código de trabalho existente. Além disso, não tocar no código em primeiro lugar é a maneira ideal de preservar a integridade das interfaces existentes; este é um corolário significativo da OCP, na minha opinião.

Finalmente, vejo o OCP como um indicador da qualidade do design existente. Se eu me descobrir violando classes abertas (ou métodos) com muita frequência e / ou sem razões realmente sólidas (ha, ha) para fazê-lo, isso pode estar me dizendo que eu tenho um design ruim (e / ou não) saber codificar OO).

Dê o seu melhor, temos uma equipe de médicos à disposição

Se a sua análise de requisitos indicar que você precisa expressar o relacionamento Revisão do Produto de ambas as perspectivas, faça-o.

Portanto, Wolfgang, você pode ter um bom motivo para modificar essas classes existentes. Dado os novos requisitos, se uma Revisão for agora uma parte fundamental de um Produto, se todas as extensões do Produto precisarem de Revisão, se isso fizer com que o código do cliente seja adequadamente expressivo, integre-o ao Produto.

radarbob
fonte
1
+1 por observar que o OCP é mais um indicador de boa qualidade, não uma regra rápida para qualquer código. Se você achar que é difícil ou impossível seguir adequadamente o OCP, é um sinal de que seu modelo precisa ser refatorado para permitir uma reutilização mais flexível.
CodexArcanum 16/03/12
Eu escolhi o exemplo de Produto e Revisão para ressaltar que o Produto é uma entidade principal do projeto, enquanto a Revisão é um mero complemento. Portanto, o produto já existe, terminou e a revisão vem mais tarde e deve ser introduzida sem a abertura do código existente (incluindo o produto).
Wolfgang
10

SÓLIDO são diretrizes, portanto, influencie a tomada de decisão em vez de ditá-la.

Uma coisa a ter em atenção ao usar métodos estáticos é o seu impacto na testabilidade .


Testar forProduct(Product product)não será um problema.

Testar algo que depende disso será.

Você não terá uma costura para isolar o CUT (Code-Under-Test) dependente para usar uma simulação, pois quando o aplicativo está sendo executado, os métodos estáticos necessariamente existem.

Vamos ter um método chamado CUT()que chamaforProduct()

Se forProduct()for, staticvocê não pode testar CUT()como uma unidade atômica e todos os seus testes se tornam testes de integração que testam a lógica da unidade.

Uma falha em um teste para CUT pode ser causada por um problema CUT()no forProduct()(ou em qualquer um de seu código dependente) que remove os benefícios de diagnóstico dos testes de unidade.

Consulte esta excelente publicação no blog para obter informações mais detalhadas: http://googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


Isso pode levar à frustração com falhas nos testes e ao abandono de boas práticas e benefícios que os cercam.

StuperUser
fonte
1
Esse é um ponto muito bom. Em geral, não para mim. ;-) Eu não sou tão rigoroso em escrever testes de unidade que abrangem mais de uma classe. Todos eles vão para o banco de dados, tudo bem para mim. Portanto, não zombarei do objeto de negócios ou do localizador.
Wolfgang
+1, obrigado por definir a bandeira vermelha para os testes! Concordo com o @Wolfgang, geralmente também não sou tão rigoroso, mas quando preciso desse tipo de teste, odeio métodos estáticos. Normalmente, se um método estático interage demais com seus parâmetros ou se interage com qualquer outro método estático, prefiro torná-lo um método de instância.
Adriano Repetti
Isso não depende inteiramente do idioma que está sendo usado? O OP usou um exemplo de Java, mas nunca mencionou uma linguagem na pergunta, nem é especificada nas tags.
Izkata 16/03/12
1
@StuperUser If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.- Eu acredito que Javascript e Python permitem que métodos estáticos sejam substituídos / zombados. Não tenho 100% de certeza.
Izkata 16/03/12
1
O @Izkata JS é digitado dinamicamente, portanto, não possui static, você simula-o com um fechamento e o padrão singleton. Lendo sobre Python (especialmente stackoverflow.com/questions/893015/… ), você precisa herdar e estender. Substituir não é zombar; Parece que você ainda não tem uma costura para testar o código como uma unidade atômica.
StuperUser
4

Se você acha que o Produto é o local certo para encontrar as Revisões do produto, sempre pode dar ao Produto uma classe de ajuda para fazer o trabalho. (Você pode saber porque sua empresa nunca fala sobre uma revisão, exceto em termos de um produto).

Por exemplo, eu ficaria tentado a injetar algo que desempenhava o papel de recuperador de críticas. Eu provavelmente daria a interface IRetrieveReviews. Você pode colocar isso no construtor do produto (injeção de dependência). Se você quiser mudar a forma como os comentários são recuperados você pode fazê-lo facilmente através da injeção de um colaborador diferente - um TwitterReviewRetrieverou um AmazonReviewRetrieverou MultipleSourceReviewRetrieverou qualquer outra coisa que você precisa.

Agora, ambos têm uma única responsabilidade (sendo o objetivo de todas as coisas relacionadas a produtos e recuperando resenhas, respectivamente) e, no futuro, o comportamento do produto em relação a resenhas pode ser modificado sem realmente alterar o produto (você pode estendê-lo como ProductWithReviewsse você realmente quisesse ser pedante sobre seus princípios do SOLID, mas isso seria bom o suficiente para mim).

Lunivore
fonte
Parece o padrão DAO que é muito comum em software orientado a serviços / componentes. Eu gosto dessa idéia porque expressa que a recuperação de objetos não é de responsabilidade deles. No entanto, desde que eu prefiro uma maneira orientada a objetos em vez de orientada a serviços.
Wolfgang
1
Usar uma interface como a IRetrieveReviewsimpede de ser orientada a serviços - não determina o que recebe as críticas, nem como, nem quando. Talvez seja um serviço com muitos métodos para coisas assim. Talvez seja uma classe que faça uma coisa. Talvez seja um repositório ou faça uma solicitação HTTP para um servidor. Você não sabe. Você não deveria saber. Essa é a questão.
Lunivore 15/03/12
Sim, seria uma implementação do padrão de estratégia. O que seria um argumento para uma terceira classe. (A) e (B) não suportariam isso. O localizador definitivamente usará um ORM, portanto não há razão para substituir o algoritmo. Desculpe se eu não estava claro sobre isso na minha pergunta.
Wolfgang
3

Eu teria uma classe ProductsReview. Você diz que a revisão é nova de qualquer maneira. Isso não significa que pode ser qualquer coisa. Ele ainda deve ter apenas um motivo para mudar. Se você alterar a forma como recebe as críticas por qualquer motivo, terá que alterar a classe Review.

Isso não está correto.

Você está colocando o método estático na classe Review porque ... por quê? Não é com isso que você está lutando? Não é esse o problema todo?

Então não. Faça uma aula que tenha como única responsabilidade obter as análises dos produtos. Você pode subclassificá-lo para ProductReviewsByStartRating, seja qual for. Ou subclasse para obter análises de uma classe de produtos.

ElGringoGrande
fonte
Não concordo que o método Revisar viole o SRP. No Produto, seria, mas não na Revisão. Meu problema era que é estático. Se eu movesse o método para alguma terceira classe, ele ainda seria estático e teria o parâmetro product.
Wolfgang
Então, você está dizendo que a única responsabilidade pela classe Review, a única razão para ela mudar, é se o método estático do Product precisou mudar? Não há outra funcionalidade na classe Review?
ElGringoGrande
Há muitas coisas na Review. Mas acho que um forProduct (Product) se encaixaria perfeitamente nele. A localização de revisões depende muito dos atributos e da estrutura da revisão (quais identificadores exclusivos, quais escopos alteram os atributos?). O Produto, no entanto, não sabe e não deve saber sobre Revisões.
Wolfgang
3

Eu não colocaria a funcionalidade 'Obter comentários para o produto' na Productclasse nem na Reviewclasse ...

Você tem um lugar para recuperar seus produtos, certo? Algo com GetProductById(int productId)e talvez GetProductsByCategory(int categoryId)e assim por diante.

Da mesma forma, você deve ter um local para recuperar seus comentários, com um GetReviewbyId(int reviewId)e talvez um GetReviewsForProduct(int productId).

Quando quero alterar a maneira como as revisões são coletadas para um produto, preciso alterar a classe do produto.

Se você separar o acesso aos dados das classes de domínio, não precisará alterar nenhuma das classes ao alterar a maneira como as revisões são coletadas.

Eric King
fonte
É assim que eu lidaria com isso, e estou confuso (e preocupado se minhas próprias idéias são adequadas) pela falta dessa resposta até o seu post. Claramente, nem a classe que representa um relatório nem a representação de um produto devem ser responsáveis ​​por recuperar o outro. Algum tipo de dados que fornece serviços deve lidar com isso.
CodexArcanum 16/03/12
@ CodexArcanum Bem, você não está sozinho. :-)
Eric King
2

Padrões e princípios são diretrizes, não regras escritas na pedra. Na minha opinião, a questão não é se é melhor seguir os princípios do SOLID ou manter uma interface mais simples. O que você deve se perguntar é o que é mais legível e compreensível para a maioria das pessoas. Geralmente, isso significa que ele deve estar o mais próximo possível do domínio.

Nesse caso, eu preferiria a solução (B) porque, para mim, o ponto de partida é o Produto, não a Revisão, mas imagine que você esteja escrevendo um software para gerenciar revisões. Nesse caso, o centro é a Revisão, portanto a solução (A) pode ser preferível.

Quando tenho muitos métodos como este ("conexões" entre classes), retiro todos eles para fora e crio uma (ou mais) nova classe estática para organizá-los. Geralmente você pode vê-los como consultas ou tipo de repositório.

Adriano Repetti
fonte
Eu também estava pensando nessa idéia da semântica de ambas as extremidades e qual delas é "mais forte" para reivindicar o buscador. Por isso eu inventei (B). Também ajudaria a criar uma hierarquia de "abstração" das classes de negócios. O produto era um mero objeto básico, onde o Review é maior. Portanto, a Revisão pode fazer referência ao Produto, mas não o contrário. Dessa forma, é possível evitar ciclos de referências entre as classes de negócios, o que seria outro problema da minha lista resolvido.
Wolfgang
Eu acho que, para um pequeno conjunto de classes, seria bom fornecer os dois métodos (A) e (B). Muitas vezes a interface do usuário não é bem conhecida no momento de escrever essa lógica.
Adriano Repetti
1

Seu Produto pode apenas delegar para o seu método estático de Revisão. Nesse caso, você está fornecendo uma interface conveniente em um local natural (Product.getReviews), mas os detalhes da sua implementação estão em Review.getForProduct.

SÓLIDO são diretrizes e devem resultar em uma interface simples e sensível. Como alternativa, você pode derivar o SOLID a partir de interfaces simples e sensíveis. É tudo sobre gerenciamento de dependência no código. O objetivo é minimizar as dependências que criam atritos e barreiras à mudança inevitável.

pfries
fonte
Não tenho certeza se gostaria disso. Dessa forma, eu tenho o método nos dois lugares, o que é bom porque outros desenvolvedores não precisam procurar nas duas classes para ver onde eu o coloco. Por outro lado, eu teria as duas desvantagens do método estático e a alteração da classe de produto "fechada". Não tenho certeza de que a delegação ajude a resolver o problema do atrito. Se alguma vez houve um motivo para alterar o localizador, certamente resultará na alteração da assinatura e, portanto, na alteração do Produto novamente.
21912 Wolfgang #
A chave do OCP é que você pode substituir implementações sem alterar seu código. Na prática, isso está intimamente ligado à substituição de Liskov. Uma implementação deve ser substituída por outra. Você pode ter DatabaseReviews e SoapReviews como classes diferentes, implementando IGetReviews.getReviews e getReviewsForProducts. Em seguida, seu sistema está aberto para extensão (alterando como as revisões são obtidas) e fechado para modificação (as dependências do IGetReviews não quebram). É isso que quero dizer com gerenciamento de dependência. No seu caso, você precisará modificar seu código para alterar seu comportamento.
Pfries
Não é esse o princípio da inversão de dependência (DIP) que você está descrevendo?
Wolfgang
Eles são princípios relacionados. Seu ponto de substituição é alcançado pelo DIP.
Pfries
1

Eu tenho uma opinião um pouco diferente do que a maioria das outras respostas. Eu acho que Producte Reviewsão basicamente objetos de transferência de dados (DTOs). No meu código, tento fazer com que meus DTOs / Entidades evitem ter comportamentos. Eles são apenas uma API agradável para armazenar o estado atual do meu modelo.

Quando você fala sobre OO e SOLID, geralmente fala sobre um "objeto" que não representa estado (necessariamente), mas representa algum tipo de serviço que responde perguntas para você ou para o qual você pode delegar parte do seu trabalho. . Por exemplo:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

Então o seu real ProductRepositoryretornaria um ExistingProductpara o GetProductByProductIdmétodo, etc.

Agora você está seguindo o princípio de responsabilidade única (tudo o que herda IProducté apenas um estado e o que IProductRepositoryé herdado é responsável por saber como persistir e reidratar seu modelo de dados).

Se você alterar o esquema do banco de dados, poderá alterar a implementação do repositório sem alterar os DTOs, etc.

Então, resumindo, acho que não escolheria nenhuma das suas opções. :)

Scott Whitlock
fonte
0

Os métodos estáticos podem ser mais difíceis de testar, mas isso não significa que você não pode usá-los - você só precisa configurá-los para que não precise testá-los.

Crie os métodos product.GetReviews e Review.ForProduct de uma linha que são algo como

new ReviewService().GetReviews(productID);

ReviewService contém todo o código mais complexo e possui uma interface projetada para teste, mas não exposta diretamente ao usuário.

Se você precisar ter 100% de cobertura, faça com que seus testes de integração chamem os métodos de classe de produto / revisão.

Pode ajudar se você pensar no design de API pública em vez de no design de classe - nesse contexto, basta uma interface simples com agrupamento lógico - a estrutura de código real é importante apenas para os desenvolvedores.

Tom Clarkson
fonte