CodeGym /Java Blog /Random-IT /Regole di codifica: dalla creazione di un sistema all'uti...
John Squirrels
Livello 41
San Francisco

Regole di codifica: dalla creazione di un sistema all'utilizzo degli oggetti

Pubblicato nel gruppo Random-IT
Buona giornata a tutti! Oggi vorremmo parlarvi di come scrivere un buon codice. Certo, non tutti vogliono masticare subito libri come Clean Code, poiché contengono abbondanti quantità di informazioni ma all'inizio non molto è chiaro. E quando finisci di leggere, potresti uccidere tutto il tuo desiderio di programmare. Considerando tutto ciò, oggi voglio fornirti una piccola guida (un piccolo insieme di raccomandazioni) per scrivere codice migliore. In questo articolo esaminiamo le regole ei concetti di base relativi alla creazione di un sistema e all'utilizzo di interfacce, classi e oggetti. La lettura di questo articolo non richiederà molto tempo e, spero, non ti annoierà. Lavorerò dall'alto verso il basso, cioè dalla struttura generale di un'applicazione ai suoi dettagli più ristretti. Regole di programmazione: dalla creazione di un sistema al lavoro con gli oggetti - 1

Sistemi

Le seguenti sono generalmente caratteristiche desiderabili di un sistema:
  • Complessità minima. Bisogna evitare progetti eccessivamente complicati. La cosa più importante è la semplicità e la chiarezza (più semplice = migliore).
  • Facilità di manutenzione. Quando crei un'applicazione, devi ricordare che dovrà essere mantenuta (anche se non sarai personalmente responsabile della sua manutenzione). Ciò significa che il codice deve essere chiaro e ovvio.
  • Accoppiamento lasco. Ciò significa che riduciamo al minimo il numero di dipendenze tra le diverse parti del programma (massimizzando la nostra conformità ai principi OOP).
  • Riutilizzabilità. Progettiamo il nostro sistema con la possibilità di riutilizzare i componenti in altre applicazioni.
  • Portabilità. Dovrebbe essere facile adattare un sistema a un altro ambiente.
  • Stile uniforme. Progettiamo il nostro sistema utilizzando uno stile uniforme nelle sue varie componenti.
  • Estensibilità (scalabilità). Possiamo migliorare il sistema senza violare la sua struttura di base (l'aggiunta o la modifica di un componente non dovrebbe influire su tutti gli altri).
È praticamente impossibile costruire un'applicazione che non richieda modifiche o nuove funzionalità. Avremo costantemente bisogno di aggiungere nuove parti per aiutare la nostra idea a stare al passo con i tempi. È qui che entra in gioco la scalabilità. La scalabilità è essenzialmente l'estensione dell'applicazione, l'aggiunta di nuove funzionalità e l'utilizzo di più risorse (o, in altre parole, con un carico maggiore). In altre parole, per facilitare l'aggiunta di nuove logiche, ci si attiene ad alcune regole, come ridurre l'accoppiamento del sistema aumentando la modularità.Regole di programmazione: dalla creazione di un sistema al lavoro con gli oggetti - 2

Fonte immagine

Fasi di progettazione di un sistema

  1. Sistema informatico. Progettare l'applicazione in generale.
  2. Divisione in sottosistemi/pacchetti. Definire parti logicamente distinte e definire le regole per l'interazione tra di esse.
  3. Divisione dei sottosistemi in classi. Dividere le parti del sistema in classi e interfacce specifiche e definire l'interazione tra di esse.
  4. Divisione delle classi in metodi. Creare una definizione completa dei metodi necessari per una classe, in base alla responsabilità assegnata.
  5. Progettazione del metodo. Creare una definizione dettagliata della funzionalità dei singoli metodi.
Di solito gli sviluppatori ordinari gestiscono questo progetto, mentre l'architetto dell'applicazione gestisce i punti sopra descritti.

Principi generali e concetti di progettazione di sistemi

Inizializzazione pigra. In questo linguaggio di programmazione, l'applicazione non perde tempo a creare un oggetto fino a quando non viene effettivamente utilizzato. Ciò velocizza il processo di inizializzazione e riduce il carico sul Garbage Collector. Detto questo, non dovresti esagerare, perché ciò può violare il principio di modularità. Forse vale la pena spostare tutte le istanze di costruzione in una parte specifica, ad esempio il metodo principale o in una classe di fabbrica . Una caratteristica del buon codice è l'assenza di codice standard ripetitivo. Di norma, tale codice viene inserito in una classe separata in modo che possa essere richiamato quando necessario.

AOP

Vorrei anche notare la programmazione orientata agli aspetti. Questo paradigma di programmazione riguarda l'introduzione di una logica trasparente. Cioè, il codice ripetitivo viene inserito in classi (aspetti) e viene chiamato quando vengono soddisfatte determinate condizioni. Ad esempio, quando si chiama un metodo con un nome specifico o si accede a una variabile di un tipo specifico. A volte gli aspetti possono creare confusione, poiché non è immediatamente chiaro da dove viene chiamato il codice, ma questa è comunque una funzionalità molto utile. Soprattutto durante la memorizzazione nella cache o la registrazione. Aggiungiamo questa funzionalità senza aggiungere ulteriore logica alle classi ordinarie. Le quattro regole di Kent Beck per un'architettura semplice:
  1. Espressività: l'intento di una lezione dovrebbe essere chiaramente espresso. Ciò si ottiene attraverso una denominazione corretta, dimensioni ridotte e adesione al principio di responsabilità singola (che considereremo più in dettaglio di seguito).
  2. Numero minimo di classi e metodi - Nel tuo desiderio di rendere le classi il più piccole e focalizzate possibile, puoi andare troppo lontano (risultando nell'anti-pattern della chirurgia del fucile da caccia). Questo principio richiede di mantenere il sistema compatto e di non andare troppo lontano, creando una classe separata per ogni possibile azione.
  3. Nessuna duplicazione: il codice duplicato, che crea confusione ed è un'indicazione di una progettazione del sistema non ottimale, viene estratto e spostato in una posizione separata.
  4. Esegue tutti i test: un sistema che supera tutti i test è gestibile. Qualsiasi modifica potrebbe causare il fallimento di un test, rivelandoci che il nostro cambiamento nella logica interna di un metodo ha cambiato anche il comportamento del sistema in modi inaspettati.

SOLIDO

Quando si progetta un sistema, vale la pena considerare i ben noti principi SOLID:

S (responsabilità singola), O (aperto-chiuso), L (sostituzione di Liskov), I (segregazione dell'interfaccia), D (inversione delle dipendenze).

Non ci soffermeremo su ogni singolo principio. Sarebbe un po' oltre lo scopo di questo articolo, ma puoi leggere di più qui .

Interfaccia

Forse uno dei passaggi più importanti nella creazione di una classe ben progettata è la creazione di un'interfaccia ben progettata che rappresenti una buona astrazione, nascondendo i dettagli di implementazione della classe e presentando contemporaneamente un gruppo di metodi chiaramente coerenti tra loro. Diamo un'occhiata più da vicino a uno dei principi SOLID: la segregazione dell'interfaccia: i client (classi) non dovrebbero implementare metodi non necessari che non useranno. In altre parole, se stiamo parlando di creare un'interfaccia con il minor numero di metodi volti a svolgere l'unico lavoro dell'interfaccia (che penso sia molto simile al principio di responsabilità singola), è meglio invece crearne un paio di metodi più piccoli di un'interfaccia gonfia. Fortunatamente, una classe può implementare più di un'interfaccia. Ricorda di nominare correttamente le tue interfacce: il nome dovrebbe riflettere il compito assegnato nel modo più accurato possibile. E, naturalmente, più breve è, meno confusione causerà. I commenti alla documentazione sono generalmente scritti a livello di interfaccia. Questi commenti forniscono dettagli su cosa dovrebbe fare ciascun metodo, quali argomenti accetta e cosa restituirà.

Classe

Regole di codifica: dalla creazione di un sistema al lavoro con gli oggetti - 3

Fonte immagine

Diamo un'occhiata a come le classi sono organizzate internamente. O meglio, alcune prospettive e regole da seguire quando si scrivono le lezioni. Di norma, una classe dovrebbe iniziare con un elenco di variabili in un ordine specifico:
  1. costanti statiche pubbliche;
  2. costanti statiche private;
  3. variabili di istanza private.
Poi vengono i vari costruttori, in ordine da quelli con il minor numero di argomenti a quelli con il maggior numero di argomenti. Sono seguiti da metodi dal più pubblico al più privato. In generale, i metodi privati ​​che nascondono l'implementazione di alcune funzionalità che vogliamo limitare sono in fondo.

Dimensione della classe

Ora vorrei parlare delle dimensioni delle classi. Ricordiamo uno dei principi SOLIDI: il principio di responsabilità unica. Afferma che ogni oggetto ha un solo scopo (responsabilità) e la logica di tutti i suoi metodi mira a realizzarlo. Questo ci dice di evitare classi grandi e gonfie (che in realtà sono l'anti-pattern dell'oggetto Dio), e se abbiamo molti metodi con tutti i tipi di logica diversa stipati in una classe, dobbiamo pensare a scomporla in un coppia di parti logiche (classi). Questo, a sua volta, aumenterà la leggibilità del codice, poiché non ci vorrà molto per capire lo scopo di ciascun metodo se conosciamo lo scopo approssimativo di una data classe. Inoltre, tieni d'occhio il nome della classe, che dovrebbe riflettere la logica che contiene. Ad esempio, se abbiamo una classe con più di 20 parole nel suo nome, dobbiamo pensare al refactoring. Qualsiasi classe che si rispetti non dovrebbe avere così tante variabili interne. In effetti, ogni metodo funziona con uno o pochi di essi, causando molta coesione all'interno della classe (che è esattamente come dovrebbe essere, poiché la classe dovrebbe essere un insieme unificato). Di conseguenza, l'aumento della coesione di una classe porta a una riduzione delle dimensioni della classe e, naturalmente, il numero delle classi aumenta. Questo è fastidioso per alcune persone, poiché è necessario esaminare maggiormente i file di classe per vedere come funziona un'attività specifica di grandi dimensioni. Inoltre, ogni classe è un piccolo modulo che dovrebbe essere minimamente correlato agli altri. Questo isolamento riduce il numero di modifiche che dobbiamo apportare quando si aggiunge ulteriore logica a una classe. ogni metodo funziona con uno o pochi di essi, causando molta coesione all'interno della classe (che è esattamente come dovrebbe essere, poiché la classe dovrebbe essere un insieme unificato). Di conseguenza, l'aumento della coesione di una classe porta a una riduzione delle dimensioni della classe e, naturalmente, il numero delle classi aumenta. Questo è fastidioso per alcune persone, poiché è necessario esaminare maggiormente i file di classe per vedere come funziona un'attività specifica di grandi dimensioni. Inoltre, ogni classe è un piccolo modulo che dovrebbe essere minimamente correlato agli altri. Questo isolamento riduce il numero di modifiche che dobbiamo apportare quando si aggiunge ulteriore logica a una classe. ogni metodo funziona con uno o pochi di essi, causando molta coesione all'interno della classe (che è esattamente come dovrebbe essere, poiché la classe dovrebbe essere un insieme unificato). Di conseguenza, l'aumento della coesione di una classe porta a una riduzione delle dimensioni della classe e, naturalmente, il numero delle classi aumenta. Questo è fastidioso per alcune persone, poiché è necessario esaminare maggiormente i file di classe per vedere come funziona un'attività specifica di grandi dimensioni. Inoltre, ogni classe è un piccolo modulo che dovrebbe essere minimamente correlato agli altri. Questo isolamento riduce il numero di modifiche che dobbiamo apportare quando si aggiunge ulteriore logica a una classe. La coesione di s porta a una riduzione delle dimensioni delle classi e, naturalmente, il numero delle classi aumenta. Questo è fastidioso per alcune persone, poiché è necessario esaminare maggiormente i file di classe per vedere come funziona un'attività specifica di grandi dimensioni. Inoltre, ogni classe è un piccolo modulo che dovrebbe essere minimamente correlato agli altri. Questo isolamento riduce il numero di modifiche che dobbiamo apportare quando si aggiunge ulteriore logica a una classe. La coesione di s porta a una riduzione delle dimensioni delle classi e, naturalmente, il numero delle classi aumenta. Questo è fastidioso per alcune persone, poiché è necessario esaminare maggiormente i file di classe per vedere come funziona un'attività specifica di grandi dimensioni. Inoltre, ogni classe è un piccolo modulo che dovrebbe essere minimamente correlato agli altri. Questo isolamento riduce il numero di modifiche che dobbiamo apportare quando si aggiunge ulteriore logica a una classe.

Oggetti

Incapsulamento

Qui parleremo prima di un principio OOP: l'incapsulamento. Nascondere l'implementazione non equivale a creare un metodo per isolare le variabili (limitando sconsideratamente l'accesso tramite singoli metodi, getter e setter, il che non va bene, poiché l'intero punto di incapsulamento è perso). Nascondere l'accesso ha lo scopo di formare astrazioni, ovvero la classe fornisce metodi concreti condivisi che usiamo per lavorare con i nostri dati. E l'utente non ha bisogno di sapere esattamente come stiamo lavorando con questi dati: funziona e basta.

Legge di Demetra

Possiamo anche considerare la Legge di Demetra: è un piccolo insieme di regole che aiuta a gestire la complessità a livello di classe e metodo. Supponiamo di avere un oggetto Car e che abbia un metodo move(Object arg1, Object arg2) . Secondo la Legge di Demetra, questo metodo si limita a chiamare:
  • metodi dell'oggetto Car stesso (in altre parole, l'oggetto "questo");
  • metodi di oggetti creati all'interno del metodo move ;
  • metodi di oggetti passati come argomenti ( arg1 , arg2 );
  • metodi degli oggetti Car interni (di nuovo, "questo").
In altre parole, la Legge di Demetra è qualcosa di simile a ciò che i genitori potrebbero dire a un bambino: "puoi parlare con i tuoi amici, ma non con gli estranei".

Struttura dati

Una struttura dati è una raccolta di elementi correlati. Quando si considera un oggetto come una struttura di dati, esiste un insieme di elementi di dati su cui operano i metodi. L'esistenza di questi metodi è implicitamente assunta. Cioè, una struttura dati è un oggetto il cui scopo è archiviare e lavorare con (elaborare) i dati memorizzati. La differenza fondamentale rispetto a un oggetto normale è che un oggetto ordinario è una raccolta di metodi che operano su elementi di dati che si presume implicitamente esistano. Capisci? L'aspetto principale di un oggetto ordinario sono i metodi. Le variabili interne facilitano il loro corretto funzionamento. Ma in una struttura di dati, i metodi sono lì per supportare il tuo lavoro con gli elementi di dati memorizzati, che qui sono fondamentali. Un tipo di struttura dati è un oggetto di trasferimento dati (DTO). Questa è una classe con variabili pubbliche e nessun metodo (o solo metodi per la lettura/scrittura) che viene utilizzata per trasferire dati quando si lavora con database, si analizzano messaggi da socket, ecc. I dati non vengono solitamente archiviati in tali oggetti per un lungo periodo. Viene quasi immediatamente convertito nel tipo di entità su cui funziona la nostra applicazione. Un'entità, a sua volta, è anch'essa una struttura dati, ma il suo scopo è partecipare alla logica aziendale a vari livelli dell'applicazione. Lo scopo di un DTO è trasportare dati da/verso l'applicazione. Esempio di DTO: è anche una struttura dati, ma il suo scopo è partecipare alla logica aziendale a vari livelli dell'applicazione. Lo scopo di un DTO è trasportare dati da/verso l'applicazione. Esempio di DTO: è anche una struttura dati, ma il suo scopo è partecipare alla logica aziendale a vari livelli dell'applicazione. Lo scopo di un DTO è trasportare dati da/verso l'applicazione. Esempio di DTO:

@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Tutto sembra abbastanza chiaro, ma qui apprendiamo dell'esistenza degli ibridi. Gli ibridi sono oggetti che dispongono di metodi per gestire la logica importante, memorizzare elementi interni e includere anche metodi di accesso (get/set). Tali oggetti sono disordinati e rendono difficile l'aggiunta di nuovi metodi. Dovresti evitarli, perché non è chiaro a cosa servano: memorizzare elementi o eseguire logica?

Principi di creazione di variabili

Riflettiamo un po' sulle variabili. Più specificamente, pensiamo a quali principi si applicano durante la loro creazione:
  1. Idealmente, dovresti dichiarare e inizializzare una variabile appena prima di usarla (non crearne una e dimenticartene).
  2. Quando possibile, dichiarare le variabili come finali per evitare che il loro valore cambi dopo l'inizializzazione.
  3. Non dimenticare le variabili contatore, che di solito usiamo in una sorta di ciclo for . Cioè, non dimenticare di azzerarli. Altrimenti, tutta la nostra logica potrebbe rompersi.
  4. Dovresti provare a inizializzare le variabili nel costruttore.
  5. Se è possibile scegliere tra l'utilizzo di un oggetto con un riferimento o senza ( new SomeObject() ), optare per senza, poiché dopo che l'oggetto è stato utilizzato verrà eliminato durante il successivo ciclo di raccolta dei rifiuti e le sue risorse non verranno sprecate.
  6. Mantieni la durata di una variabile (la distanza tra la creazione della variabile e l'ultima volta che vi si fa riferimento) il più breve possibile.
  7. Inizializza le variabili utilizzate in un ciclo appena prima del ciclo, non all'inizio del metodo che contiene il ciclo.
  8. Inizia sempre con l'ambito più limitato ed espandi solo quando necessario (dovresti provare a rendere una variabile il più locale possibile).
  9. Utilizzare ciascuna variabile per un solo scopo.
  10. Evita le variabili con uno scopo nascosto, ad esempio una variabile suddivisa tra due compiti: questo significa che il suo tipo non è adatto per risolverne uno.

Metodi

Regole di programmazione: dalla creazione di un sistema al lavoro con gli oggetti - 4

dal film "Star Wars: Episodio III - La vendetta dei Sith" (2005)

Procediamo direttamente all'implementazione della nostra logica, cioè ai metodi.
  1. Regola n. 1: compattezza. Idealmente, un metodo non dovrebbe superare le 20 righe. Ciò significa che se un metodo pubblico "si gonfia" in modo significativo, è necessario pensare a separare la logica e spostarla in metodi privati ​​separati.

  2. Regola n. 2 — if , else , while e altre istruzioni non dovrebbero avere blocchi fortemente nidificati: molti annidamenti riducono significativamente la leggibilità del codice. Idealmente, non dovresti avere più di due blocchi {} nidificati .

    Ed è anche desiderabile mantenere il codice in questi blocchi compatto e semplice.

  3. Regola n. 3: un metodo dovrebbe eseguire solo un'operazione. Cioè, se un metodo esegue tutti i tipi di logica complessa, lo suddividiamo in sottometodi. Di conseguenza, il metodo stesso sarà una facciata il cui scopo è chiamare tutte le altre operazioni nell'ordine corretto.

    Ma cosa succede se l'operazione sembra troppo semplice per essere inserita in un metodo separato? È vero, a volte può sembrare di sparare con un cannone ai passeri, ma i piccoli metodi offrono una serie di vantaggi:

    • Migliore comprensione del codice;
    • I metodi tendono a diventare più complessi con il progredire dello sviluppo. Se un metodo è semplice all'inizio, sarà un po' più facile complicarne la funzionalità;
    • I dettagli di implementazione sono nascosti;
    • Riutilizzo del codice più semplice;
    • Codice più affidabile.

  4. La regola stepdown: il codice dovrebbe essere letto dall'alto verso il basso: più in basso si legge, più si approfondisce la logica. E viceversa, più in alto si va, più astratti sono i metodi. Ad esempio, le istruzioni switch sono piuttosto non compatte e indesiderabili, ma se non puoi evitare di utilizzare uno switch, dovresti provare a spostarlo il più in basso possibile, ai metodi di livello più basso.

  5. Argomenti del metodo: qual è il numero ideale? Idealmente, nessuno :) Ma succede davvero? Detto questo, dovresti cercare di avere meno argomenti possibili, perché meno ce ne sono, più facile è usare un metodo e più facile è testarlo. In caso di dubbio, prova ad anticipare tutti gli scenari per l'utilizzo del metodo con un numero elevato di parametri di input.

  6. Inoltre, sarebbe utile separare i metodi che hanno un flag booleano come parametro di input, poiché questo implica di per sé che il metodo esegue più di un'operazione (se vero, allora fai una cosa; se falso, allora fai un altro). Come ho scritto sopra, questo non va bene e dovrebbe essere evitato se possibile.

  7. Se un metodo ha un numero elevato di parametri di input (un estremo è 7, ma dovresti davvero iniziare a pensare dopo 2-3), alcuni degli argomenti dovrebbero essere raggruppati in un oggetto separato.

  8. Se sono presenti diversi metodi simili (sovraccarico), parametri simili devono essere passati nello stesso ordine: questo migliora la leggibilità e l'usabilità.

  9. Quando passi parametri ad un metodo, devi essere sicuro che siano tutti usati, altrimenti perché ti servono? Taglia tutti i parametri inutilizzati dall'interfaccia e falla finita.

  10. try/catch non ha un bell'aspetto in natura, quindi sarebbe una buona idea spostarlo in un metodo intermedio separato (un metodo per gestire le eccezioni):

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

Ho parlato di codice duplicato sopra, ma lasciatemelo ripetere ancora una volta: se abbiamo un paio di metodi con codice ripetuto, dobbiamo spostarlo in un metodo separato. Ciò renderà sia il metodo che la classe più compatti. Non dimenticare le regole che governano i nomi: i dettagli su come denominare correttamente classi, interfacce, metodi e variabili saranno discussi nella parte successiva dell'articolo. Ma è tutto quello che ho per te oggi.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION