Recentemente, tive uma discussão com um amigo meu sobre OOP no desenvolvimento de videogames.
Eu estava explicando a arquitetura de um dos meus jogos que, para surpresa do meu amigo, continha muitas classes pequenas e várias camadas de abstração. Argumentei que esse foi o resultado de me concentrar em dar a tudo uma única responsabilidade e também em afrouxar o acoplamento entre os componentes.
Sua preocupação era que o grande número de classes se traduzisse em um pesadelo de manutenção. Na minha opinião, isso teria o efeito exatamente oposto. Passamos a discutir isso pelo que pareceu séculos, finalmente concordando em discordar, dizendo que talvez houvesse casos em que os princípios do SOLID e a POO adequada não se misturassem bem.
Até a entrada da Wikipedia sobre os princípios do SOLID afirma que são diretrizes que ajudam a escrever código sustentável e que fazem parte de uma estratégia geral de programação ágil e adaptável.
Então, minha pergunta é:
Existem casos no OOP em que alguns ou todos os princípios do SOLID não se prestam à limpeza de código?
Posso imaginar imediatamente que o Princípio da Substituição de Liskov poderia entrar em conflito com outro sabor de herança segura. Ou seja, se alguém idealizou outro padrão útil implementado por meio da herança, é bem possível que o LSP esteja em conflito direto com ele.
Existem outros? Talvez certos tipos de projetos ou determinadas plataformas de destino funcionem melhor com uma abordagem menos SOLID?
Editar:
Gostaria apenas de especificar que não estou perguntando como melhorar meu código;) A única razão pela qual mencionei um projeto nesta pergunta foi dar um pequeno contexto. Minha pergunta é sobre OOP e princípios de design em geral .
Se você está curioso sobre o meu projeto, veja isso .
Edição 2:
Imaginei que essa pergunta seria respondida de uma de três maneiras:
- Sim, existem princípios de projeto OOP que conflitam parcialmente com o SOLID
- Sim, existem princípios de projeto OOP que conflitam completamente com o SOLID
- Não, o SOLID é o joelho da abelha e a POO será sempre melhor com ela. Mas, como em tudo, não é uma panacéia. Beba com responsabilidade.
As opções 1 e 2 provavelmente teriam gerado respostas longas e interessantes. A opção 3, por outro lado, seria uma resposta curta, desinteressante, mas reconfortante.
Parece que estamos convergindo para a opção 3.
fonte
Respostas:
Em geral, não. A história mostrou que todos os princípios do SOLID contribuem amplamente para aumentar a dissociação, o que, por sua vez, demonstrou aumentar a flexibilidade no código e, portanto, sua capacidade de acomodar as mudanças, além de facilitar o raciocínio, o teste e a reutilização. em resumo, torne seu código mais limpo.
Agora, pode haver casos em que os princípios do SOLID colidem com DRY (não se repita), KISS (mantenha a simplicidade estúpida) ou outros princípios de bom design de OO. E, é claro, eles podem colidir com a realidade dos requisitos, as limitações humanas, as limitações de nossas linguagens de programação, outros obstáculos.
Em resumo, os princípios do SOLID sempre se prestam a código limpo, mas em alguns cenários se prestam menos do que alternativas conflitantes. Eles são sempre bons, mas às vezes outras coisas são mais boas.
fonte
Count
easImmutable
, e propriedades comogetAbilities
[cujo retorno indicará se coisas comoCount
serão "eficientes"], seria possível ter um método estático que pega várias seqüências e as agrega para que elas se comportará como uma única sequência mais longa. Se essas habilidades estiverem presentes no tipo de sequência básica, mesmo que elas estejamAcho que tenho uma perspectiva incomum, pois trabalhei em finanças e jogos.
Muitos dos programadores que conheci nos jogos eram péssimos na Engenharia de Software - mas eles não precisavam de práticas como o SOLID. Tradicionalmente, eles colocam o jogo em uma caixa - e terminam.
Em finanças, descobri que os desenvolvedores podem ser muito desleixados e indisciplinados porque não precisam do desempenho que você faz nos jogos.
Ambas as instruções acima são naturalmente generalizações excessivas, mas, na verdade, em ambos os casos, o código limpo é essencial. Para uma otimização clara e fácil de entender, o código é essencial. Por uma questão de manutenção, você quer a mesma coisa.
Isso não quer dizer que o SOLID não esteja isento de críticas. Eles são chamados de princípios e, no entanto, devem ser seguidos como diretrizes? Então, quando exatamente devo segui-los? Quando devo quebrar as regras?
Você provavelmente pode olhar para dois trechos de código e dizer qual deles atende melhor ao SOLID. Porém, isoladamente, não há uma maneira objetiva de transformar o código em SOLID. É definitivamente a interpretação do desenvolvedor.
É um não sequitur dizer que "um grande número de classes pode levar a um pesadelo de manutenção". No entanto, se o SRP não for interpretado corretamente, você poderá facilmente acabar com esse problema.
Eu vi código com muitas camadas de praticamente nenhuma responsabilidade. Classes que não fazem nada além do estado de passagem de uma classe para outra. O problema clássico de muitas camadas de indireção.
O SRP, se abusado, pode resultar em falta de coesão no seu código. Você pode dizer isso ao tentar adicionar um recurso. Se você sempre precisar alterar vários locais ao mesmo tempo, seu código não terá coesão.
Aberto-fechado não deixa de ter seus críticos. Por exemplo, veja a revisão de Jon Skeet do princípio Aberto Fechado . Não vou reproduzir seus argumentos aqui.
Seu argumento sobre o LSP parece bastante hipotético e eu realmente não entendo o que você quer dizer com outra forma de herança segura?
O ISP parece duplicar um pouco o SRP. Certamente eles devem ser interpretados da mesma maneira que você nunca pode prever o que todos os clientes da sua interface farão. A interface coesa de hoje é o violador do ISP de amanhã. A única conclusão ilógica é não ter mais de um membro por interface.
DIP pode levar a código inutilizável. Eu já vi classes com um grande número de parâmetros em nome de DIP. Essas classes normalmente "atualizavam" suas partes compostas, mas, como nome da capacidade de teste, nunca devemos usar a nova palavra-chave novamente.
O SOLID por si só não é suficiente para você escrever um código limpo. Eu vi o livro de Robert C. Martin, " Código Limpo: Um Manual de Artesanato em Software Ágil ". O maior problema é que é fácil ler este livro e interpretá-lo como regras. Se você fizer isso, está perdendo o objetivo. Ele contém uma grande lista de odores, heurísticas e princípios - muitos mais do que apenas os cinco do SOLID. Todos esses "princípios" não são realmente princípios, mas diretrizes. O tio Bob os descreve como "cubículos" para conceitos. Eles são um ato de equilíbrio; seguir cegamente qualquer diretriz causará problemas.
fonte
Aqui está a minha opinião:
Embora os princípios do SOLID tenham como objetivo uma base de código não redundante e flexível, isso pode ser uma troca de legibilidade e manutenção se houver muitas classes e camadas.
É especialmente sobre o número de abstrações . Um grande número de classes pode ser bom se elas forem baseadas em poucas abstrações. Como não sabemos o tamanho e as especificidades do seu projeto, é difícil dizer, mas é um pouco preocupante que você mencione várias camadas, além de um grande número de classes.
Eu acho que os princípios do SOLID não discordam do OOP, mas ainda é possível escrever um código não limpo aderindo a eles.
fonte
Nos meus primeiros dias de desenvolvimento, comecei a escrever um Roguelike em C ++. Como eu queria aplicar a boa metodologia orientada a objetos que aprendi com minha formação, vi imediatamente os itens do jogo como objetos C ++.
Potion
,Swords
,Food
,Axe
,Javelins
Etc. todos derivados de um núcleo básicoItem
do código, nome, ícone, peso, esse tipo de coisa manipulação.Codifiquei a lógica do saco e enfrentei um problema. Posso colocar
Items
na bolsa, mas, se eu escolher uma, como sei se é umaPotion
ou umaSword
? Procurei na Internet como diminuir itens. Eu hackeei as opções do compilador para habilitar as informações de tempo de execução para saber quais são meusItem
tipos verdadeiros abstratos e comecei a mudar a lógica dos tipos para saber quais operações eu permitiria (por exemplo, evite beberSwords
e colocar dePotions
pé)Essencialmente, transformou o código em uma grande bola de lama meio copipada. À medida que o projeto prosseguia, comecei a entender qual era a falha no design. O que aconteceu é que tentei usar classes para representar valores. Minha hierarquia de classes não era uma abstração significativa. O que eu deveria ter feito é fazer
Item
implementar um conjunto de funções, tais comoEquip ()
,Apply ()
eThrow ()
, e fazer com que cada comportam com base em valores. Usando enums para representar os slots de equipamento. Usando enums para representar diferentes tipos de armas. Usando mais valores e menos classificação, porque minha subclassificação não tinha outro propósito senão preencher esses valores finais.Como você está enfrentando a multiplicação de objetos em uma camada de abstração, acho que meu insight pode ser valioso aqui. Se você parece ter muitos tipos de objetos, pode ser que você esteja confundindo o que deve ser tipos com o que devem ser valores.
fonte
Estou absolutamente do lado de seu amigo, mas pode ser uma questão de nossos domínios e os tipos de problemas e projetos que enfrentamos e, especialmente, que tipos de coisas provavelmente exigirão mudanças no futuro. Problemas diferentes, soluções diferentes. Não acredito no certo ou no errado, apenas nos programadores que tentam encontrar a melhor maneira de resolver seus problemas de design específicos. Eu trabalho em efeitos visuais, o que não é muito diferente dos mecanismos de jogo.
Mas o problema para mim com o qual lutei no que poderia ser chamado de arquitetura um pouco mais compatível com o SOLID (era baseada em COM), pode ser resumido a "muitas classes" ou "muitas funções", como seu amigo pode descrever. Eu diria especificamente: "muitas interações, muitos lugares que poderiam se comportar mal, muitos lugares que poderiam causar efeitos colaterais, muitos lugares que talvez precisassem mudar e muitos lugares que talvez não fizessem o que pensamos que eles fazem . "
Tínhamos um punhado de interfaces abstratas (e puras) implementadas por uma grande quantidade de subtipos, assim: (fizemos este diagrama no contexto de falar sobre os benefícios do ECS, desconsidere o comentário no canto inferior esquerdo):
Onde uma interface de movimento ou uma interface de nó de cena pode ser implementada por centenas de subtipos: luzes, câmeras, malhas, solucionadores de física, sombreadores, texturas, ossos, formas primitivas, curvas, etc. etc. (e geralmente havia vários tipos de cada ) E o problema final era realmente que esses projetos não eram tão estáveis. Tivemos mudanças nos requisitos e, às vezes, as próprias interfaces precisavam mudar, e quando você deseja alterar uma interface abstrata implementada por 200 subtipos, é uma mudança extremamente cara. Começamos a mitigar isso usando classes básicas abstratas entre as quais reduzimos os custos de tais alterações de design, mas elas ainda eram caras.
Então, alternativamente, comecei a explorar a arquitetura do sistema de componente de entidade usada com bastante frequência na indústria de jogos. Isso mudou tudo para ficar assim:
E uau! Essa foi uma diferença em termos de manutenção. As dependências não fluíram mais para abstrações , mas para dados (componentes). E no meu caso, pelo menos, os dados eram muito mais estáveis e mais fáceis de acertar em termos de design, apesar das mudanças nos requisitos (embora o que possamos fazer com os mesmos dados esteja constantemente mudando com os requisitos).
Também porque as entidades em um ECS usam composição em vez de herança, elas realmente não precisam conter funcionalidade. Eles são apenas o "recipiente de componentes" analógico. Com isso, os 200 subtipos analógicos que implementaram uma interface de movimento se transformam em 200 instâncias de entidade (não tipos separados com código separado) que simplesmente armazenam um componente de movimento (que não passa de dados associados ao movimento). A
PointLight
não é mais uma classe / subtipo separado. Não é uma aula. É uma instância de uma entidade que apenas combina alguns componentes (dados) relacionados a onde está no espaço (movimento) e as propriedades específicas das luzes pontuais. A única funcionalidade associada a eles está dentro dos sistemas, como oRenderSystem
, que procura componentes leves na cena para determinar como renderizar a cena.Com a alteração dos requisitos sob a abordagem do ECS, muitas vezes havia apenas a necessidade de alterar um ou dois sistemas operando com esses dados ou apenas introduzir um novo sistema ao lado ou introduzir um novo componente, se novos dados fossem necessários.
Portanto, para o meu domínio, pelo menos, e tenho quase certeza de que não é para todos, isso facilitou muito as coisas, porque as dependências estavam fluindo em direção à estabilidade (coisas que não precisavam mudar com frequência). Esse não era o caso na arquitetura COM quando as dependências estavam fluindo uniformemente em direção às abstrações. No meu caso, é muito mais fácil descobrir quais dados são necessários para o movimento antecipadamente, em vez de todas as coisas possíveis que você poderia fazer com eles, o que geralmente muda um pouco ao longo dos meses ou anos à medida que novos requisitos chegam.
Bem, código limpo, não posso dizer, já que algumas pessoas equiparam o código limpo ao SOLID, mas, definitivamente, existem alguns casos em que separar dados da funcionalidade como o ECS e redirecionar dependências das abstrações para os dados podem definitivamente facilitar muito as coisas. mudar, por razões óbvias de acoplamento, se os dados forem muito mais estáveis que as abstrações. É claro que dependências de dados podem dificultar a manutenção de invariantes, mas o ECS tende a atenuar isso ao mínimo com a organização do sistema, o que minimiza o número de sistemas que acessam qualquer tipo de componente.
Não é necessariamente que as dependências fluam para abstrações, como sugere o DIP; as dependências devem fluir para coisas que dificilmente precisarão de mudanças futuras. Isso pode ou não ser abstrações em todos os casos (certamente não estava no meu).
Não tenho certeza se o ECS é realmente um sabor de POO. Algumas pessoas o definem dessa maneira, mas eu o vejo como muito diferente inerentemente às características do acoplamento e à separação de dados (componentes) da funcionalidade (sistemas) e à falta de encapsulamento de dados. Se for considerado uma forma de POO, acho que está em conflito com o SOLID (pelo menos as idéias mais estritas de SRP, aberto / fechado, substituição de liskov e DIP). Mas espero que este seja um exemplo razoável de um caso e domínio em que os aspectos mais fundamentais do SOLID, pelo menos como as pessoas geralmente os interpretem em um contexto de POO mais reconhecível, possam não ser tão aplicáveis.
Teeny Classes
O ECS desafiou e mudou muito meus pontos de vista. Como você, eu pensava que a própria idéia de manutenção é ter a implementação mais simples para as coisas possíveis, o que implica muitas coisas e, além disso, muitas coisas interdependentes (mesmo que as interdependências sejam entre abstrações). Faz mais sentido se você estiver ampliando apenas uma classe ou função para querer ver a implementação mais direta e simples e, se não encontrarmos uma, refatorar e talvez até decompor ainda mais. Mas pode ser fácil perder o que está acontecendo com o mundo exterior como resultado, porque sempre que você divide algo relativamente complexo em 2 ou mais coisas, essas 2 ou mais coisas devem inevitavelmente interagir * (veja abaixo) entre si em alguns maneira, ou algo de fora tem que interagir com todos eles.
Hoje em dia, acho que há um ato de equilíbrio entre a simplicidade de algo e quantas coisas existem e quanta interação é necessária. Os sistemas em um ECS tendem a ser bastante pesados com implementações não triviais para operar nos dados, como
PhysicsSystem
ouRenderSystem
ouGuiLayoutSystem
. No entanto, o fato de um produto complexo precisar de poucos deles tende a facilitar a recuperação e o raciocínio sobre o comportamento geral de toda a base de código. Há algo nele que pode sugerir que pode não ser uma má idéia apoiar-se em menos classes mais volumosas (ainda executando uma responsabilidade indiscutivelmente singular), se isso significa menos classes para manter e raciocinar e menos interações ao longo o sistema.Interações
Digo "interações" em vez de "acoplamento" (embora reduzir interações implique em reduzir as duas), pois é possível usar abstrações para desacoplar dois objetos concretos, mas eles ainda conversam entre si. Eles ainda podem causar efeitos colaterais no processo dessa comunicação indireta. E, freqüentemente, acho que minha capacidade de raciocinar sobre a correção de um sistema está mais relacionada a essas "interações" do que a "acoplamento". Minimizar as interações tende a facilitar muito as coisas para eu raciocinar sobre tudo, do ponto de vista de um pássaro. Isso significa que as coisas não se falam e, nesse sentido, a ECS também tende a realmente minimizar as "interações", e não apenas o acoplamento, ao mínimo dos mínimos (pelo menos eu não
Dito isto, isso pode ser pelo menos parcialmente eu e minhas fraquezas pessoais. Eu encontrei o maior impedimento para eu criar sistemas de enorme escala, e ainda raciocinar com confiança sobre eles, navegar por eles e sentir que posso fazer as possíveis mudanças desejadas em qualquer lugar de uma maneira previsível, seja o gerenciamento de estado e recursos, juntamente com efeitos colaterais. É o maior obstáculo que começa a surgir à medida que passo de dezenas de milhares de LOC a centenas de milhares de LOC a milhões de LOC, mesmo para códigos criados por mim mesmo. Se algo vai me atrasar para um rastreamento acima de tudo, é nesse sentido que não consigo mais entender o que está acontecendo em termos de estado do aplicativo, dados e efeitos colaterais. Isto' não é o tempo robótico necessário para fazer uma mudança que me atrapalha tanto quanto a incapacidade de entender todos os impactos da mudança se o sistema crescer além da capacidade da minha mente de raciocinar sobre ela. E reduzir as interações tem sido, para mim, a maneira mais eficaz de permitir que o produto cresça muito mais, com muito mais recursos, sem que eu fique pessoalmente impressionado com essas coisas, pois reduzir as interações ao mínimo também reduz o número de locais que podem possivelmente altere o estado do aplicativo e cause efeitos colaterais substancialmente.
Pode virar algo assim (onde tudo no diagrama tem funcionalidade e, obviamente, um cenário do mundo real teria muitas, muitas vezes o número de objetos, e este é um diagrama de "interação", não um acoplamento, como um acoplamento um teria abstrações no meio):
... para isso em que apenas os sistemas têm funcionalidade (os componentes azuis agora são apenas dados e agora este é um diagrama de acoplamento):
E há pensamentos emergentes sobre tudo isso e talvez uma maneira de enquadrar alguns desses benefícios em um contexto de POO mais compatível e mais compatível com o SOLID, mas ainda não encontrei os desenhos e as palavras, e acho difícil desde a terminologia que eu estava acostumado a abordar todos relacionados diretamente ao POO. Eu continuo tentando descobrir isso lendo as respostas das pessoas aqui e também tentando o meu melhor para formular as minhas, mas há algumas coisas muito interessantes sobre a natureza de um ECS que eu não consegui identificar perfeitamente. pode ser mais amplamente aplicável, mesmo para arquiteturas que não o usam. Também espero que essa resposta não seja uma promoção da ECS! Eu acho isso muito interessante, já que projetar um ECS realmente mudou meus pensamentos drasticamente,
fonte
Os princípios do SOLID são bons, mas o KISS e o YAGNI têm prioridade em caso de conflito. O objetivo do SOLID é gerenciar a complexidade, mas, ao aplicar o próprio SOLID, o código fica mais complexo, mas o objetivo é derrotado. Por exemplo, se você tiver um programa pequeno com apenas algumas classes, a aplicação de DI, segregação de interface etc. poderá muito bem aumentar a complexidade geral do programa.
O princípio aberto / fechado é especialmente controverso. Basicamente, diz que você deve adicionar recursos a uma classe criando uma subclasse ou uma implementação separada da mesma interface, mas deixe a classe original intocada. Se você estiver mantendo uma biblioteca ou serviço usado por clientes não coordenados, isso faz sentido, pois você não deseja interromper o comportamento no qual o cliente existente pode confiar. No entanto, se você estiver modificando uma classe que é usada apenas em seu próprio aplicativo, muitas vezes seria muito mais simples e mais limpo simplesmente modificar a classe.
fonte
Em minha experiência:-
Classes grandes são exponencialmente mais difíceis de manter à medida que aumentam.
Classes pequenas são mais fáceis de manter e a dificuldade de manter uma coleção de classes pequenas aumenta aritmeticamente para o número de classes.
Às vezes, aulas grandes são inevitáveis; um grande problema às vezes precisa de uma aula grande, mas são muito mais difíceis de ler e entender. Para classes menores bem nomeadas, apenas o nome pode ser suficiente para a compreensão. Você nem precisa olhar o código, a menos que exista um problema muito específico com a classe.
fonte
Sempre acho difícil traçar a linha entre o 'S' do sólido e a (pode ser antiquada) necessidade de deixar um objeto ter todo o conhecimento de si mesmo.
Há 15 anos, trabalhei com desenvolvedores Deplhi que tiveram seu santo graal para ter aulas que continham tudo. Portanto, para uma hipoteca, você pode adicionar seguros e pagamentos, renda, empréstimos e informações fiscais, além de calcular impostos a pagar, impostos a deduzir e ele pode serializar-se, persistir em disco etc. etc.
E agora eu tenho o mesmo objeto dividido em várias classes menores, que têm a vantagem de poderem ser testadas em unidade muito mais fácil e melhor, mas não oferecem uma boa visão geral de todo o modelo de negócios.
E sim, eu sei que você não deseja vincular seus objetos ao banco de dados, mas pode injetar um repositório na grande classe de hipotecas para resolver isso.
Além disso, nem sempre é fácil encontrar bons nomes para as classes que fazem apenas uma pequena coisa.
Então, eu não tenho certeza se o 'S' sempre é uma boa ideia.
fonte