Princípios do SOLID e estrutura de código

150

Em uma recente entrevista de emprego, não consegui responder a uma pergunta sobre o SOLID - além de fornecer o significado básico dos vários princípios. Isso realmente me incomoda. Passei alguns dias pesquisando e ainda tenho que apresentar um resumo satisfatório.

A pergunta da entrevista foi:

Se você visse um projeto .Net que eu lhe disse que seguia estritamente os princípios do SOLID, o que você esperaria ver em termos de projeto e estrutura de código?

Andei um pouco, realmente não respondi a pergunta e depois fui bombardeada.

Como eu poderia ter lidado melhor com essa questão?

Unidade S
fonte
3
Eu queria saber o que não é claro na página wiki de SOLID
BЈовић
Blocos de construção abstratos extensíveis.
Rwong
Seguindo os Princípios do SOLID de Design Orientado a Objetos, suas aulas naturalmente tenderão a ser pequenas, bem fatoradas e facilmente testadas. Fonte: docs.asp.net/pt/latest/fundamentals/…
WhileTrueSleep

Respostas:

188

S = Princípio da responsabilidade única

Então, eu esperaria ver uma estrutura de pastas / arquivos bem organizada e hierarquia de objetos. Cada classe / parte da funcionalidade deve receber um nome que sua funcionalidade é muito óbvia e deve conter apenas lógica para executar essa tarefa.

Se você visse grandes classes de gerentes com milhares de linhas de código, isso seria um sinal de que uma única responsabilidade não estava sendo seguida.

O = Princípio aberto / fechado

Essa é basicamente a ideia de que novas funcionalidades devem ser adicionadas por meio de novas classes que tenham um impacto mínimo / exigem modificação da funcionalidade existente.

Eu esperaria ver muito uso de herança de objeto, sub-digitação, interfaces e classes abstratas para separar o design de uma parte da funcionalidade da implementação real, permitindo que outras pessoas venham e implementem outras versões ao lado sem afetar o original.

L = princípio de substituição de Liskov

Isso tem a ver com a capacidade de tratar subtipos como o tipo pai. Isso sai da caixa em C # se você estiver implementando uma hierarquia de objetos herdada adequada.

Eu esperaria ver o código tratando objetos comuns como seu tipo base e chamando métodos nas classes base / abstract em vez de instanciar e trabalhar nos próprios subtipos.

I = Princípio de Segregação de Interface

Isso é semelhante ao SRP. Basicamente, você define subconjuntos menores de funcionalidade como interfaces e trabalha com aqueles para manter seu sistema dissociado (por exemplo, um FileManagerpode ter a única responsabilidade de lidar com a E / S do arquivo, mas isso poderia implementar um IFileReadere IFileWriterque continha as definições de método específicas para a leitura e gravação de arquivos).

D = Princípio da inversão de dependência.

Novamente, isso se refere a manter um sistema desacoplado. Talvez você esteja atento ao uso de uma biblioteca do .NET Dependency Injection, sendo usada na solução como Unityou Ninjectou em um sistema ServiceLocator como AutoFacServiceLocator.

Eoin Campbell
fonte
36
Eu já vi muitas violações de LSP em C #, toda vez que alguém decide que seu subtipo específico é especializado e, portanto, não precisa implementar uma parte da interface e apenas gera uma exceção nessa parte ... Essa é uma abordagem comum para juniores para resolver o problema da implementação da interface mal-entendido e design
Jimmy Hoffa
2
@ JimmyHoffa Essa é uma das principais razões pelas quais insisto em usar os Contratos de Código; passar pelo processo de elaboração dos contratos ajuda bastante a tirar as pessoas desse mau hábito.
21413 Andy
12
Eu não gosto do "LSP sai da caixa em C #" e iguala DIP à prática de injeção de dependência.
Euphoric
3
+1, mas Inversão de Dependência <> Injeção de Dependência. Eles jogam bem juntos, mas a inversão de dependência é muito mais do que apenas injeção de dependência. Referência: DIP em estado selvagem
Marjan Venema
3
@ Andy: o que ajuda também são os testes de unidade definidos nas interfaces nas quais todos os implementadores (qualquer classe que pode / é instanciada) são testados.
Marjan Venema
17

