JavaRush /Java Blog /Random-IT /Regole per scrivere codice: dalla creazione di un sistema...

Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti

Pubblicato nel gruppo Random-IT
Buon pomeriggio a tutti: oggi vorrei parlarvi della scrittura corretta del codice. Quando ho iniziato a programmare per la prima volta, non era scritto chiaramente da nessuna parte che puoi scrivere in questo modo, e se scrivi in ​​questo modo, ti troverò e…. Di conseguenza, avevo molte domande in testa: come scrivere correttamente, quali principi dovrebbero essere seguiti in questa o quella sezione del programma, ecc. Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 1Ebbene, non tutti vogliono tuffarsi subito in libri come Clean Code, poiché contengono molto, ma all'inizio poco è chiaro. E quando finisci di leggere, puoi scoraggiare ogni desiderio di programmare. Quindi, in base a quanto sopra, oggi voglio fornirti una piccola guida (un insieme di piccoli consigli) per scrivere codice di livello superiore. In questo articolo esamineremo le regole e i concetti di base relativi alla creazione di un sistema e al lavoro con interfacce, classi e oggetti. La lettura di questo materiale non ti richiederà molto tempo e, spero, non ti farà annoiare. Andrò dall'alto verso il basso, cioè dalla struttura generale dell'applicazione ai dettagli più specifici. Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 2

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).
È praticamente impossibile creare un'applicazione che non richieda modifiche, senza aggiungere funzionalità. Avremo costantemente bisogno di introdurre nuovi elementi affinché la nostra idea possa stare al passo con i tempi. Ed è qui che entra in gioco la scalabilità . La scalabilità significa essenzialmente espandere l'applicazione, aggiungere nuove funzionalità, lavorare con più risorse (o, in altre parole, con più carico). Bisogna cioè rispettare alcune regole, come ad esempio ridurre l’accoppiamento del sistema aumentando la modularità, in modo che sia più semplice aggiungere nuove logiche.

Fasi di progettazione del sistema

  1. Sistema software : progettazione di un'applicazione in forma generale.
  2. Separazione in sottosistemi/pacchetti - definizione di parti logicamente separabili e definizione delle regole di interazione tra di loro.
  3. Dividere i sottosistemi in classi : dividere parti del sistema in classi e interfacce specifiche, nonché definire l'interazione tra loro.
  4. 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.
In genere, gli sviluppatori ordinari sono responsabili della progettazione e l'architetto dell'applicazione è responsabile degli elementi sopra descritti.

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
  1. 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).
  2. 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.
  3. Mancanza di duplicazione : il codice aggiuntivo che crea confusione è un segno di scarsa progettazione del sistema e viene spostato in un luogo separato.
  4. 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 .
SOLID Quando si progetta un sistema, vale la pena tenere conto dei noti principi di SOLID: S - responsabilità unica - il principio della responsabilità unica; O - aperto-chiuso - principio di apertura/chiusura; L - Sostituzione di Liskov - Principio di sostituzione di Barbara Liskov; I - segregazione dell'interfaccia - il principio della separazione dell'interfaccia; D - inversione di dipendenza - principio di inversione di dipendenza; Non ci soffermeremo su ciascun principio nello specifico (questo va un po' oltre lo scopo di questo articolo, ma puoi saperne di più qui)

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

Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 3Diamo 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:
  1. costanti statiche pubbliche;
  2. costanti statiche private;
  3. variabili di istanza private.
Successivamente ci sono vari costruttori in ordine da meno a più argomenti. Seguono i metodi dall'accesso più aperto a quelli più chiusi: di norma, i metodi privati ​​che nascondono l'implementazione di alcune funzionalità che vogliamo limitare si trovano in fondo.

Dimensione della classe

Ora vorrei parlare della dimensione della classe. Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 4Ricordiamo 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

Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 5

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 oggetto Care 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).
In altre parole, la legge di Demetra è qualcosa come una regola per bambini: puoi parlare con gli amici, ma non con gli estranei .

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

Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 6Pensiamo un po' alle variabili, o meglio, pensiamo a quali potrebbero essere i principi per crearle:
  1. Idealmente, dovresti dichiarare e inizializzare una variabile immediatamente prima di usarla (piuttosto che crearla 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 (di solito le usiamo in una sorta di loop for, cioè non dobbiamo dimenticare di reimpostarle, altrimenti potrebbe interrompere l'intera logica).
  4. Dovresti provare a inizializzare le variabili nel costruttore.
  5. 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.
  6. Rendi la durata delle variabili quanto più breve possibile (la distanza tra la creazione di una variabile e l'ultimo accesso).
  7. Inizializza le variabili utilizzate in un ciclo immediatamente prima del ciclo, anziché all'inizio del metodo che contiene il ciclo.
  8. Inizia sempre con l'ambito più limitato ed espandilo solo se necessario (dovresti provare a rendere la variabile il più locale possibile).
  9. Utilizzare ciascuna variabile per un solo scopo.
  10. Evita variabili con significati nascosti (la variabile è divisa tra due compiti, il che significa che il suo tipo non è adatto per risolverne uno).
Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 7

Metodi

Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 8Passiamo direttamente all'implementazione della nostra logica, ovvero ai metodi.
  1. 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.

  2. La seconda regola è che i blocchi nei comandi if, else, whilee 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.

  3. 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.
  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. Se esistono più metodi simili (sovraccarati) , allora parametri simili devono essere passati nello stesso ordine: questo migliora la leggibilità e l'usabilità.

  9. 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.

  10. try/catchNon 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();
        }
    }
Ho parlato della ripetizione del codice sopra, ma lo aggiungerò qui: se abbiamo un paio di metodi con parti di codice ripetute, dobbiamo spostarli in un metodo separato, il che aumenterà la compattezza sia del metodo che del codice classe. E non dimenticare i nomi corretti. Ti dirò i dettagli sulla corretta denominazione di classi, interfacce, metodi e variabili nella parte successiva dell'articolo. E per noi oggi è tutto. Regole per scrivere codice: dalla creazione di un sistema al lavoro con gli oggetti - 9Regole del codice: il potere della denominazione corretta, dei commenti positivi e negativi
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION