SÓLIDO vs. Evitar abstrações prematuras

27

Entendo o que o SOLID deve realizar e o uso regularmente em situações em que a modularidade é importante e seus objetivos são claramente úteis. No entanto, duas coisas me impedem de aplicá-lo consistentemente na minha base de código:

  • Eu quero evitar abstrações prematuras. Na minha experiência, desenhar linhas de abstração sem casos de uso concretos (do tipo que existe agora ou no futuro previsível ) os leva a serem desenhados nos lugares errados. Quando tento modificar esse código, as linhas de abstração atrapalham, em vez de ajudar. Portanto, tenho a tendência de errar ao não desenhar nenhuma linha de abstração até ter uma boa idéia de onde elas seriam úteis.

  • Acho difícil justificar o aumento da modularidade por si só, se isso torna meu código mais detalhado, mais difícil de entender etc. e não elimina nenhuma duplicação. Acho que o código processual simples, com procedimentos bem acoplados ou de objeto de Deus, às vezes é mais fácil de entender do que o código ravioli muito bem fatorado, porque o fluxo é simples e linear. Também é muito mais fácil escrever.

Por outro lado, essa mentalidade geralmente leva a objetos de Deus. Geralmente refatoro-as de maneira conservadora, adicionando linhas de abstração claras apenas quando vejo padrões claros emergindo. O que há de errado com os objetos de Deus e com o código fortemente associado se você não precisar claramente de mais modularidade, não possui duplicação significativa e o código é legível?

EDIT: No que diz respeito aos princípios individuais do SOLID, eu quis enfatizar que a Substituição de Liskov é IMHO uma formalização do senso comum e deve ser aplicada em todos os lugares, pois as abstrações não fazem sentido se não forem. Além disso, todas as classes devem ter uma única responsabilidade em algum nível de abstração, embora possa ser um nível muito alto, com os detalhes da implementação compactados em uma enorme classe de 2.000 linhas. Basicamente, suas abstrações devem fazer sentido onde você escolhe abstrair. Os princípios que eu questiono nos casos em que a modularidade não é claramente útil são de abertura aberta, segregação de interface e especialmente inversão de dependência, já que se trata de modularidade, e não apenas abstrações que fazem sentido.

dsimcha
fonte
10
@ S.Lott: A palavra-chave existe "mais". Este é precisamente o tipo de coisa pela qual YAGNI foi inventado. Aumentar a modularidade ou diminuir o acoplamento simplesmente por si só é apenas uma programação de culto à carga.
Mason Wheeler
3
@ S.Lott: Isso deve ser óbvio a partir do contexto. Pelo que li, pelo menos, "mais do que existe atualmente no código em questão".
Mason Wheeler
3
@ S.Lott: Se você insiste em ser pedante, "mais" se refere a mais do que existe atualmente. A implicação é que algo, seja um código ou um design aproximado, já existe e você está pensando em como refatorá-lo.
Dsimcha 06/04
5
@ back2dos: Certo, mas sou cético em projetar algo para extensibilidade sem uma ideia clara de como é provável que seja estendido. Sem essas informações, suas linhas de abstração provavelmente acabarão nos lugares errados. Se você não sabe onde as linhas de abstração provavelmente serão úteis, sugiro que você escreva apenas o código mais conciso que funcione e seja legível, mesmo se você tiver alguns objetos de Deus lançados lá.
dsimcha
2
@dsimcha: Você tem sorte se os requisitos mudarem depois que você escreveu um código, porque geralmente eles mudam enquanto você faz isso. É por isso que as implementações de instantâneos não são adequadas para sobreviver a um ciclo de vida real do software. Além disso, o SOLID não exige que você abstraia cegamente. O contexto para uma abstração é definido através da inversão de dependência. Você reduz a dependência de cada módulo à abstração mais simples e clara de outros serviços nos quais se baseia. Não é arbitrário, artificial ou complexo, mas bem definido, plausível e simples.
Apr2

Respostas:

8