Muitas classes pequenas e interfaces com injeção de dependência em todo o lugar. Provavelmente, em um grande projeto, você também usaria uma estrutura de IoC para ajudá-lo a construir e gerenciar a vida útil de todos esses pequenos objetos. Consulte https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-looking-into

Observe que um grande projeto .NET que segue estritamente os princípios do SOLID não significa necessariamente uma boa base de código para trabalhar com todos. Dependendo de quem foi o entrevistador, ele / ela pode querer que você mostre que entende o que o SOLID significa e / ou verifique como dogmaticamente você segue os princípios de design.

Veja bem, para ser SÓLIDO, você precisa seguir:

S princípio da responsabilidade ingle, assim você terá muitas pequenas aulas de cada um deles fazendo uma coisa única

O princípio de caneta fechada, que no .NET geralmente é implementado com injeção de dependência, que também requer o I e o D abaixo ...

O princípio da substituição de Iskov provavelmente é impossível de explicar em c # com uma única linha. Felizmente, existem outras perguntas para resolver, por exemplo, https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

O Princípio da Segregação de Interface funciona em conjunto com o princípio Aberto-Fechado. Se seguido literalmente, significaria preferir um grande número de interfaces muito pequenas do que poucas interfaces "grandes"

D princípio de inversão ependency as classes de alto nível não deve depender em classes de baixo nível, ambos devem depender de captações.

Paolo Falabella
fonte
SRP não significa "faça apenas uma coisa".
31718 Robert Harvey
13

