Sistema
Le caratteristiche generali desiderabili del sistema sono:- complessità minima : i progetti eccessivamente complicati dovrebbero essere evitati. La cosa principale è la semplicità e la chiarezza (migliore = semplice);
- facilità di manutenzione : quando crei un'applicazione, devi ricordare che dovrà essere supportata (anche se non sei tu), quindi il codice dovrebbe essere chiaro ed evidente;
- l'accoppiamento debole è il numero minimo di connessioni tra le diverse parti del programma (massimo utilizzo dei principi OOP);
- riusabilità : progettare un sistema con la capacità di riutilizzare i suoi frammenti in altre applicazioni;
- portabilità : il sistema deve essere facilmente adattabile ad un altro ambiente;
- stile unico - progettare un sistema in un unico stile nei suoi diversi frammenti;
- estensibilità (scalabilità) - migliorare il sistema senza disturbare la sua struttura di base (se aggiungi o modifichi un frammento, ciò non dovrebbe influenzare il resto).
Fasi di progettazione del sistema
- Sistema software : progettazione di un'applicazione in forma generale.
- Separazione in sottosistemi/pacchetti - definizione di parti logicamente separabili e definizione delle regole di interazione tra di loro.
- Dividere i sottosistemi in classi : dividere parti del sistema in classi e interfacce specifiche, nonché definire l'interazione tra loro.
- La divisione delle classi in metodi è una definizione completa dei metodi necessari per una classe, in base al compito di questa classe. Progettazione del metodo: definizione dettagliata della funzionalità dei singoli metodi.
Principi e concetti fondamentali della progettazione di sistemi
Linguaggio di inizializzazione pigro Un'applicazione non impiega tempo a creare un oggetto finché non viene utilizzato, il che accelera il processo di inizializzazione e riduce il carico del garbage collector. Ma non dovresti esagerare con questo, poiché ciò può portare a una violazione della modularità. Potrebbe valere la pena spostare tutte le fasi di progettazione su una parte specifica, ad esempio principale, o su una classe che funziona come una fabbrica . Uno degli aspetti di un buon codice è l'assenza di codice standard ripetuto frequentemente. Di norma, tale codice viene inserito in una classe separata in modo che possa essere richiamato al momento giusto. AOP Separatamente vorrei menzionare la programmazione orientata agli aspetti . Si tratta di programmare introducendo la logica end-to-end, ovvero il codice ripetuto viene inserito in classi - aspetti e richiamato quando vengono raggiunte determinate condizioni. Ad esempio, quando si accede a un metodo con un determinato nome o si accede a una variabile di un determinato tipo. A volte gli aspetti possono creare confusione, poiché non è immediatamente chiaro da dove viene richiamato il codice, ma si tratta comunque di una funzionalità molto utile. In particolare, durante la memorizzazione nella cache o il logging: aggiungiamo questa funzionalità senza aggiungere logica aggiuntiva alle classi regolari. Puoi leggere ulteriori informazioni sull'OAP qui . 4 regole per progettare un'architettura semplice secondo Kent Beck- Espressività : la necessità di uno scopo della classe chiaramente espresso si ottiene attraverso la corretta denominazione, le dimensioni ridotte e il rispetto del principio di responsabilità unica (lo vedremo più in dettaglio di seguito).
- Un minimo di classi e metodi : nel tuo desiderio di suddividere le classi in classi quanto più piccole e unidirezionali possibile, puoi andare troppo lontano (antipattern - shotgunning). Questo principio impone di mantenere il sistema compatto e di non esagerare, creando una classe per ogni starnuto.
- Mancanza di duplicazione : il codice aggiuntivo che crea confusione è un segno di scarsa progettazione del sistema e viene spostato in un luogo separato.
- Esecuzione di tutti i test - un sistema che ha superato tutti i test è controllato, poiché qualsiasi cambiamento può portare al fallimento dei test, il che può mostrarci che un cambiamento nella logica interna del metodo ha portato anche a un cambiamento nel comportamento atteso .
Interfaccia
Forse una delle fasi più importanti nella creazione di una classe adeguata è creare un'interfaccia adeguata che rappresenterà una buona astrazione che nasconde i dettagli di implementazione della classe e allo stesso tempo rappresenterà un gruppo di metodi chiaramente coerenti tra loro . Diamo uno sguardo più da vicino a uno dei principi SOLID: la segregazione dell'interfaccia : i client (classi) non dovrebbero implementare metodi non necessari che non utilizzeranno. Cioè, se parliamo di costruire interfacce con un numero minimo di metodi volti a svolgere l'unico compito di questa interfaccia (per me è molto simile alla responsabilità singola ), è meglio crearne un paio più piccoli quelli invece di un'interfaccia gonfia. Fortunatamente, una classe può implementare più di un'interfaccia, come nel caso dell'ereditarietà. È inoltre necessario ricordare la corretta denominazione delle interfacce: il nome dovrebbe riflettere il suo compito nel modo più accurato possibile. E, naturalmente, più breve sarà, minore sarà la confusione che causerà. È a livello di interfaccia che di solito vengono scritti i commenti per la documentazione , che, a loro volta, ci aiutano a descrivere in dettaglio cosa dovrebbe fare il metodo, quali argomenti accetta e cosa restituirà.Classe
Diamo un'occhiata all'organizzazione interna delle classi. O meglio, alcuni punti di vista e regole da seguire durante la costruzione delle classi. In genere, una classe dovrebbe iniziare con un elenco di variabili, disposte in un ordine specifico:- costanti statiche pubbliche;
- costanti statiche private;
- variabili di istanza private.
Dimensione della classe
Ora vorrei parlare della dimensione della classe. Ricordiamo uno dei principi di SOLID: la responsabilità unica . Responsabilità unica : il principio della responsabilità unica. Afferma che ogni oggetto ha un solo obiettivo (la responsabilità) e la logica di tutti i suoi metodi mira a garantirlo. Cioè, sulla base di ciò, dovremmo evitare classi grandi e gonfie (che per loro natura sono un antipattern - "oggetto divino"), e se abbiamo molti metodi di logica diversa ed eterogenea in una classe, dobbiamo pensare di suddividerlo in un paio di parti logiche (classi). Ciò, a sua volta, migliorerà la leggibilità del codice, poiché non abbiamo bisogno di molto tempo per comprendere lo scopo di un metodo se conosciamo lo scopo approssimativo di una determinata classe. Bisogna anche tenere d'occhio il nome della classe : dovrebbe riflettere la logica che contiene. Diciamo che se abbiamo una classe il cui nome contiene più di 20 parole, dobbiamo pensare al refactoring. Ogni classe che si rispetti non dovrebbe avere un numero così elevato di variabili interne. In effetti, ogni metodo funziona con uno o più di essi, il che provoca un maggiore accoppiamento all'interno della classe (che è esattamente quello che dovrebbe essere, poiché la classe dovrebbe essere un tutt'uno). Di conseguenza, l'aumento della coerenza di una classe porta a una sua diminuzione in quanto tale e, ovviamente, il nostro numero di classi aumenta. Per alcuni, questo è fastidioso; hanno bisogno di andare di più in classe per vedere come funziona un compito specifico e di grandi dimensioni. Tra l'altro ogni classe è un piccolo modulo che dovrebbe essere minimamente connesso alle altre. Questo isolamento riduce il numero di modifiche che dobbiamo apportare quando aggiungiamo ulteriore logica a una classe.Oggetti
Incapsulamento
Qui parleremo prima di tutto di uno dei principi dell'incapsulamento OOP . Quindi, nascondere l'implementazione non si riduce alla creazione di un livello di metodo tra le variabili (limitando sconsideratamente l'accesso tramite singoli metodi, getter e setter, il che non va bene, poiché si perde l'intero punto di incapsulamento). Nascondere l'accesso ha lo scopo di formare astrazioni, ovvero la classe fornisce metodi concreti comuni attraverso i quali lavoriamo con i nostri dati. Ma non è necessario che l'utente sappia esattamente come lavoriamo con questi dati: funziona e va bene.Legge di Demetra
Puoi anche considerare la Legge di Demetra: è un piccolo insieme di regole che aiuta a gestire la complessità a livello di classe e metodo. Quindi, supponiamo di avere un oggettoCar
e che abbia un metodo - move(Object arg1, Object arg2)
. Secondo la Legge di Demetra, questo metodo si limita a chiamare:
- metodi dell'oggetto stesso
Car
(in altre parole, this); - metodi degli oggetti creati in
move
; - metodi degli oggetti passati come argomenti -
arg1
,arg2
; - metodi degli oggetti interni
Car
(lo stesso di questo).
Struttura dati
Una struttura dati è una raccolta di elementi correlati. Quando si considera un oggetto come una struttura dati, si tratta di un insieme di elementi dati che vengono elaborati da metodi, la cui esistenza è implicita. Cioè, è un oggetto il cui scopo è archiviare e gestire (elaborare) i dati memorizzati. La differenza fondamentale rispetto a un oggetto normale è che un oggetto è un insieme di metodi che operano su elementi di dati la cui esistenza è implicita. Capisci? In un oggetto normale, l'aspetto principale sono i metodi e le variabili interne mirano al loro corretto funzionamento, ma in una struttura dati è il contrario: i metodi supportano e aiutano a lavorare con gli elementi memorizzati, che qui sono i principali. Un tipo di struttura dati è Data Transfer Object (DTO) . Questa è una classe con variabili pubbliche e nessun metodo (o solo metodi di lettura/scrittura) che passa dati quando si lavora con i database, lavora con l'analisi dei messaggi dai socket, ecc. In genere, i dati in tali oggetti non vengono archiviati per molto tempo e sono convertito quasi immediatamente nell'entità con cui funziona la nostra applicazione. Un'entità, a sua volta, è anche una struttura dati, ma il suo scopo è partecipare alla logica aziendale a diversi livelli dell'applicazione, mentre il DTO è trasportare dati da/verso l'applicazione. Esempio DTO:@Setter
@Getter
@NoArgsConstructor
public class UserDto {
private long id;
private String firstName;
private String lastName;
private String email;
private String password;
}
Tutto sembra chiaro, ma qui apprendiamo dell'esistenza degli ibridi. Gli ibridi sono oggetti che contengono metodi per gestire la logica importante e archiviare elementi interni e metodi di accesso (get/set). Tali oggetti sono disordinati e rendono difficile l'aggiunta di nuovi metodi. Non dovresti usarli, poiché non è chiaro a cosa siano destinati: memorizzare elementi o eseguire qualche tipo di logica. Puoi leggere informazioni sui possibili tipi di oggetti qui .
Principi di creazione delle variabili
Pensiamo un po' alle variabili, o meglio, pensiamo a quali potrebbero essere i principi per crearle:- Idealmente, dovresti dichiarare e inizializzare una variabile immediatamente prima di usarla (piuttosto che crearla e dimenticartene).
- Quando possibile, dichiarare le variabili come finali per evitare che il loro valore cambi dopo l'inizializzazione.
- Non dimenticare le variabili contatore (di solito le usiamo in una sorta di loop
for
, cioè non dobbiamo dimenticare di reimpostarle, altrimenti potrebbe interrompere l'intera logica). - Dovresti provare a inizializzare le variabili nel costruttore.
- Se è possibile scegliere tra l'utilizzo di un oggetto con o senza riferimento (
new SomeObject()
), scegliere senza ( ), poiché questo oggetto, una volta utilizzato, verrà eliminato durante la successiva garbage collection e non sprecherà risorse. - Rendi la durata delle variabili quanto più breve possibile (la distanza tra la creazione di una variabile e l'ultimo accesso).
- Inizializza le variabili utilizzate in un ciclo immediatamente prima del ciclo, anziché all'inizio del metodo che contiene il ciclo.
- Inizia sempre con l'ambito più limitato ed espandilo solo se necessario (dovresti provare a rendere la variabile il più locale possibile).
- Utilizzare ciascuna variabile per un solo scopo.
- Evita variabili con significati nascosti (la variabile è divisa tra due compiti, il che significa che il suo tipo non è adatto per risolverne uno).
Metodi
Passiamo direttamente all'implementazione della nostra logica, ovvero ai metodi.-
La prima regola è la compattezza. Idealmente, un metodo non dovrebbe superare le 20 righe, quindi se, ad esempio, un metodo pubblico “si gonfia” in modo significativo, è necessario pensare a spostare la logica separata in metodi privati.
-
La seconda regola è che i blocchi nei comandi
if
,else
,while
e così via non dovrebbero essere altamente annidati: ciò riduce notevolmente la leggibilità del codice. Idealmente, l'annidamento non dovrebbe superare i due blocchi{}
.Si consiglia inoltre di rendere il codice in questi blocchi compatto e semplice.
-
La terza regola è che un metodo deve eseguire una sola operazione. Cioè, se un metodo esegue una logica complessa e varia, lo dividiamo in sottometodi. Di conseguenza, il metodo stesso sarà una facciata, il cui scopo è richiamare tutte le altre operazioni nell'ordine corretto.
Ma cosa succede se l'operazione sembra troppo semplice per creare un metodo separato? Sì, a volte può sembrare come sparare ai passeri da un cannone, ma i piccoli metodi offrono numerosi vantaggi:
- lettura più semplice del codice;
- i metodi tendono a diventare più complessi nel corso dello sviluppo e, se inizialmente il metodo era semplice, complicarne le funzionalità sarà un po' più semplice;
- nascondere i dettagli di implementazione;
- facilitare il riutilizzo del codice;
- maggiore affidabilità del codice.
-
La regola al ribasso è che il codice va letto dall'alto verso il basso: più è basso, maggiore è la profondità della logica, e viceversa, più alti, più astratti sono i metodi. Ad esempio, i comandi switch sono piuttosto poco compatti e indesiderabili, ma se non puoi fare a meno di usare uno switch, dovresti provare a spostarlo il più in basso possibile, nei metodi di livello più basso.
-
Argomenti sul metodo : quanti sono ideali? Idealmente, non ce ne sono affatto)) Ma succede davvero? Tuttavia, dovresti cercare di averne il minor numero possibile, perché meno ce ne sono, più facile sarà usare questo metodo e testarlo. In caso di dubbio, provare a indovinare tutti gli scenari per l'utilizzo di un metodo con un numero elevato di argomenti di input.
-
Separatamente, vorrei evidenziare i metodi che hanno un flag booleano come argomento di input , poiché ciò implica naturalmente che questo metodo implementa più di un'operazione (se vero allora uno, falso - un'altra). Come ho scritto sopra, questo non va bene e dovrebbe essere evitato se possibile.
-
Se un metodo ha un gran numero di argomenti in entrata (il valore estremo è 7, ma dovresti pensarci dopo 2-3), devi raggruppare alcuni argomenti in un oggetto separato.
-
Se esistono più metodi simili (sovraccarati) , allora parametri simili devono essere passati nello stesso ordine: questo migliora la leggibilità e l'usabilità.
-
Quando passi i parametri ad un metodo devi essere sicuro che verranno utilizzati tutti, altrimenti a cosa serve l'argomento? Taglialo fuori dall'interfaccia e basta.
-
try/catch
Non sembra molto carino per sua natura, quindi una buona mossa sarebbe spostarlo in un metodo intermedio separato (metodo per la gestione delle eccezioni):public void exceptionHandling(SomeObject obj) { try { someMethod(obj); } catch (IOException e) { e.printStackTrace(); } }
GO TO FULL VERSION