A seguir, são princípios simples que você pode aplicar para ajudá-lo a entender como equilibrar o design do seu sistema:

  • Princípio de responsabilidade única: o Sno SOLID. Você pode ter um objeto muito grande em termos de número de métodos ou quantidade de dados e ainda assim manter esse princípio. Tomemos, por exemplo, o objeto Ruby String. Esse objeto tem mais métodos do que você pode usar, mas ainda tem apenas uma responsabilidade: mantenha uma sequência de texto para o aplicativo. Assim que seu objeto começar a assumir uma nova responsabilidade, comece a pensar muito sobre isso. O problema de manutenção está se perguntando "onde eu esperaria encontrar esse código se tivesse problemas com ele mais tarde?"
  • A simplicidade conta: Albert Einstein disse: "Torne tudo o mais simples possível, mas não mais simples". Você realmente precisa desse novo método? Você consegue o que precisa com a funcionalidade existente? Se você realmente acha que precisa haver um novo método, você pode alterar um método existente para satisfazer tudo o que precisa? Em essência, refatorar à medida que você constrói coisas novas.

Em essência, você está tentando fazer o possível para não dar um tiro no pé quando chegar a hora de manter o software. Se seus objetos grandes são abstrações razoáveis, há poucas razões para dividi-los apenas porque alguém criou uma métrica que diz que uma classe não deve ter mais que X linhas / métodos / propriedades / etc. Se alguma coisa, essas são diretrizes e não regras rígidas e rápidas.

Berin Loritsch
fonte
3
Bom comentário. Como discutido em outras respostas, um problema com "responsabilidade única" é que a responsabilidade de uma classe pode ser descrita em vários níveis de abstração.
dsimcha
5

Eu acho que na maioria das vezes você respondeu sua própria pergunta. O SOLID é um conjunto de diretrizes sobre como refatorar seu código, quando os conceitos precisam subir níveis de abstração.

Como todas as outras disciplinas criativas, não há absolutos - apenas trocas. Quanto mais você o faz, mais fácil se torna decidir quando é suficiente para o seu domínio ou requisitos atuais.

Dito isto - abstrair é o cerne do desenvolvimento de software -, se não houver uma boa razão para não fazê-lo, a prática o fará melhor e você terá uma ideia das vantagens e desvantagens. Então favor, a menos que se torne prejudicial.

Stephen Bailey
fonte
5
I want to avoid premature abstraction.In my experience drawing abstraction lines without concrete use cases... 

Isso está parcialmente certo e parcialmente errado.

Errado
Este não é um motivo para impedir a aplicação dos princípios OO / SOLID. Apenas impede a aplicação muito cedo.

Qual é...

Direito
Não refatorar o código até que seja refatorável; quando os requisitos estiverem completos. Ou, quando os "casos de uso" estiverem todos lá, como você diz.

I find simple, tightly coupled procedural or God object code is sometimes easier to understand than very well-factored...

Ao trabalhar sozinho ou em um silo
O problema de não fazer OO não é imediatamente óbvio. Depois de escrever a primeira versão de um programa:

Code is fresh in your head
Code is familiar
Code is easy to mentally compartmentalize
You wrote it

3/4 dessas coisas boas morrem rapidamente em pouco tempo.

Finalmente, você reconhece que possui objetos de Deus (objetos com muitas funções) e reconhece funções individuais. Encapsular. Coloque-os em pastas. Use interfaces de vez em quando. Faça um favor a si mesmo para manutenção a longo prazo. Especialmente porque os métodos não-refatorados tendem a inchar infinitamente e se tornar métodos de Deus.

Em uma equipe
Os problemas em não fazer OO são imediatamente perceptíveis. Um bom código OO é pelo menos um pouco auto-documentável e legível. Especialmente quando combinado com um bom código verde. Além disso, a falta de estrutura dificulta a separação de tarefas e integração muito mais complexa.