Algumas coisas básicas que eu esperaria ver na base de código de uma loja que defendia o SOLID em seu trabalho diário:

  • Muitos arquivos de código pequenos - com uma classe por arquivo como prática recomendada no .NET e o Princípio de Responsabilidade Única incentivando pequenas estruturas modulares de classe, eu esperaria ver muitos arquivos cada um contendo uma classe pequena e focada.
  • Muitos padrões de adaptador e composto - eu esperaria o uso de muitos padrões de adaptador (uma classe que implementa uma interface "passando" para a funcionalidade de uma interface diferente) para simplificar a conexão de uma dependência desenvolvida para uma finalidade lugares diferentes onde sua funcionalidade também é necessária. Atualizações tão simples quanto substituir um registrador de console por um registrador de arquivos violarão o LSP / ISP / DIP se a interface for atualizada para expor um meio de especificar o nome do arquivo a ser usado; em vez disso, a classe do registrador de arquivos exporá os membros adicionais e, em seguida, um Adaptador fará com que o registrador de arquivos pareça um registrador de console ocultando o novo material, para que apenas o objeto que agrupa tudo isso saiba a diferença.

    Da mesma forma, quando uma classe precisa adicionar uma dependência de uma interface semelhante à existente, para evitar alterar o objeto (OCP), a resposta usual é implementar um padrão Composite / Strategy (uma classe que implementa a interface de dependência e consome várias outras). implementações dessa interface, com quantidades variáveis ​​de lógica, permitindo que a classe passe uma chamada para uma, algumas ou todas as implementações).

  • Muitas interfaces e ABCs - DIP exigem necessariamente que existam abstrações, e o ISP incentiva que elas tenham escopo limitado. Portanto, interfaces e classes básicas abstratas são a regra, e você precisará de muitas delas para cobrir a funcionalidade de dependência compartilhada da sua base de código. Embora o SOLID estrito necessite injetar tudo , é óbvio que você precisa criar em algum lugar; portanto, se um formulário da GUI for criado apenas como filho de um formulário pai, executando alguma ação no referido pai, não tenho escrúpulos em renovar o formulário filho do código diretamente dentro do pai. Normalmente, apenas faço desse código seu próprio método, portanto, se duas ações do mesmo formulário abrirem a janela, eu chamo o método
  • Muitos projetos - O objetivo de tudo isso é limitar o escopo da mudança. A mudança inclui a necessidade de recompilar (um exercício relativamente trivial, mas ainda importante em muitas operações críticas de processador e largura de banda, como implantar atualizações em um ambiente móvel). Se um arquivo em um projeto precisar ser reconstruído, todos os arquivos serão necessários. Isso significa que, se você colocar interfaces nas mesmas bibliotecas de suas implementações, estará perdendo o ponto; você terá que recompilar todos os usos se alterar uma implementação da interface, porque também recompilará a própria definição de interface, exigindo que os usos apontem para um novo local no binário resultante. Portanto, manter as interfaces separadas dos usos e implementações, embora as separe adicionalmente pela área geral de uso, é uma prática recomendada típica.
  • Muita atenção prestada à terminologia "Gang of Four" - Os padrões de design identificados no livro Design Patterns de 1994 enfatizam o design de código modular que o SOLID procura criar. O Princípio de Inversão de Dependência e o Princípio Aberto / Fechado, por exemplo, estão no centro da maioria dos padrões identificados nesse livro. Como tal, eu esperaria que uma loja que aderisse fortemente aos princípios do SOLID também adotasse a terminologia do livro do Gang of Four e nomeie as classes de acordo com sua função nesse sentido, como "AbcFactory", "XyzRepository", "DefToXyzAdapter "," A1Command "etc.
  • Um repositório genérico - de acordo com o ISP, o DIP e o SRP, como geralmente entendido, o repositório é quase onipresente no design do SOLID, pois permite que o consumo de código solicite classes de dados de maneira abstrata, sem a necessidade de conhecimento específico do mecanismo de recuperação / persistência e coloca o código que faz isso em um só lugar, em oposição ao padrão DAO (no qual, se você tivesse, por exemplo, uma classe de dados da Fatura, também teria um InvoiceDAO que produzisse objetos hidratados desse tipo, etc. todos os objetos / tabelas de dados na base de código / esquema).
  • Um contêiner de IoC - hesito em adicioná-lo, pois, na verdade, não uso uma estrutura de IoC para fazer a maioria da minha injeção de dependência. Torna-se rapidamente um antipadrão de Objeto Deus, jogando tudo para dentro do recipiente, sacudindo-o e liberando a dependência verticalmente hidratada necessária através de um método de fábrica injetado. Parece ótimo, até você perceber que a estrutura fica bastante monolítica e o projeto com as informações de registro, se "fluente", agora precisa saber tudo sobre tudo em sua solução. Essa é uma série de razões para mudar. Se não for fluente (registros atrasados ​​usando arquivos de configuração), uma parte importante do seu programa se baseia em "seqüências de caracteres mágicas", uma variedade totalmente diferente de worms.
KeithS
fonte
1
por que os votos negativos?
Keiths
Eu acho que essa é uma boa resposta. Em vez de ser semelhante a muitos posts sobre o que esses termos são, você listou exemplos e explicações que vão para mostrar o seu uso e valor
Crowie
10

Distraia-os com a discussão de Jon Skeet de como o 'O' no SOLID é "inútil e mal compreendido" e faça com que falem sobre a "variação protegida" de Alistair Cockburn e o "design de herança ou proibição de Josh Bloch".

