La sua preoccupazione era che il gran numero di classi si traducesse in un incubo di manutenzione. La mia opinione era che avrebbe avuto esattamente l'effetto opposto.
Sono assolutamente dalla parte del tuo amico, ma potrebbe essere una questione dei nostri domini e dei tipi di problemi e progetti che affrontiamo e soprattutto quali tipi di cose potrebbero richiedere cambiamenti in futuro. Problemi diversi, soluzioni diverse. Non credo nel giusto o nel male, solo i programmatori cercano di trovare il modo migliore per risolvere al meglio i loro particolari problemi di progettazione. Lavoro in VFX che non è molto diverso dai motori di gioco.
Ma il problema per me con cui ho lottato in quella che potrebbe almeno essere definita un po 'più di un'architettura conforme a SOLID (era basata su COM), potrebbe essere grossolanamente ridotta a "troppe classi" o "troppe funzioni" come il tuo amico potrebbe descrivere. Direi in particolare "troppe interazioni, troppi posti che potrebbero comportarsi in modo anomalo, troppi posti che potrebbero causare effetti collaterali, troppi posti che potrebbero dover cambiare e troppi posti che potrebbero non fare ciò che pensiamo che facciano ".
Avevamo una manciata di interfacce astratte (e pure) implementate da un carico di sottotipi, in questo modo (fatto questo diagramma nel contesto di parlare dei vantaggi di ECS, ignorando il commento in basso a sinistra):
Laddove un'interfaccia di movimento o un'interfaccia di nodo scena potrebbe essere implementata da centinaia di sottotipi: luci, telecamere, mesh, solutori di fisica, shader, trame, ossa, forme primitive, curve, ecc. Ecc. (E c'erano spesso più tipi di ciascuno ). E l'ultimo problema era proprio che quei progetti non erano così stabili. Avevamo esigenze mutevoli e talvolta le interfacce stesse dovevano cambiare, e quando vuoi cambiare un'interfaccia astratta implementata da 200 sottotipi, questo è un cambiamento estremamente costoso. Abbiamo iniziato a mitigarlo utilizzando classi di base astratte tra le quali ridurre i costi di tali modifiche di progettazione, ma erano comunque costose.
Quindi, in alternativa, ho iniziato a esplorare l'architettura del sistema entità-componente utilizzata piuttosto comunemente nel settore dei giochi. Ciò ha cambiato tutto per essere così:
E wow! Questa era una differenza in termini di manutenibilità. Le dipendenze non fluivano più verso le astrazioni , ma verso i dati (componenti). E almeno nel mio caso, i dati erano molto più stabili e più facili da ottenere in termini di progettazione in anticipo nonostante i requisiti mutevoli (anche se ciò che possiamo fare con gli stessi dati cambia costantemente con i requisiti in evoluzione).
Inoltre, poiché le entità in un ECS utilizzano la composizione anziché l'ereditarietà, in realtà non devono contenere funzionalità. Sono solo il "contenitore di componenti" analogico. Che ha reso così i analogiche 200 sottotipi che hanno implementato un moto un'interfaccia svolta in 200 entità casi (non tipi separati con codice separato) che memorizzano semplicemente un movimento componente (che non è altro che dati associati con il movimento). A PointLight
non è più una classe / sottotipo separato. Non è affatto una lezione. È un'istanza di un'entità che combina solo alcuni componenti (dati) relativi a dove si trova nello spazio (movimento) e alle proprietà specifiche delle luci puntiformi. L'unica funzionalità ad essi associata è all'interno dei sistemi, come ilRenderSystem
, che cerca componenti leggeri nella scena per determinare come renderizzare la scena.
Con la modifica dei requisiti nell'ambito dell'approccio ECS, spesso c'era solo la necessità di cambiare uno o due sistemi che operano su quei dati o semplicemente introdurre un nuovo sistema sul lato o introdurre un nuovo componente se fossero necessari nuovi dati.
Quindi, almeno per il mio dominio, e sono quasi certo che non lo sia per tutti, questo ha reso le cose molto più facili perché le dipendenze scorrevano verso la stabilità (cose che non avevano bisogno di cambiare spesso). Questo non era il caso dell'architettura COM quando le dipendenze scorrevano uniformemente verso le astrazioni. Nel mio caso è molto più facile capire quali dati sono necessari per il movimento in anticipo piuttosto che tutte le possibili cose che potresti fare con esso, che spesso cambia un po 'nel corso dei mesi o degli anni quando arrivano nuovi requisiti.
Ci sono casi in OOP in cui alcuni o tutti i principi SOLID non si prestano a pulire il codice?
Bene, codice pulito non posso dire dal momento che alcune persone identificano il codice pulito con SOLID, ma sicuramente ci sono alcuni casi in cui separare i dati dalla funzionalità come fa l'ECS e reindirizzare le dipendenze dalle astrazioni verso i dati può sicuramente rendere le cose molto più facili da cambiare, per ovvi motivi di accoppiamento, se i dati saranno molto più stabili delle astrazioni. Naturalmente le dipendenze dai dati possono rendere difficile il mantenimento degli invarianti, ma ECS tende a mitigarlo al minimo con l'organizzazione di sistema che minimizza il numero di sistemi che accedono a un dato tipo di componente.
Non è necessariamente che le dipendenze dovrebbero fluire verso le astrazioni come suggerirebbe DIP; le dipendenze dovrebbero fluire verso cose che difficilmente avranno bisogno di cambiamenti futuri. Ciò può essere o non essere astrazioni in tutti i casi (certamente non era nel mio).
- Sì, esistono principi di progettazione OOP che sono parzialmente in conflitto con SOLID
- Sì, esistono principi di progettazione OOP che sono completamente in conflitto con SOLID.
Non sono sicuro che l'ECS sia davvero un sapore di OOP. Alcune persone lo definiscono in questo modo, ma lo vedo molto diverso intrinsecamente dalle caratteristiche di accoppiamento e dalla separazione dei dati (componenti) dalla funzionalità (sistemi) e dalla mancanza di incapsulamento dei dati. Se dovesse essere considerato una forma di OOP, penso che sia molto in conflitto con SOLID (almeno le idee più rigorose di SRP, open / closed, sostituzione di liskov e DIP). Ma spero che questo sia un esempio ragionevole di un caso e dominio in cui gli aspetti più fondamentali di SOLID, almeno come le persone generalmente li interpreterebbero in un contesto OOP più riconoscibile, potrebbero non essere così applicabili.
Classi per adolescenti
Stavo spiegando l'architettura di uno dei miei giochi che, con sorpresa del mio amico, conteneva molte piccole classi e diversi livelli di astrazione. Ho sostenuto che questo è stato il risultato del mio concentrarsi sul dare a tutto una singola responsabilità e anche per allentare l'accoppiamento tra i componenti.
L'ECS ha sfidato e cambiato molto le mie opinioni. Come te, pensavo che l'idea stessa di manutenibilità abbia l'implementazione più semplice per le cose possibili, il che implica molte cose e, inoltre, molte cose interdipendenti (anche se le interdipendenze sono tra le astrazioni). Ha più senso se si esegue lo zoom in avanti su una sola classe o funzione per vedere l'implementazione più semplice e semplice, e se non ne vediamo una, rifattorizzarla e magari anche scomporla ulteriormente. Ma può essere facile perdere ciò che sta succedendo con il mondo esterno di conseguenza, perché ogni volta che dividi qualcosa di relativamente complesso in 2 o più cose, quelle 2 o più cose devono inevitabilmente interagire * (vedi sotto) in alcune modo, o qualcosa al di fuori deve interagire con tutti loro.
In questi giorni trovo che ci sia un atto di equilibrio tra la semplicità di qualcosa e quante cose ci sono e quanta interazione è richiesta. I sistemi in un ECS tendono ad essere piuttosto pesanti con implementazioni non banali per operare sui dati, come PhysicsSystem
o RenderSystem
o GuiLayoutSystem
. Tuttavia, il fatto che un prodotto complesso abbia bisogno di così pochi di essi tende a facilitare il passo indietro e ragionare sul comportamento generale dell'intera base di codice. C'è qualcosa lì che potrebbe suggerire che potrebbe non essere una cattiva idea appoggiarsi al lato di un numero inferiore di classi più voluminose (ancora eseguendo una responsabilità discutibilmente singolare), se ciò significa meno classi da mantenere e ragionare, e meno interazioni durante il sistema.
interazioni
Dico "interazioni" piuttosto che "accoppiamento" (sebbene ridurre le interazioni implica ridurre entrambi), poiché è possibile utilizzare le astrazioni per disaccoppiare due oggetti concreti, ma si parlano comunque. Potrebbero comunque causare effetti collaterali nel processo di questa comunicazione indiretta. E spesso trovo che la mia capacità di ragionare sulla correttezza di un sistema sia correlata a queste "interazioni" più che all '"accoppiamento". Ridurre al minimo le interazioni tende a rendere le cose molto più facili per me ragionare su tutto da una prospettiva a volo d'uccello. Ciò significa che le cose non parlano affatto tra loro, e da quel senso, l'ECS tende anche a minimizzare davvero le "interazioni", e non solo l'accoppiamento, con il minimo dei minimi (almeno non ho '
Detto questo, questo potrebbe essere almeno in parte io e le mie debolezze personali. Ho trovato il più grande impedimento per me nel creare sistemi di dimensioni enormi, e ancora con fiducia ragionare su di essi, navigare attraverso di essi e sentirmi come se potessi apportare qualsiasi potenziale cambiamento desiderato ovunque in modo prevedibile, è la gestione dello stato e delle risorse insieme a effetti collaterali. È il più grande ostacolo che inizia a sorgere mentre vado da decine di migliaia di LOC a centinaia di migliaia di LOC a milioni di LOC, anche per il codice che ho creato completamente da solo. Se qualcosa mi rallenta in una scansione sopra ogni altra cosa, è questo senso che non riesco più a capire cosa sta succedendo in termini di stato dell'applicazione, dati, effetti collaterali. E' Non è il tempo robotico che richiede per fare un cambiamento che mi rallenta tanto quanto l'incapacità di comprendere gli impatti completi del cambiamento se il sistema cresce oltre la capacità della mia mente di ragionarci. E ridurre le interazioni è stato, per me, il modo più efficace per consentire al prodotto di crescere molto più grande con molte più funzionalità senza che io venissi personalmente sopraffatto da queste cose, poiché ridurre le interazioni al minimo riduce anche il numero di posti che possono anche possibilmente cambiare lo stato dell'applicazione e causare effetti collaterali sostanzialmente.
Può trasformare qualcosa del genere (in cui tutto nel diagramma ha funzionalità e ovviamente uno scenario del mondo reale avrebbe molti, molte volte il numero di oggetti, e questo è un diagramma di "interazione", non di accoppiamento, come accoppiamento uno avrebbe astrazioni tra):
... a questo dove solo i sistemi hanno funzionalità (i componenti blu ora sono solo dati, e ora questo è uno schema di accoppiamento):
E ci sono pensieri emergenti su tutto questo e forse un modo per inquadrare alcuni di questi benefici in un contesto OOP più conforme che è più compatibile con SOLID, ma non ho ancora trovato i disegni e le parole, e lo trovo difficile dal momento che la terminologia ero abituato a gettare tutto in relazione direttamente con OOP. Continuo a cercare di capirlo leggendo le risposte delle persone qui e anche facendo del mio meglio per formulare le mie, ma ci sono alcune cose molto interessanti sulla natura di un ECS che non sono riuscito a mettere perfettamente il dito su quello potrebbe essere più ampiamente applicabile anche alle architetture che non lo utilizzano. Spero anche che questa risposta non venga fuori come una promozione ECS! Lo trovo molto interessante dal momento che la progettazione di un ECS ha davvero cambiato drasticamente i miei pensieri,