P.Brian.Mackey
fonte
Direita. Reconheço vagamente que meus objetos fazem mais de uma coisa, mas não reconheço de maneira mais concreta como separá-los de maneira útil para que as linhas de abstração estejam nos lugares certos quando a manutenção precisar ser feita. Parece uma perda de tempo refatorar se o programador de manutenção provavelmente terá que fazer muito mais refatoração para colocar as linhas de abstração no lugar certo.
dsimcha
Encapsulamento e abstração são dois conceitos diferentes. Encapsulamento é um conceito básico de OO que basicamente significa que você tem classes. As aulas oferecem modularidade e legibilidade por si mesmas. Isso é útil.
precisa saber é o seguinte
A abstração pode diminuir a manutenção quando aplicada corretamente. Se aplicado incorretamente, pode aumentar a manutenção. Saber quando e onde traçar as linhas requer uma sólida compreensão do negócio, da estrutura e do cliente, além dos aspectos técnicos básicos de como "aplicar um padrão de fábrica" ​​em si.
precisa saber é o seguinte
3

Não há nada errado com objetos grandes e códigos fortemente acoplados quando eles são apropriados e quando nada de melhor engenharia é necessário . Esta é apenas outra manifestação de uma regra prática que se transforma em dogma.

O acoplamento frouxo e objetos pequenos e simples tendem a fornecer benefícios específicos em muitos casos comuns; portanto, é uma boa ideia usá-los em geral. O problema está nas pessoas que não entendem a lógica por trás dos princípios tentando cegamente aplicá-los universalmente, mesmo onde eles não se aplicam.

Mason Wheeler
fonte
1
+1. Ainda precisa de uma boa definição do que "apropriado" significa.
jprete
2
@ jprete: Por quê? Tentar aplicar definições absolutas faz parte da causa de confusão conceitual como esta. Há muita habilidade envolvida na criação de um bom software. O que é realmente necessário A IMO é experiência e bom senso.
Mason Wheeler
4
Bem, sim, mas uma definição ampla de algum tipo ainda é útil.
jprete
1
Ter uma definição de apropriado seria útil, mas esse é o ponto. É difícil definir uma frase de efeito porque é aplicável apenas caso a caso. Se você faz a escolha apropriada em cada caso, depende muito da experiência. Se você não consegue articular por que sua solução monolítica de alta coesão é melhor do que uma solução altamente modular e de baixa coesão, você "provavelmente" seria melhor se fosse modular e de baixa coesão. É sempre mais fácil refatorar.
31411 Ian
1
Prefiro alguém cegamente escrever código desacoplado em vez de cegamente escrever código espaguete :)
Boris Yankov
2

Eu costumo abordar isso mais do ponto de vista Você não vai precisar. Esta postagem se concentrará em um item em particular: herança.

No meu design inicial, crio uma hierarquia de coisas que sei que haverá duas ou mais delas. Provavelmente, eles precisarão muito do mesmo código, por isso vale a pena projetá-lo desde o início. Depois que o código inicial está em vigor e preciso adicionar mais recursos / funcionalidades, olho para o que tenho e penso: "Alguma coisa que eu já implementei a mesma ou semelhante função?" Nesse caso, é mais provável que uma nova abstração implore para ser lançada.

Um bom exemplo disso é uma estrutura MVC. Começando do zero, você cria uma página grande com um código por trás. Mas então você deseja adicionar outra página. No entanto, o único código por trás já implementa grande parte da lógica que a nova página exige. Então, você abstrai uma Controllerclasse / interface que implementa a lógica específica dessa página, deixando as coisas comuns no código "deus" original.

Michael K
fonte
1

Se você sabe como desenvolvedor quando NÃO aplicar os princípios por causa do futuro do código, é bom para você.

Espero que o SOLID permaneça no fundo de sua mente para saber quando as coisas precisam ser abstraídas para evitar a complexidade e melhorar a qualidade do código, se começar a se degradar nos valores que você declarou.

Mais importante ainda, considere que a maioria dos desenvolvedores está fazendo o trabalho diário e não se importa tanto com você. Se você inicialmente definiu métodos de execução longa, quais são os outros desenvolvedores que entram no código quando pensam em mantê-lo ou estendê-lo? ... Sim, você adivinhou, MAIOR funções, maiores classes de execução e MAIS objetos de deus, e eles não têm os princípios em mente para poder capturar o código crescente e encapsular corretamente. Seu código "legível" agora se torna uma bagunça podre.

Portanto, você pode argumentar que um desenvolvedor ruim fará isso independentemente, mas pelo menos você terá uma vantagem melhor se as coisas forem mantidas simples e bem organizadas desde o início.

