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.
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.
Existem casos no OOP em que alguns ou todos os princípios do SOLID não se prestam à limpeza de código?
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).
- 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 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
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.
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
ou RenderSystem
ou GuiLayoutSystem
. 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,