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).
fonte
Assert(5 = Math.Abs(-5));
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 noAbs()
(ou seu código dependente) e remove os benefícios de diagnóstico dos testes de unidade.Respostas:
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.
fonte
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,static
você não pode testarCUT()
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()
noforProduct()
(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.
fonte
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.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.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 - umTwitterReviewRetriever
ou umAmazonReviewRetriever
ouMultipleSourceReviewRetriever
ou 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
ProductWithReviews
se você realmente quisesse ser pedante sobre seus princípios do SOLID, mas isso seria bom o suficiente para mim).fonte
IRetrieveReviews
impede 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.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.
fonte
Eu não colocaria a funcionalidade 'Obter comentários para o produto' na
Product
classe nem naReview
classe ...Você tem um lugar para recuperar seus produtos, certo? Algo com
GetProductById(int productId)
e talvezGetProductsByCategory(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 umGetReviewsForProduct(int productId)
.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.
fonte
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.
fonte
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.
fonte
Eu tenho uma opinião um pouco diferente do que a maioria das outras respostas. Eu acho que
Product
eReview
sã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:
Então o seu real
ProductRepository
retornaria umExistingProduct
para oGetProductByProductId
método, etc.Agora você está seguindo o princípio de responsabilidade única (tudo o que herda
IProduct
é apenas um estado e o queIProductRepository
é 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. :)
fonte
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
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.
fonte