Scrivere codice, che è facile da cambiare, è il Santo Graal della programmazione. Benvenuto nella programmazione del nirvana! Ma le cose sono molto più difficili in realtà: il codice sorgente è difficile da capire, il punto di dipendenza in infinite direzioni, l'accoppiamento è fastidioso, e presto senti il calore dell'inferno di programmazione. In questo tutorial, discuteremo alcuni principi, tecniche e idee che ti aiuteranno a scrivere codice che è facile da cambiare.
La programmazione orientata agli oggetti (OOP) divenne popolare, grazie alla sua promessa di organizzazione e riutilizzo dei codici; fallì completamente in questa impresa. Abbiamo utilizzato i concetti OOP per molti anni, eppure continuiamo a implementare ripetutamente la stessa logica nei nostri progetti. OOP ha introdotto una serie di buoni principi di base che, se utilizzati correttamente, possono portare a un codice migliore e più pulito.
Le cose che appartengono insieme dovrebbero essere tenute insieme; altrimenti, dovrebbero essere spostati altrove. Questo è ciò a cui si riferisce il termine "coesione". Il miglior esempio di coesione può essere dimostrato con una classe:
class ANOTCohesiveClass private $ firstNumber; private $ secondNumber; lunghezza $ privata; $ larghezza privata; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; function setLength ($ length) $ this-> length = $ length; function setHeight ($ height) $ this-> width = $ height; function add () return $ this-> firstNumber + $ this-> secondNumber; function sottrarre () return $ this-> firstNumber - $ this-> secondNumber; function area () return $ this-> length * $ this-> width;
Questo esempio definisce una classe con campi che rappresentano numeri e dimensioni. Queste proprietà, giudicate solo dal loro nome, non appartengono insieme. Abbiamo quindi due metodi, Inserisci()
e sottrarre ()
, che operano solo sulle due variabili numeriche. Abbiamo anche un la zona()
metodo, che opera su lunghezza
e larghezza
i campi.
È ovvio che questa classe è responsabile per gruppi separati di informazioni. Ha una coesione molto bassa. Facciamo il refactoring.
class ACohesiveClass private $ firstNumber; private $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; function add () return $ this-> firstNumber + $ this-> secondNumber; function sottrarre () return $ this-> firstNumber - $ this-> secondNumber;
Questo è un classe altamente coesa. Perché? Perché ogni sezione di questa classe appartiene l'una all'altra. Dovresti lottare per la coesione, ma attenzione, può essere difficile da raggiungere.
In termini semplici, l'ortogonalità si riferisce all'isolamento o all'eliminazione degli effetti collaterali. Un metodo, classe o modulo che cambia lo stato di altre classi o moduli non correlati non è ortogonale. Ad esempio, la scatola nera di un aereo è ortogonale. Ha la sua funzionalità interna, fonte di alimentazione interna, microfoni e sensori. Non ha alcun effetto sull'aereo in cui risiede, o nel mondo esterno. Fornisce solo un meccanismo per registrare e recuperare i dati di volo.
Un esempio di un tale sistema non ortogonale è l'elettronica della tua auto. Aumentare la velocità del tuo veicolo ha diversi effetti collaterali, come aumentare il volume della radio (tra le altre cose). La velocità non è ortogonale alla macchina.
class Calculator private $ firstNumber; private $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; function add () $ sum = $ this-> firstNumber + $ this-> secondNumber; if ($ sum> 100) (new AlertMechanism ()) -> tooBigNumber ($ sum); return $ sum; function sottrarre () return $ this-> firstNumber - $ this-> secondNumber; class AlertMechanism function tooBigNumber ($ number) echo $ number. 'è troppo grande!';
In questo esempio, il Calcolatrice
La classe di Inserisci()
il metodo mostra un comportamento inaspettato: crea un AlertMechanism
oggetto e chiama uno dei suoi metodi. Questo è un comportamento inaspettato e indesiderato; i consumatori delle biblioteche non si aspetteranno mai che un messaggio venga stampato sullo schermo. Invece, si aspettano solo la somma dei numeri forniti.
class Calculator private $ firstNumber; private $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; function add () return $ this-> firstNumber + $ this-> secondNumber; function sottrarre () return $ this-> firstNumber - $ this-> secondNumber; class AlertMechanism function checkLimits ($ firstNumber, $ secondNumber) $ sum = (nuovo Calculator ($ firstNumber, $ secondNumber)) -> add (); se ($ sum> 100) $ this-> tooBigNumber ($ sum); function tooBigNumber ($ number) echo $ number. 'è troppo grande!';
Questo è meglio. AlertMechanism
non ha alcun effetto Calcolatrice
. Anziché, AlertMechanism
utilizza tutto ciò di cui ha bisogno per determinare se deve essere emessa una segnalazione.
Nella maggior parte dei casi, queste due parole sono intercambiabili; ma, in alcuni casi, un termine è preferito a un altro.
Quindi cosa è un dipendenza? Quando oggetto UN
ha bisogno di usare l'oggetto B
, per eseguire il suo comportamento prescritto, lo diciamo UN
dipende da B
. In OOP, le dipendenze sono estremamente comuni. Gli oggetti lavorano frequentemente e dipendono l'uno dall'altro. Quindi, mentre eliminare la dipendenza è una ricerca nobile, è quasi impossibile farlo. Tuttavia, è preferibile controllare le dipendenze e ridurle.
I termini, pesante accoppiamento e accoppiamento lasco, di solito si riferisce a quanto un oggetto dipende da altri oggetti.
In un sistema liberamente accoppiato, i cambiamenti in un oggetto hanno un effetto ridotto sugli altri oggetti che dipendono da esso. In tali sistemi, le classi dipendono dalle interfacce anziché dalle implementazioni concrete (ne parleremo più avanti in seguito). Questo è il motivo per cui i sistemi liberamente accoppiati sono più aperti alle modifiche.
Consideriamo un esempio:
class Display calcolatrice privata $; function __construct () $ this-> calculator = new Calculator (1,2);
È comune vedere questo tipo di codice. Una classe, Display
in questo caso, dipende dal Calcolatrice
classe facendo direttamente riferimento a quella classe. Nel codice sopra, Display
'S $ calcolatrice
il campo è di tipo Calcolatrice
. L'oggetto che il campo contiene è il risultato della chiamata diretta Calcolatrice
Il costruttore.
Rivedere il seguente codice per una dimostrazione di questo tipo di accoppiamento:
class Display calcolatrice privata $; function __construct () $ this-> calculator = new Calculator (1, 2); function printSum () echo $ this-> calculator-> add ();
Il Display
la classe chiama il Calcolatrice
oggetto di Inserisci()
metodo. Questa è un'altra forma di accoppiamento, perché una classe accede al metodo dell'altro.
Puoi anche abbinare le classi con i riferimenti al metodo. Per esempio:
class Display calcolatrice privata $; function __construct () $ this-> calculator = $ this-> makeCalculator (); function printSum () echo $ this-> calculator-> add (); function makeCalculator () return new Calculator (1, 2);
È importante notare che il makeCalculator ()
metodo restituisce a Calcolatrice
oggetto. Questa è una dipendenza.
L'ereditarietà è probabilmente la forma più forte di dipendenza:
class AdvancedCalculator estende la funzione Calculator function sinus ($ value) return sin ($ value);
Non solo può AdvancedCalculator
non fare il suo lavoro senza Calcolatrice
, ma non potrebbe nemmeno esistere senza di essa.
Si può ridurre l'accoppiamento iniettando una dipendenza. Ecco uno di questi esempi:
class Display calcolatrice privata $; function __construct (Calculator $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator (); // ... //
Iniettando il Calcolatrice
oggetto attraverso Display
Costruttore, abbiamo ridotto Display
La dipendenza da Calcolatrice
classe. Ma questa è solo metà della soluzione.
Possiamo ridurre ulteriormente l'accoppiamento usando le interfacce. Per esempio:
interfaccia CanCompute function add (); funzione sottrarre (); class Calculator implementa CanCompute private $ firstNumber; private $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; function add () return $ this-> firstNumber + $ this-> secondNumber; function sottrarre () return $ this-> firstNumber - $ this-> secondNumber; display di classe calcolatore $ privato; function __construct (CanCompute $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator (); function printSum () echo $ this-> calculator-> add (); function makeCalculator () return new Calculator (1, 2);
Puoi pensare all'ISP come a un principio di coesione di alto livello.
Questo codice introduce il CanCompute
interfaccia. Un'interfaccia è astratta come è possibile ottenere in OOP; definisce i membri che una classe deve implementare. Nel caso dell'esempio precedente, Calcolatrice
implementa il CanCompute
interfaccia.
Display
Il costruttore si aspetta un oggetto che implementa CanCompute
. A questo punto, Display
La dipendenza con Calcolatrice
è effettivamente rotto In qualsiasi momento, possiamo creare un'altra classe che implementa CanCompute
e passare un oggetto di quella classe a Display
Il costruttore. Display
ora dipende solo dal CanCompute
interfaccia, ma anche quella dipendenza è facoltativa. Se non passiamo argomenti a Display
Costruttore, creerà semplicemente un classico Calcolatrice
oggetto chiamando makeCalculator ()
. Questa tecnica è spesso utilizzata ed è estremamente utile per lo sviluppo basato sui test (TDD).
SOLID è un insieme di principi per scrivere codice pulito, che rende quindi più semplice cambiare, mantenere ed estendere in futuro. Sono raccomandazioni che, se applicate al codice sorgente, hanno un effetto positivo sulla manutenibilità.
I principi SOLID, noti anche come principi Agile, furono inizialmente definiti da Robert C. Martin. Anche se non ha inventato tutti questi principi, è stato lui a metterli insieme. Puoi leggere di più su di loro nel suo libro: Agile Software Development, Principles, Patterns and Practices. I principi di SOLID coprono una vasta gamma di argomenti, ma li presenterò nel modo più semplice possibile. Sentiti libero di chiedere ulteriori dettagli nei commenti, se necessario.
Una classe ha una sola responsabilità. Questo può sembrare semplice, ma a volte può essere difficile da capire e mettere in pratica.
class Reporter function generateIncomeReports (); function generatePaymentsReports (); function computeBalance (); funzione printReport ();
Chi pensi benefici dal comportamento di questa classe? Bene, un reparto contabilità è un'opzione (per il saldo), il reparto finanziario può essere un altro (per i rapporti reddito / pagamento), e anche il dipartimento di archiviazione potrebbe stampare e archiviare i rapporti.
Ci sono quattro ragioni per cui potresti dover cambiare questa classe; ogni dipartimento può voler personalizzare i rispettivi metodi per le proprie esigenze.
L'SRP raccomanda di suddividere tali classi in classi più piccole e specifiche per il singolo, ognuna con una sola ragione per cambiare. Tali classi tendono ad essere molto coese e liberamente accoppiate. In un certo senso, SRP è definito dalla coesione dal punto di vista degli utenti.
Le classi (e i moduli) dovrebbero accogliere l'estensione delle loro funzionalità, oltre a resistere alle modifiche delle loro attuali funzionalità. Giochiamo con il classico esempio di un elettroventilatore. Hai un interruttore e vuoi controllare la ventola. Quindi, potresti scrivere qualcosa sulla falsariga di:
class Switch_ private $ fan; function __construct () $ this-> fan = new Fan (); function turnOn () $ this-> fan-> on (); function turnOff () $ this-> fan-> off ();
L'ereditarietà è probabilmente la forma più forte di dipendenza.
Questo codice definisce a Interruttore_
classe che crea e controlla a Fan
oggetto. Si prega di notare la sottolineatura dopo "Switch_". PHP non ti permette di definire una classe con il nome "Switch".
Il tuo capo decide che vuole controllare la luce con lo stesso interruttore. Questo è un problema, perché tu Devo cambiare Interruttore_
.
Qualsiasi modifica al codice esistente è un rischio; altre parti del sistema potrebbero essere interessate e richiedere ulteriori modifiche. È sempre preferibile lasciare la funzionalità esistente da sola, quando si aggiunge una nuova funzione.
Nella terminologia OOP, puoi vederlo Interruttore_
ha una forte dipendenza da Fan
. È qui che si trova il nostro problema e dove dovremmo apportare i nostri cambiamenti.
interfaccia Commutabile function on (); funzione off (); classe Fan implementa Switchable public function on () // codice per avviare il fan public function off () // codice per arrestare il fan classe Switch_ private $ switchable; function __construct (Commutabile $ switchable) $ this-> commutabile = $ commutabile; function turnOn () $ this-> commutabile-> on (); function turnOff () $ this-> commutabile-> off ();
Questa soluzione introduce il commutabile
interfaccia. Definisce i metodi che tutti gli oggetti abilitati allo switch devono implementare. Il Fan
attrezzi commutabile
, e Interruttore_
accetta un riferimento a a commutabile
oggetto all'interno del suo costruttore.
Come ci aiuta??
In primo luogo, questa soluzione rompe la dipendenza tra Interruttore_
e Fan
. Interruttore_
non ha idea che si avvia un fan, né gli importa. Secondo, introducendo a Luce
la classe non influenzerà Interruttore_
o commutabile
. Vuoi controllare a Luce
oggetto con il tuo Interruttore_
classe? Crea semplicemente a Luce
oggetto e passarlo a Interruttore_
, come questo:
class Light implementa Commutabile public function on () // codice per attivare ligh on public function off () // codice per spegnere la luce classe SomeWhereInYourCode function controlLight () $ light = new Light (); $ switch = new Switch _ ($ light); $ -SWITCH-> turnOn (); $ -SWITCH-> bivio ();
LSP afferma che una classe figlia non dovrebbe mai interrompere la funzionalità della classe genitore. Questo è estremamente importante perché i consumatori di una classe genitore si aspettano che la classe si comporti in un certo modo. Passare una lezione di bambino a un consumatore dovrebbe funzionare e non influire sulla funzionalità originale.
Ciò è confuso a prima vista, quindi diamo un'occhiata a un altro esempio classico:
class Rectangle private $ width; altezza $ privata; function setWidth ($ width) $ this-> width = $ width; function setHeigth ($ heigth) $ this-> height = $ heigth; function area () return $ this-> width * $ this-> height;
Questo esempio definisce un semplice Rettangolo
classe. Possiamo impostare la sua altezza e larghezza, e la sua la zona()
il metodo fornisce l'area del rettangolo. Usando il Rettangolo
la classe potrebbe apparire come la seguente:
class Geometry function rectArea (Rectangle $ rectangle) $ rectangle-> setWidth (10); $ Rectangle-> setHeigth (5); return $ rectangle-> area ();
Il rectArea ()
il metodo accetta a Rettangolo
oggetto come argomento, imposta la sua altezza e larghezza e restituisce l'area della forma.
A scuola, ci viene insegnato che i quadrati sono rettangoli. Questo suggerisce che se modelliamo il nostro programma sul nostro oggetto geometrico, a Piazza
la classe dovrebbe estendersi a Rettangolo
classe. Come sarebbe una classe del genere?
class square estende Rectangle // Quale codice scrivere qui?
Ho difficoltà a capire cosa scrivere in Piazza
classe. Abbiamo diverse opzioni. Potremmo scavalcare il la zona()
metodo e restituire il quadrato di $ larghezza
:
class Rectangle protected $ width; altezza $ protetta; // ... // class Square estende Rectangle function area () return $ this-> width ^ 2;
Nota che ho cambiato Rettangolo
I campi di protetta
, dando Piazza
accesso a quei campi. Questo sembra ragionevole da un punto di vista geometrico. Un quadrato ha lati uguali; restituire il quadrato di larghezza è ragionevole.
Tuttavia, abbiamo un problema da un punto di vista della programmazione. Se Piazza
è un Rettangolo
, non dovremmo avere problemi ad alimentarlo nel Geometria
classe. Ma, così facendo, puoi vederlo Geometria
Il codice non ha molto senso; imposta due valori diversi per altezza e larghezza. Questo è il motivo per cui un quadrato non è un rettangolo in programmazione. LSP violata.
I test unitari dovrebbero funzionare velocemente - molto velocemente.
Questo principio si concentra sulla rottura di interfacce di grandi dimensioni in interfacce piccole e specializzate. L'idea di base è che i diversi consumatori della stessa classe non dovrebbero conoscere le diverse interfacce, ma solo le interfacce che il consumatore deve utilizzare. Anche se un consumatore non utilizza direttamente tutti i metodi pubblici su un oggetto, dipende comunque da tutti i metodi. Quindi, perché non fornire interfacce con questo, solo dichiarare i metodi che ogni utente ha bisogno?
Questo è in stretta conformità che le interfacce dovrebbero appartenere ai client e non all'implementazione. Se si adattano le interfacce alle classi che consumano, rispetteranno l'ISP. L'implementazione stessa può essere unica, in quanto una classe può implementare diverse interfacce.
Immaginiamo di implementare un'applicazione di borsa. Abbiamo un broker che compra e vende azioni e può riportare i suoi guadagni e le perdite giornaliere. Un'implementazione molto semplice includerebbe qualcosa come a Broker
interfaccia, a NYSEBroker
classe che implementa Broker
e un paio di classi di interfaccia utente: una per la creazione di transazioni (TransactionsUI
) e uno per la segnalazione (DailyReporter
). Il codice per tale sistema potrebbe essere simile al seguente:
Interfaccia Broker funzione buy ($ symbol, $ volume); funzione sell ($ symbol, $ volume); funzione dailyLoss ($ date); funzione dailyEarnings ($ date); classe NYSEBroker implementa Broker public function buy ($ symbol, $ volume) // implementsation goes here public function currentBalance () // implementsation goes here public function dailyEarnings ($ date) // implementsation goes here public function dailyLoss ($ date) // implementsation goes here public function sell ($ symbol, $ volume) // implementsation goes here classe TransactionsUI private $ broker; funzione __construct (broker $ Broker) $ this-> broker = $ broker; function buyStocks () // La logica dell'interfaccia utente qui per ottenere informazioni da un modulo in $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volume']); function sellStocks () // La logica dell'interfaccia utente qui per ottenere informazioni da un modulo in $ data $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']); class DailyReporter private $ broker; funzione __construct (broker $ Broker) $ this-> broker = $ broker; function currentBalance () echo 'Current balace for today'. appuntamento()) . "\ N"; echo 'Guadagni:'. $ this-> broker-> dailyEarnings (time ()). "\ N"; echo 'Losses:'. $ this-> broker-> dailyLoss (time ()). "\ N";
Mentre questo codice potrebbe funzionare, viola l'ISP. Tutti e due DailyReporter
e TransactionUI
dipende dal Broker
interfaccia. Tuttavia, ognuno utilizza solo una frazione dell'interfaccia. TransactionUI
usa il acquistare()
e vendere()
metodi, mentre DailyReporter
usa il dailyEarnings ()
e dailyLoss ()
metodi.
Potresti obiettare
Broker
non è coeso perché ha metodi non correlati e quindi non appartengono insieme.
Questo può essere vero, ma la risposta dipende dalle implementazioni di Broker
; la vendita e l'acquisto possono essere strettamente correlati alle perdite e ai guadagni correnti. Ad esempio, potresti non essere autorizzato ad acquistare azioni se stai perdendo denaro.
Potresti anche sostenerlo Broker
viola anche SRP. Poiché abbiamo due classi che lo utilizzano in modi diversi, potrebbero esserci due utenti diversi. Bene, io dico di no. L'unico utente è probabilmente il vero broker. Lui / lei vuole comprare, vendere e vedere i loro fondi attuali. Ma ancora una volta, la risposta effettiva dipende dall'intero sistema e dal business.
ISP è sicuramente violato. Entrambe le classi dell'interfaccia utente dipendono dal tutto Broker
. Questo è un problema comune, se pensi che le interfacce appartengano alle loro implementazioni. Tuttavia, spostando il tuo punto di vista puoi suggerire il seguente disegno:
interfaccia BrokerTransactions funzione buy ($ symbol, $ volume); funzione sell ($ symbol, $ volume); interfaccia BrokerStatistics function dailyLoss ($ date); funzione dailyEarnings ($ date); classe NYSEBroker implementa BrokerTransactions, BrokerStatistics public function buy ($ symbol, $ volume) // l'implicazione va qui public function currentBalance () // implementsation goes here public function dailyEarnings ($ date) // l'implicazione va qui public function dailyLoss ($ date) // implementsation goes here public function sell ($ symbol, $ volume) // implementsation goes here classe TransactionsUI private $ broker; funzione __construct (BrokerTransactions $ broker) $ this-> broker = $ broker; function buyStocks () // La logica dell'interfaccia utente qui per ottenere informazioni da un modulo in $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volume']); function sellStocks () // La logica dell'interfaccia utente qui per ottenere informazioni da un modulo in $ data $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']); class DailyReporter private $ broker; funzione __construct (broker $ BrokerStatistics) $ this-> broker = $ broker; function currentBalance () echo 'Current balace for today'. appuntamento()) . "\ N"; echo 'Guadagni:'. $ this-> broker-> dailyEarnings (time ()). "\ N"; echo 'Losses:'. $ this-> broker-> dailyLoss (time ()). "\ N";
Questo in realtà ha senso e rispetta l'ISP. DailyReporter
dipende solo da BrokerStatistics
; non gli importa e non deve sapere su eventuali operazioni di vendita e acquisto. TransactionsUI
, d'altra parte, sa solo di comprare e vendere. Il NYSEBroker
è identico alla nostra classe precedente, tranne che ora implementa il BrokerTransactions
e BrokerStatistics
interfacce.
Puoi pensare all'ISP come a un principio di coesione di alto livello.
Quando entrambe le classi UI dipendevano dal Broker
interfaccia, erano simili a due classi, ognuna con quattro campi, di cui due erano utilizzati in un metodo e gli altri due in un altro metodo. La classe non sarebbe stata molto coesa.
Un esempio più complesso di questo principio può essere trovato in uno dei primi articoli di Robert C. Martin sull'argomento: Il principio di segregazione dell'interfaccia.
Questo principio afferma che i moduli di alto livello non dovrebbero dipendere da moduli di basso livello; entrambi dovrebbero dipendere dalle astrazioni. Le astrazioni non dovrebbero dipendere dai dettagli; i dettagli dovrebbero dipendere dalle astrazioni. In parole povere, dovresti dipendere dalle astrazioni il più possibile e mai da implementazioni concrete.
Il trucco con DIP è che vuoi invertire la dipendenza, ma vuoi sempre mantenere il flusso del controllo. Esaminiamo il nostro esempio dall'OCP (il Interruttore
e Luce
classi). Nell'implementazione originale, avevamo un interruttore che controlla direttamente una luce.
Come puoi vedere, sia la dipendenza che il flusso di controllo Interruttore
verso Luce
. Mentre questo è ciò che vogliamo, non vogliamo dipendere direttamente da Luce
. Quindi abbiamo introdotto un'interfaccia.
È sorprendente come la semplice introduzione di un'interfaccia faccia sì che il nostro codice rispetti sia il DIP che l'OCP. Come puoi vedere no, la classe dipende dall'implementazione concreta di Luce
, ed entrambi Luce
e Interruttore
dipende dal commutabile
interfaccia. Abbiamo invertito la dipendenza e il flusso di controllo è rimasto invariato.
Un altro aspetto importante del tuo codice è il tuo design di alto livello e l'architettura generale. Un'architettura entangled produce codice che è difficile da modificare. Mantenere un'architettura pulita è essenziale, e il primo passo è capire come separare le diverse preoccupazioni del codice.
In questa immagine, ho tentato di riassumere le principali preoccupazioni. Al centro dello schema c'è la nostra logica aziendale. Dovrebbe essere ben isolato dal resto del mondo ed essere in grado di lavorare e comportarsi come previsto senza l'esistenza di nessuna delle altre parti. Guardalo come ortogonalità a un livello più alto.
Partendo da destra, hai il tuo "principale" - il punto di accesso all'applicazione - e le fabbriche che creano oggetti. Una soluzione ideale otterrebbe i suoi oggetti da fabbriche specializzate, ma ciò è in gran parte impossibile o poco pratico. Tuttavia, dovresti utilizzare le fabbriche quando ne hai l'opportunità e tenerle al di fuori della tua logica aziendale.
Quindi, in basso (in arancione), abbiamo persistenza (database, accesso ai file, comunicazioni di rete) ai fini delle informazioni persistenti. Nessun oggetto nella nostra logica aziendale dovrebbe sapere come funziona la persistenza.
A sinistra è il meccanismo di consegna.
Un MVC, come Laravel o CakePHP, dovrebbe essere solo il meccanismo di consegna, niente di più.
Ciò consente di scambiare un meccanismo con un altro senza toccare la logica aziendale. Questo può sembrare scandaloso per alcuni di voi. Ci è stato detto che la nostra logica aziendale dovrebbe essere inserita nei nostri modelli. Bene, non sono d'accordo. I nostri modelli dovrebbero essere "modelli di richiesta", ovvero oggetti dati stupidi usati per trasmettere informazioni da MVC alla logica aziendale. Opzionalmente, non vedo alcun problema, inclusa la convalida dell'input nei modelli, ma niente di più. La logica aziendale non dovrebbe essere nei modelli.
Quando guardi l'architettura della tua applicazione o la struttura della directory, dovresti vedere una struttura che suggerisce cosa fa il programma invece di quale framework o database hai usato.
Infine, assicurati che tutte le dipendenze puntino verso la nostra logica aziendale. Interfacce utente, fabbriche, database sono implementazioni molto concrete e non dovresti mai dipendere da esse. Invertire le dipendenze per puntare alla nostra logica aziendale, modularizza il nostro sistema, permettendoci di cambiare le dipendenze senza modificare la logica aziendale.
Gli schemi di progettazione svolgono un ruolo importante nel rendere il codice più facile da modificare, offrendo una soluzione progettuale comune che ogni programmatore può comprendere. Da un punto di vista strutturale, i modelli di progettazione sono ovviamente vantaggiosi. Sono soluzioni ben collaudate e ponderate.
Se vuoi saperne di più sui modelli di design, ho creato un corso Tuts + Premium su di essi!
Test-Driven Development incoraggia la scrittura di codice facile da testare. TDD ti obbliga a rispettare la maggior parte dei principi sopra riportati per rendere il tuo codice facile da testare. Iniettare dipendenze e scrivere classi ortogonali sono essenziali; altrimenti, si finisce con enormi metodi di test. I test unitari dovrebbero funzionare velocemente, molto velocemente, in realtà, e tutto ciò che non è testato dovrebbe essere deriso. Prendere in giro molte classi complesse per un semplice test può essere schiacciante. Quindi, quando ti ritrovi a prendere in giro dieci oggetti per testare un singolo metodo su una classe, potresti avere un problema con il tuo codice ... non il tuo test.
Alla fine della giornata, tutto si riduce a quanto ti preoccupi del tuo codice sorgente. Avere conoscenze tecniche non è abbastanza; hai bisogno di applicare questa conoscenza ancora e ancora, non essere mai soddisfatto al 100% con il tuo codice. Devi rendere il tuo codice facile da mantenere, pulito e aperto al cambiamento.
Grazie per la lettura e sentiti libero di contribuire con le tue tecniche nei commenti qui sotto.