Principi solidi e struttura del codice


150

Durante un recente colloquio di lavoro, non ho potuto rispondere a una domanda su SOLID - oltre a fornire il significato di base dei vari principi. Mi infastidisce davvero. Ho fatto un paio di giorni per scavare e non ho ancora trovato un riassunto soddisfacente.

La domanda dell'intervista era:

Se dovessi guardare un progetto .Net che ti ho detto di seguire rigorosamente i principi SOLID, cosa ti aspetteresti di vedere in termini di progetto e struttura del codice?

Mi sono agitato un po ', non ho risposto alla domanda e poi sono esploso.

Come avrei potuto gestire meglio questa domanda?


3
Mi chiedevo cosa non fosse chiaro nella pagina wiki di SOLID
BЈовић

Particelle elementari astratte estensibili.
rwong,

Seguendo i principi solidi del design orientato agli oggetti, le tue classi tenderanno naturalmente ad essere piccole, ben ponderate e facilmente testabili. Fonte: docs.asp.net/it/latest/fundamentals/…
WhileTrueSleep

Risposte:


188

S = Principio di responsabilità singola

Quindi mi aspetto di vedere una struttura di cartelle / file ben organizzata e una gerarchia di oggetti. A ogni classe / pezzo di funzionalità dovrebbe essere assegnato il nome che la sua funzionalità è molto ovvia e dovrebbe contenere solo la logica per eseguire tale compito.

Se vedessi enormi classi manager con migliaia di righe di codice, ciò significherebbe che non è stata seguita la singola responsabilità.

O = Principio aperto / chiuso

Questa è fondamentalmente l'idea che nuove funzionalità dovrebbero essere aggiunte attraverso nuove classi che hanno un impatto minimo su / richiedono la modifica delle funzionalità esistenti.

Mi aspetterei di vedere un grande uso dell'ereditarietà degli oggetti, della sotto-digitazione, delle interfacce e delle classi astratte per separare il design di un pezzo di funzionalità dall'attuazione effettiva, consentendo agli altri di venire avanti e implementare altre versioni lungo il lato senza influire sul originale.

L = principio di sostituzione di Liskov

Ciò ha a che fare con la capacità di trattare i sottotipi come il loro tipo genitore. Questo viene fuori dalla scatola in C # se stai implementando una corretta gerarchia di oggetti ereditati.

Mi aspetterei di vedere il codice che tratta gli oggetti comuni come il loro tipo di base e i metodi di chiamata nelle classi base / astratte piuttosto che creare un'istanza e lavorare sui sottotipi stessi.

I = Principio di segregazione dell'interfaccia