Não me importo particularmente com um "Mapa de Registro" global ou um "Objeto Divino" se eles forem simples. Realmente depende do design do sistema, às vezes você pode se safar e permanecer simples, às vezes não.

Também não devemos esquecer que grandes funções e objetos de Deus podem ser extremamente difíceis de testar. Se você deseja permanecer ágil e se sentir "seguro" ao refazer e alterar o código, precisará de testes. É difícil escrever testes quando funções ou objetos fazem muitas coisas.

Martin Blore
fonte
1
Basicamente, uma boa resposta. Minha única queixa é que, se algum desenvolvedor de manutenção aparecer e bagunçar, a culpa é dele por não refatorar, a menos que tais mudanças pareçam prováveis ​​o suficiente na previsão para justificar o planejamento para eles ou o código foi tão mal documentado / testado / etc . que a refatoração teria sido uma forma de insanidade.
dsimcha
Isso é verdade, dsimcha. É que eu pensei em um sistema em que um desenvolvedor cria uma pasta "RequestHandlers" e cada classe nessa pasta é um manipulador de solicitações; depois, o próximo desenvolvedor que aparece e pensa: "Preciso colocar um novo manipulador de solicitações", a infraestrutura atual quase o obriga a seguir a convenção. Eu acho que é isso que eu estava tentando dizer. A criação de uma convenção óbvia desde o início pode orientar até os desenvolvedores mais inexperientes a continuar o padrão.
Martin Blore
1

O problema com um objeto divino é que você geralmente pode dividi-lo de maneira lucrativa em pedaços. Por definição, faz mais de uma coisa. Todo o objetivo de dividi-lo em classes menores é para que você possa ter uma classe que faça uma coisa bem (e também não deve esticar a definição de "uma coisa"). Isso significa que, para qualquer classe, depois de saber a única coisa que ela deve fazer, você poderá lê-la com bastante facilidade e dizer se está fazendo ou não essa coisa corretamente.

Eu acho que é essa coisa a como tendo muito modularidade e muita flexibilidade, mas que é mais de um problema de excesso de desenho e excesso de engenharia, onde você está contabilização de requisitos que você nem sequer sabem o cliente quer. Muitas idéias de design são orientadas para facilitar o controle de alterações na forma como o código é executado, mas se ninguém espera a alteração, é inútil incluir a flexibilidade.

jprete
fonte
Pode ser difícil definir "mais de uma coisa", dependendo do nível de abstração em que você está trabalhando. Pode-se argumentar que qualquer método com duas ou mais linhas de código está fazendo mais de uma coisa. No extremo oposto do espectro, algo complexo como um mecanismo de script precisará de muitos métodos, todos eles fazendo "coisas" individuais que não estão diretamente relacionadas entre si, mas cada uma delas representa uma parte importante da "meta- coisa "que está fazendo com que seus scripts sejam executados corretamente, e há tantas coisas que podem ser feitas sem interromper o mecanismo de script.
Mason Wheeler
Há definitivamente um espectro de níveis conceituais, mas pode escolher um ponto sobre ele: o tamanho de uma "classe que faz uma coisa" deve ser tal que você pode entender quase imediatamente a implementação. Ou seja, os detalhes da implementação cabem na sua cabeça. Se ficar maior, você não poderá entender toda a turma de uma só vez. Se for menor, você está abstraindo demais. Mas, como você disse em outro comentário, há muito julgamento e experiência envolvidos.
jprete
IMHO, o melhor nível de abstração para se pensar é o nível que você espera que os consumidores do objeto usem. Digamos que você tenha um objeto chamado Queryerque otimiza as consultas ao banco de dados, consulta o RDBMS e analisa os resultados em objetos a serem retornados. Se houver apenas uma maneira de fazer isso no seu aplicativo e Queryerfor hermeticamente selado e encapsular dessa maneira, isso fará apenas uma coisa. Se houver várias maneiras de fazer isso e alguém puder se importar com os detalhes de uma parte, isso fará várias coisas e você poderá dividir.
perfil completo de Dsimcha
1