Breve resumo do artigo de Skeet (apesar de eu não recomendar o nome dele sem ler a postagem original do blog!):

  • A maioria das pessoas não sabe o que significa "aberto" e "fechado" em "princípio aberto-fechado", mesmo que ache que sabe.
  • Interpretações comuns incluem:
    • que os módulos sempre devem ser estendidos por herança de implementação, ou
    • que o código fonte do módulo original nunca pode ser alterado.
  • A intenção subjacente do OCP, e a formulação original de Bertrand Meyer, são boas:
    • os módulos devem ter interfaces bem definidas (não necessariamente no sentido técnico de 'interface') das quais seus clientes possam confiar, mas
    • deve ser possível expandir o que eles podem fazer sem interromper essas interfaces.
  • Mas as palavras "aberto" e "fechado" apenas confundem a questão, mesmo que elas sejam um acrônimo agradável e pronunciável.

O OP perguntou: "Como eu poderia lidar melhor com essa pergunta?" Como engenheiro sênior que conduz uma entrevista, eu estaria incomensuravelmente mais interessado em um candidato que possa falar de maneira inteligente sobre os prós e contras de diferentes estilos de design de código do que alguém que possa recitar uma lista de tópicos.

Outra boa resposta seria: "Bem, isso depende de quão bem eles o entenderam. Se tudo o que sabem são as palavras-chave do SOLID, eu esperaria abuso de herança, uso excessivo de estruturas de injeção de dependência, um milhão de pequenas interfaces, nenhuma das quais refletem o vocabulário do domínio usado para se comunicar com o gerenciamento de produtos .... "

David Moles
fonte
6

Provavelmente, há várias maneiras pelas quais isso pode ser respondido com quantidades variáveis ​​de tempo. No entanto, acho que isso é mais parecido com "Você sabe o que o SOLID significa?" Portanto, responder a essa pergunta provavelmente se resume apenas a acertar os pontos e explicá-la em termos de um projeto.

Então, você espera ver o seguinte:

  • As classes têm uma única responsabilidade (por exemplo, uma classe de acesso a dados para clientes só obtém dados do cliente no banco de dados do cliente).
  • As classes são facilmente estendidas sem afetar o comportamento existente. Não preciso modificar propriedades ou outros métodos para adicionar funcionalidades adicionais.
  • Classes derivadas podem ser substituídas por classes base e as funções que usam essas classes base não precisam desembrulhar a classe base para o tipo mais específico para lidar com elas.
  • As interfaces são pequenas e fáceis de entender. Se uma classe faz uso de uma interface, não precisa depender de vários métodos para realizar uma tarefa.
  • O código é abstrato o suficiente para que a implementação de alto nível não dependa concretamente da implementação específica de baixo nível. Eu deveria poder alternar a implementação de baixo nível sem afetar o código de alto nível. Por exemplo, eu posso alternar minha camada de acesso a dados SQL para uma baseada em serviço da Web sem afetar o restante do meu aplicativo.
villecoder
fonte
4

Essa é uma pergunta excelente, embora eu ache que é uma pergunta difícil para uma entrevista.

Os princípios do SOLID realmente governam classes e interfaces e como eles se relacionam.

Esta questão é realmente uma que tem mais a ver com arquivos e não necessariamente com classes.

Uma breve observação ou resposta que eu daria é que geralmente você vê arquivos que contêm apenas uma interface, e geralmente a convenção é que eles começam com um I maiúsculo. Além disso, eu mencionaria que os arquivos não teriam código duplicado (especialmente dentro de um módulo, aplicativo ou biblioteca) e esse código seria compartilhado cuidadosamente através de certos limites entre módulos, aplicativos ou bibliotecas.

Robert Martin discute esse tópico no domínio do C ++ em Design de aplicativos C ++ orientados a objetos usando o método Booch (consulte as seções sobre Coesão, fechamento e reutilização) e no Código limpo .

J. Polfer
fonte
Os codificadores .NET IME geralmente seguem uma regra "1 classe por arquivo" e também espelham estruturas de pastas / namespace; o IDE do Visual Studio incentiva as duas práticas, e vários plugins como o ReSharper podem aplicá-las. Então, eu esperaria ver uma estrutura de projeto / arquivo espelhando a estrutura de classe / interface.
Keiths