Questo è simile a SRP. Fondamentalmente, si definiscono sottoinsiemi di funzionalità più piccoli come interfacce e si lavora con quelli per mantenere il sistema disaccoppiato (ad es. A FileManagerpotrebbe avere la singola responsabilità di gestire l'I / O dei file, ma ciò potrebbe implementare un IFileReadere IFileWriterche conteneva le definizioni del metodo specifico per la lettura e scrittura di file).

D = Principio di inversione di dipendenza.

Anche in questo caso si tratta di mantenere un sistema disaccoppiato. Forse saresti alla ricerca dell'uso di una libreria .NET Dependency Injection, utilizzata nella soluzione come Unityo Ninjecto in un sistema ServiceLocator come AutoFacServiceLocator.


36
Ho visto molte violazioni di LSP in C #, ogni volta che qualcuno decide che il suo particolare sottotipo è specializzato e quindi non è necessario implementare un pezzo dell'interfaccia e lancia invece un'eccezione su quel pezzo ... Questo è un approccio junior comune per risolvere il problema dell'implementazione e della progettazione dell'interfaccia incomprensibili
Jimmy Hoffa,

2
@JimmyHoffa Questo è uno dei motivi principali per cui insisto sull'uso dei contratti di codice; passare attraverso il processo di pensiero della progettazione dei contratti aiuta molte persone a uscire da quella cattiva abitudine.
Andy,

12
Non mi piace il "LSP viene fuori dalla scatola in C #" e equiparando DIP alla pratica di iniezione di dipendenza.
Euforico,

3
+1 ma inversione di dipendenza <> Iniezione di dipendenza. Giocano bene insieme, ma l'inversione di dipendenza è molto più di una semplice iniezione di dipendenza. Riferimento: DIP in the wild
Marjan Venema,

3
@Andy: ciò che aiuta anche sono i unit test definiti sulle interfacce su cui vengono testati tutti gli implementatori (qualsiasi classe che può / è istanziata).
Marjan Venema,

17

Molte piccole classi e interfacce con l'iniezione delle dipendenze ovunque. Probabilmente in un grande progetto useresti anche un framework IoC per aiutarti a costruire e gestire la vita di tutti quei piccoli oggetti. Vedi https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-looking-into

Si noti che un grande progetto .NET che segue rigorosamente i principi SOLID non significa necessariamente una buona base di codice con cui lavorare per tutti. A seconda di chi fosse l'intervistatore, potrebbe aver voluto che dimostri di aver capito cosa significa SOLID e / o di verificare come dogmaticamente segui i principi di progettazione.

Vedi, per essere SOLIDO, devi seguire:

S principio di responsabilità Ingle, in modo da avere molte piccole classi ciascuno di essi fare una cosa sola

O principio a penna chiusa, che in .NET viene solitamente implementato con l'iniezione di dipendenza, che richiede anche I e D di seguito ...

L principio di sostituzione iskov è probabilmente inmpossible di spiegare in C # con un one-liner. Fortunatamente ci sono altre domande che lo riguardano, ad esempio https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

I nterface segregazione Principio lavora in tandem con il principio aperto-chiuso. Se seguiti letteralmente significherebbe preferire un gran numero di interfacce molto piccole piuttosto che poche "grandi" interfacce

D ependency principio inversione classi di alto livello non dovrebbe dipendere classi di basso livello, sia dovrebbe dipendere astrazioni.


SRP non significa "fare solo una cosa".
Robert Harvey,

13

Alcune cose di base che mi aspetterei di vedere nella base di codice di un negozio che ha sposato SOLID nel loro lavoro quotidiano:

  • Molti piccoli file di codice - con una classe per file come best practice in .NET e il principio di responsabilità singola che incoraggia le strutture di classi modulari di piccole dimensioni, mi aspetto di vedere molti file contenenti ciascuno una classe piccola e focalizzata.
  • Moltissimi pattern Adapter e Composite - mi aspetterei l'uso di molti pattern Adapter (una classe che implementa un'interfaccia "passando" alla funzionalità di una diversa interfaccia) per semplificare il collegamento di una dipendenza sviluppata per uno scopo in un po ' luoghi diversi dove è necessaria anche la sua funzionalità. Gli aggiornamenti semplici come la sostituzione di un logger di console con un logger di file violeranno LSP / ISP / DIP se l'interfaccia viene aggiornata per esporre un mezzo per specificare il nome file da utilizzare; invece, la classe del registratore di file esporrà i membri aggiuntivi e quindi un adattatore farà apparire il registratore di file come un registratore di console nascondendo le nuove cose, quindi solo l'oggetto che fa scattare tutto questo insieme deve conoscere la differenza.

    Allo stesso modo, quando una classe deve aggiungere una dipendenza di un'interfaccia simile a quella esistente, per evitare di cambiare l'oggetto (OCP), la risposta abituale è implementare un modello composito / strategico (una classe che implementa l'interfaccia di dipendenza e ne consuma più altre implementazioni di tale interfaccia, con quantità variabili di logica che consentono alla classe di passare una chiamata a una, alcune o tutte le implementazioni).

  • Molte interfacce e ABC - DIP richiede necessariamente l'esistenza di astrazioni e ISP incoraggia queste a essere ristrette. Pertanto, le interfacce e le classi di base astratte sono la regola e ne occorreranno molte per coprire la funzionalità di dipendenza condivisa della base di codice. Mentre il SOLID rigoroso richiederebbe l'iniezione di tutto , è ovvio che devi creare da qualche parte, e quindi se un modulo GUI viene creato solo come figlio di un modulo genitore eseguendo un'azione su detto genitore, non ho scrupoli a rinnovare il modulo figlio dal codice direttamente all'interno del genitore. Solitamente faccio di quel codice il suo metodo, quindi se due azioni dello stesso modulo aprono la finestra, chiamo semplicemente il metodo.
  • Molti progetti - Il punto di tutto ciò è limitare la portata del cambiamento. Il cambiamento include la necessità di ricompilare (un esercizio relativamente banale, ma ancora importante in molte operazioni critiche per il processore e la larghezza di banda, come la distribuzione di aggiornamenti in un ambiente mobile). Se un file in un progetto deve essere ricostruito, tutti i file lo fanno. Ciò significa che se si posizionano le interfacce nelle stesse librerie delle loro implementazioni, il punto non viene considerato; dovrai ricompilare tutti gli usi se cambi un'implementazione dell'interfaccia perché ricompilerai anche la definizione dell'interfaccia stessa, richiedendo che gli usi puntino a una nuova posizione nel binario risultante. Pertanto, mantenendo le interfacce separate dagli usi e le implementazioni, pur separandole ulteriormente in base all'area di utilizzo generale, sono le migliori pratiche tipiche.
  • Molta attenzione prestata alla terminologia di "Gang of Four" - I modelli di progettazione identificati nel libro Design Patterns del 1994 enfatizzano il design del codice modulare di dimensioni ridotte che SOLID cerca di creare. Il principio di inversione di dipendenza e il principio di apertura / chiusura, ad esempio, sono al centro della maggior parte degli schemi identificati in quel libro. In quanto tale, mi aspetterei che un negozio che aderisse fortemente ai principi SOLID includesse anche la terminologia nel libro di Gang of Four e che denominasse le classi in base alla loro funzione in tal senso, come "AbcFactory", "XyzRepository", "DefToXyzAdapter "," A1Command "ecc.
  • Un repository generico - In linea con ISP, DIP e SRP come comunemente intesi, il repository è quasi onnipresente nella progettazione SOLID, in quanto consente al consumatore che consuma codice di richiedere classi di dati in modo astratto senza la necessità di una conoscenza specifica del meccanismo di recupero / persistenza, e mette il codice che lo fa in un posto rispetto al modello DAO (in cui se avessi, ad esempio, una classe di dati Invoice, avresti anche un InvoiceDAO che produceva oggetti idratati di quel tipo, e così via per tutti gli oggetti / tabelle di dati nella base di codice / schema).
  • Un contenitore IoC: esito ad aggiungere questo, poiché in realtà non utilizzo un framework IoC per eseguire la maggior parte della mia iniezione di dipendenza. Diventa rapidamente un modello anti-oggetto di Dio di gettare tutto nel contenitore, scuoterlo e riversare la dipendenza idratata verticalmente di cui hai bisogno attraverso un metodo di fabbrica iniettato. Sembra fantastico, finché non ti rendi conto che la struttura diventa piuttosto monolitica, e il progetto con le informazioni di registrazione, se "fluente", ora deve sapere tutto su tutto nella tua soluzione. Questo è un sacco di ragioni per cambiare. Se non è fluente (registrazioni in ritardo con i file di configurazione), un pezzo chiave del tuo programma si basa su "stringhe magiche", un tutto diverso può creare worm.

1
perché i downvotes?
KeithS,

Penso che questa sia una buona risposta. Invece di essere simili ai molti post sul blog su quali sono questi termini, hai elencato esempi e spiegazioni che mostrano il loro uso e valore
Crowie,

10

Distraili con la discussione di Jon Skeet su come "O" in SOLID sia "inutile e mal compreso" e inducili a parlare della "variante protetta" di Alistair Cockburn e del "progetto di eredità di Josh Bloch, o proibiscilo".

Breve riassunto dell'articolo di Skeet (anche se non consiglierei di lasciar perdere il suo nome senza leggere il post originale del blog!):

  • La maggior parte delle persone non sa cosa significhino "aperto" e "chiuso" in "principio aperto-chiuso", anche se pensano di sì.
  • Le interpretazioni comuni includono:
    • che i moduli dovrebbero essere sempre estesi tramite l'ereditarietà dell'implementazione, oppure
    • che il codice sorgente del modulo originale non può mai essere modificato.
  • L'intenzione di base di OCP, e la sua formulazione originale di Bertrand Meyer, va bene:
    • che i moduli dovrebbero avere interfacce ben definite (non necessariamente nel senso tecnico di "interfaccia") da cui i loro clienti possono dipendere, ma
    • dovrebbe essere possibile espandere ciò che possono fare senza interrompere quelle interfacce.
  • Ma le parole "aperto" e "chiuso" confondono il problema, anche se rendono un acronimo pronunciabile piacevole.

L'OP ha chiesto: "Come avrei potuto gestire meglio questa domanda?" Come ingegnere senior che conduce un'intervista, sarei incommensurabilmente più interessato a un candidato in grado di parlare in modo intelligente dei pro e dei contro di diversi stili di progettazione del codice rispetto a qualcuno che può scuotere un elenco di punti elenco.

Un'altra buona risposta sarebbe: "Beh, dipende da quanto bene l'hanno capito. Se tutto ciò che sanno sono le parole d'ordine SOLID, mi aspetterei abuso di eredità, uso eccessivo di framework di iniezione di dipendenza, un milione di piccole interfacce nessuna delle quali riflettere il vocabolario di dominio utilizzato per comunicare con la gestione del prodotto .... "


6

Probabilmente esiste una serie di modi in cui è possibile rispondere a questa domanda con diverse quantità di tempo. Tuttavia, penso che questo sia più sulla falsariga di "Sai cosa significa SOLID?" Quindi rispondere a questa domanda probabilmente si riduce a colpire i punti e spiegarlo in termini di progetto.

Quindi, ti aspetti di vedere quanto segue:

  • Le classi hanno un'unica responsabilità (ad es. Una classe di accesso ai dati per i clienti otterrà solo i dati dei clienti dal database dei clienti).
  • Le classi possono essere facilmente estese senza influire sul comportamento esistente. Non devo modificare proprietà o altri metodi per aggiungere funzionalità aggiuntive.
  • Le classi derivate possono essere sostituite con le classi base e le funzioni che usano quelle classi base non devono scartare la classe base nel tipo più specifico per gestirle.
  • Le interfacce sono piccole e facili da capire. Se una classe utilizza un'interfaccia, non è necessario dipendere da diversi metodi per eseguire un'attività.
  • Il codice è sufficientemente astratto in modo tale che l'implementazione di alto livello non dipenda concretamente da un'implementazione specifica di basso livello. Dovrei essere in grado di cambiare l'implementazione di basso livello senza influenzare il codice di alto livello. Ad esempio, posso cambiare il mio livello di accesso ai dati SQL per uno basato su un servizio Web senza influire sul resto della mia applicazione.

4

Questa è un'ottima domanda, anche se penso che sia una domanda difficile per l'intervista.

I principi SOLID regolano veramente le classi e le interfacce e il modo in cui si relazionano tra loro.

Questa domanda è davvero quella che ha più a che fare con i file e non necessariamente con le classi.

Una breve osservazione o risposta che darei è che generalmente vedrai file che contengono solo un'interfaccia, e spesso la convenzione è che iniziano con un I maiuscolo. Oltre a ciò, vorrei menzionare che i file non avrebbero un codice duplicato (specialmente all'interno di un modulo, un'applicazione o una libreria) e che il codice sarebbe stato condiviso attentamente attraverso determinati confini tra moduli, applicazioni o librerie.

Robert Martin discute questo argomento nel campo del C ++ nella progettazione di applicazioni C ++ orientate agli oggetti usando il metodo Booch (vedere le sezioni sulla coesione, la chiusura e la riusabilità) e in Clean Code .


I programmatori .NET IME seguono in genere una regola "1 classe per file" e rispecchiano anche le strutture di cartelle / spazi dei nomi; l'IDE di Visual Studio incoraggia entrambe le pratiche e vari plug-in come ReSharper possono applicarle. Quindi, mi aspetto di vedere una struttura di progetto / file che rispecchi la struttura di classe / interfaccia.
KeithS,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.