Eu uso uma diretriz mais simples: se você pode escrever testes de unidade e não possui duplicação de código, é abstrato o suficiente. Além disso, você está em uma boa posição para refatoração posterior.

Além disso, você deve manter o SOLID em mente, mas mais como orientação do que como regra.

Jeffrey Faust
fonte
0

O que há de errado com os objetos de Deus e com o código fortemente associado se você não precisar claramente de mais modularidade, não possui duplicação significativa e o código é legível?

Se o aplicativo for pequeno o suficiente, tudo poderá ser mantido. Em aplicações maiores, os objetos de Deus rapidamente se tornam um problema. Eventualmente, implementar um novo recurso requer a modificação de 17 objetos de Deus. As pessoas não se saem muito bem seguindo os procedimentos de 17 etapas. Os objetos de Deus estão sendo constantemente modificados por vários desenvolvedores, e essas mudanças precisam ser mescladas repetidamente. Você não quer ir para lá.

Kevin Cline
fonte
0

Partilho suas preocupações sobre abstração excessiva e inadequada, mas não estou necessariamente tão preocupado com abstração prematura.

Isso provavelmente parece uma contradição, mas é improvável que o uso de uma abstração cause um problema, desde que você não o comprometa muito cedo - ou seja, desde que você esteja disposto e seja capaz de refatorar, se necessário.

O que isso implica é algum grau de prototipagem, experimentação e retorno no código.

Dito isto, não há uma regra determinística simples a seguir que resolva todos os problemas. A experiência conta muito, mas você precisa adquiri-la cometendo erros. E sempre há mais erros para cometer que os erros anteriores não o tenham preparado.

Mesmo assim, trate os princípios do livro didático como ponto de partida e aprenda muito a programar e ver como esses princípios funcionam. Se fosse possível fornecer diretrizes melhores, mais precisas e confiáveis ​​do que coisas como o SOLID, alguém provavelmente o faria agora - e mesmo que o tivesse, esses princípios aprimorados ainda seriam imperfeitos e as pessoas perguntariam sobre as limitações desses .

Bom trabalho também - se alguém pudesse fornecer um algoritmo claro e determinístico para projetar e codificar programas, haveria apenas um último programa para humanos escreverem - o programa que escreveria automaticamente todos os programas futuros sem intervenção humana.

Steve314
fonte
0

Desenhar as linhas de abstração apropriadas é algo que se extrai da experiência. Você cometerá erros ao fazê-lo, o que é inegável.

O SOLID é uma forma concentrada dessa experiência que é entregue a você por pessoas que adquiriram essa experiência da maneira mais difícil. Você acertou em cheio quando diz que se abstém de criar abstração antes de perceber a necessidade. Bem, a experiência que o SOLID fornece ajuda a resolver problemas que você nunca existiu . Mas confie em mim ... existem muito reais.

O SOLID é sobre modularidade, modularidade é sobre manutenção e manutenção é sobre permitir que você mantenha um cronograma de trabalho humanizado quando o software chegar à produção e o cliente começar a perceber os erros que você não possui.

O maior benefício da modularidade é a testabilidade. Quanto mais modular o seu sistema, mais fácil é testar e mais rápido você pode construir bases sólidas para lidar com os aspectos mais difíceis do seu problema. Portanto, seu problema pode não exigir essa modularidade, mas apenas o fato de permitir que você crie códigos de teste melhores com mais rapidez é um acéfalo para lutar pela modularidade.

Ser ágil é tentar alcançar o delicado equilíbrio entre o envio rápido e o fornecimento de boa qualidade. Agile não significa que devemos cortar custos; de fato, os projetos ágeis mais bem-sucedidos com os quais me envolvi são os que prestaram muita atenção a essas diretrizes.

Newtopian
fonte
0

Um ponto importante que parece ter sido esquecido é que o SOLID é praticado junto com o TDD. O TDD tende a destacar o que é "apropriado" em seu contexto específico e ajuda a aliviar muitos dos equívocos que parecem estar nos conselhos.

delitescere
fonte
1
Por favor, considere expandir sua resposta. Como está, você está reunindo dois conceitos ortogonais e não está claro como o resultado aborda as preocupações do OP.