JavaRush /Java Blog /Random-IT /Modelli di progettazione in Java
Viacheslav
Livello 3

Modelli di progettazione in Java

Pubblicato nel gruppo Random-IT
I modelli o i modelli di progettazione sono una parte spesso trascurata del lavoro di uno sviluppatore, rendendo difficile la manutenzione del codice e l'adattamento ai nuovi requisiti. Ti suggerisco di guardare cos'è e come viene utilizzato nel JDK. Naturalmente, tutti i modelli di base in una forma o nell'altra esistono intorno a noi da molto tempo. Vediamoli in questa recensione.
Modelli di progettazione in Java - 1
Contenuto:

Modelli

Uno dei requisiti più comuni per i posti vacanti è la “Conoscenza dei modelli”. Prima di tutto, vale la pena rispondere a una semplice domanda: "Cos'è un Design Pattern?" Il modello è tradotto dall'inglese come "modello". Cioè, questo è un certo modello in base al quale facciamo qualcosa. Lo stesso vale nella programmazione. Esistono alcune migliori pratiche e approcci consolidati per risolvere i problemi comuni. Ogni programmatore è un architetto. Anche quando crei solo poche classi o anche una sola, dipende da te quanto a lungo il codice potrà sopravvivere in caso di mutevoli requisiti, quanto sarà conveniente essere utilizzato da altri. Ed è qui che la conoscenza dei modelli aiuterà, perché... Ciò ti consentirà di capire rapidamente come scrivere al meglio il codice senza riscriverlo. Come sai, i programmatori sono persone pigre ed è più facile scrivere qualcosa bene subito che rifarlo più volte) I pattern possono anche sembrare simili agli algoritmi. Ma hanno una differenza. L'algoritmo è costituito da passaggi specifici che descrivono le azioni necessarie. I modelli descrivono solo l'approccio, ma non descrivono le fasi di implementazione. I modelli sono diversi, perché... risolvere diversi problemi. Solitamente si distinguono le seguenti categorie:
  • Generativo

    Questi modelli risolvono il problema di rendere flessibile la creazione di oggetti

  • Strutturale

    Questi modelli risolvono il problema di costruire efficacemente connessioni tra oggetti

  • comportamentale

    Questi modelli risolvono il problema dell'interazione efficace tra gli oggetti

Per considerare degli esempi, suggerisco di utilizzare il compilatore di codice online repl.it.
Modelli di progettazione in Java - 2

Modelli creazionali

Cominciamo dall'inizio del ciclo di vita degli oggetti, con la creazione degli oggetti. I modelli generativi aiutano a creare oggetti in modo più conveniente e forniscono flessibilità in questo processo. Uno dei più famosi è " Builder ". Questo modello ti consente di creare oggetti complessi passo dopo passo. In Java, l'esempio più famoso è StringBuilder:
class Main {
  public static void main(String[] args) {
    StringBuilder builder = new StringBuilder();
    builder.append("Hello");
    builder.append(',');
    builder.append("World!");
    System.out.println(builder.toString());
  }
}
Un altro approccio ben noto alla creazione di un oggetto consiste nello spostare la creazione in un metodo separato. Questo metodo diventa, per così dire, una fabbrica di oggetti. Ecco perché il modello si chiama " Metodo Factory ". In Java, ad esempio, il suo effetto può essere visto nella classe java.util.Calendar. La classe stessa Calendarè astratta e per crearla viene utilizzato il metodo getInstance:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime());
    System.out.println(calendar.getClass().getCanonicalName());
  }
}
Ciò è spesso dovuto al fatto che la logica dietro la creazione degli oggetti può essere complessa. Ad esempio, nel caso precedente, accediamo alla classe base Calendare viene creata una classe GregorianCalendar. Se guardiamo il costruttore, possiamo vedere che vengono create diverse implementazioni a seconda delle condizioni Calendar. Ma a volte un metodo di fabbrica non è sufficiente. A volte è necessario creare oggetti diversi in modo che si adattino insieme. Un altro modello ci aiuterà in questo: " Fabbrica astratta ". E poi dobbiamo creare diverse fabbriche in un unico posto. Allo stesso tempo, il vantaggio è che i dettagli di implementazione non sono importanti per noi, ad es. non importa quale fabbrica specifica otteniamo. La cosa principale è che crei le giuste implementazioni. Super esempio:
Modelli di progettazione in Java - 3
Cioè, a seconda dell'ambiente (sistema operativo), otterremo una determinata fabbrica che creerà elementi compatibili. In alternativa all'approccio di creare attraverso qualcun altro, possiamo utilizzare il modello " Prototipo ". La sua essenza è semplice: i nuovi oggetti vengono creati a immagine e somiglianza di oggetti già esistenti, ad es. secondo il loro prototipo. In Java, tutti si sono imbattuti in questo modello: questo è l'uso di un'interfaccia java.lang.Cloneable:
class Main {
  public static void main(String[] args) {
    class CloneObject implements Cloneable {
      @Override
      protected Object clone() throws CloneNotSupportedException {
        return new CloneObject();
      }
    }
    CloneObject obj = new CloneObject();
    try {
      CloneObject pattern = (CloneObject) obj.clone();
    } catch (CloneNotSupportedException e) {
      //Do something
    }
  }
}
Come puoi vedere, il chiamante non sa come funziona il file clone. Cioè, creare un oggetto basato su un prototipo è responsabilità dell'oggetto stesso. Ciò è utile perché non vincola l'utente all'implementazione dell'oggetto modello. Bene, l'ultimo in questo elenco è il modello "Singleton". Il suo scopo è semplice: fornire una singola istanza dell'oggetto per l'intera applicazione. Questo modello è interessante perché spesso mostra problemi di multithreading. Per uno sguardo più approfondito, consulta questi articoli:
Modelli di progettazione in Java - 4

Modelli strutturali

Con la creazione degli oggetti tutto è diventato più chiaro. Ora è il momento di esaminare i modelli strutturali. Il loro obiettivo è costruire gerarchie di classi e relazioni facili da supportare. Uno dei primi e più noti modelli è “ Deputy ” (Proxy). Il proxy ha la stessa interfaccia dell'oggetto reale, quindi non fa differenza che il client lavori tramite il proxy o direttamente. L'esempio più semplice è java.lang.reflect.Proxy :
import java.util.*;
import java.lang.reflect.*;
class Main {
  public static void main(String[] arguments) {
    final Map<String, String> original = new HashMap<>();
    InvocationHandler proxy = (obj, method, args) -> {
      System.out.println("Invoked: " + method.getName());
      return method.invoke(original, args);
    };
    Map<String, String> proxyInstance = (Map) Proxy.newProxyInstance(
        original.getClass().getClassLoader(),
        original.getClass().getInterfaces(),
        proxy);
    proxyInstance.put("key", "value");
    System.out.println(proxyInstance.get("key"));
  }
}
Come puoi vedere, nell'esempio abbiamo l'originale: questo è quello HashMapche implementa l'interfaccia Map. Successivamente creiamo un proxy che sostituisce quello originale HashMapper la parte client, che chiama i metodi putand get, aggiungendo la nostra logica durante la chiamata. Come possiamo vedere, l'interazione nel modello avviene attraverso le interfacce. Ma a volte un sostituto non basta. E poi si può usare il modello " Decoratore ". Un decoratore è anche chiamato wrapper o wrapper. Proxy e decorator sono molto simili, ma se guardi l'esempio vedrai la differenza:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    List<String> list = new ArrayList<>();
    List<String> decorated = Collections.checkedList(list, String.class);
    decorated.add("2");
    list.add("3");
    System.out.println(decorated);
  }
}
A differenza di un proxy, un decoratore si avvolge attorno a qualcosa che viene passato come input. Un proxy può sia accettare ciò che deve essere proxy sia anche gestire la vita dell'oggetto proxy (ad esempio, creare un oggetto proxy). C'è un altro modello interessante: " Adattatore ". È simile a un decoratore: il decoratore prende un oggetto come input e restituisce un wrapper su questo oggetto. La differenza è che l'obiettivo non è modificare la funzionalità, ma adattare un'interfaccia all'altra. Java ha un esempio molto chiaro di questo:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    String[] array = {"One", "Two", "Three"};
    List<String> strings = Arrays.asList(array);
    strings.set(0, "1");
    System.out.println(Arrays.toString(array));
  }
}
All'input abbiamo un array. Successivamente, creiamo un adattatore che porta l'array all'interfaccia List. Quando lavoriamo con esso, stiamo effettivamente lavorando con un array. Pertanto, l'aggiunta di elementi non funzionerà, perché... L'array originale non può essere modificato. E in questo caso otterremo UnsupportedOperationException. Il prossimo approccio interessante allo sviluppo della struttura delle classi è il modello Composite . È interessante il fatto che un certo insieme di elementi che utilizzano un'interfaccia sono disposti in una certa gerarchia ad albero. Chiamando un metodo su un elemento genitore, otteniamo una chiamata a questo metodo su tutti gli elementi figli necessari. Un ottimo esempio di questo modello è l'interfaccia utente (che sia java.awt o JSF):
import java.awt.*;
class Main {
  public static void main(String[] arguments) {
    Container container = new Container();
    Component component = new java.awt.Component(){};
    System.out.println(component.getComponentOrientation().isLeftToRight());
    container.add(component);
    container.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
    System.out.println(component.getComponentOrientation().isLeftToRight());
  }
}
Come possiamo vedere, abbiamo aggiunto un componente al contenitore. E poi abbiamo chiesto al contenitore di applicare il nuovo orientamento dei componenti. E il contenitore, sapendo di quali componenti è composto, ha delegato l'esecuzione di questo comando a tutti i componenti figlio. Un altro modello interessante è il modello “ Ponte ”. Si chiama così perché descrive una connessione o un ponte tra due diverse gerarchie di classi. Una di queste gerarchie è considerata un'astrazione e l'altra un'implementazione. Ciò è evidenziato perché l'astrazione stessa non esegue azioni, ma delega questa esecuzione all'implementazione. Questo modello viene spesso utilizzato quando sono presenti classi "di controllo" e diversi tipi di classi "piattaforma" (ad esempio Windows, Linux e così via). Con questo approccio, una di queste gerarchie (astrazione) riceverà un riferimento agli oggetti di un'altra gerarchia (implementazione) e delegherà loro il lavoro principale. Poiché tutte le implementazioni seguiranno un'interfaccia comune, potranno essere scambiate all'interno dell'astrazione. In Java, un chiaro esempio di ciò è java.awt:
Modelli di progettazione in Java - 5
Per ulteriori informazioni, consultare l'articolo " Modelli in Java AWT ". Tra gli schemi strutturali vorrei segnalare anche lo schema “ Facciata ”. La sua essenza è nascondere la complessità dell'utilizzo delle librerie/framework dietro questa API dietro un'interfaccia comoda e concisa. Ad esempio, puoi utilizzare JSF o EntityManager da JPA come esempio. Esiste anche un altro modello chiamato " Flyweight ". La sua essenza è che se oggetti diversi hanno lo stesso stato, allora può essere generalizzato e memorizzato non in ciascun oggetto, ma in un unico posto. E poi ogni oggetto potrà fare riferimento a una parte comune, il che ridurrà i costi di memoria per l'archiviazione. Questo modello spesso implica la pre-memorizzazione nella cache o il mantenimento di un pool di oggetti. È interessante notare che conosciamo anche questo schema fin dall'inizio:
Modelli di progettazione in Java - 6
Per la stessa analogia, qui è possibile includere un pool di stringhe. Puoi leggere l'articolo su questo argomento: " Flyweight Design Pattern ".
Modelli di progettazione in Java - 7

Modelli comportamentali

Quindi, abbiamo capito come creare oggetti e come organizzare le connessioni tra le classi. La cosa più interessante rimasta è fornire flessibilità nel modificare il comportamento degli oggetti. E i modelli comportamentali ci aiuteranno in questo. Uno dei modelli citati più frequentemente è il modello " Strategia ". È qui che inizia lo studio dei pattern nel libro " Head First. Design Patterns ". Utilizzando il pattern “Strategia” possiamo memorizzare all’interno di un oggetto il modo in cui eseguiremo l’azione, ovvero l'oggetto al suo interno memorizza una strategia che può essere modificata durante l'esecuzione del codice. Questo è uno schema che utilizziamo spesso quando utilizziamo un comparatore:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Comparator<String> comparator = Comparator.comparingInt(String::length);
    Set dataSet = new TreeSet(comparator);
    dataSet.addAll(data);
    System.out.println("Dataset : " + dataSet);
  }
}
Prima di noi - TreeSet. Ha il comportamento di TreeSetmantenere l'ordine degli elementi, cioè li ordina (poiché è un SortedSet). Questo comportamento ha una strategia predefinita, che vediamo nel JavaDoc: ordinamento in "ordine naturale" (per le stringhe, questo è l'ordine lessicografico). Ciò accade se si utilizza un costruttore senza parametri. Ma se vogliamo cambiare strategia, possiamo passare Comparator. In questo esempio, possiamo creare il nostro set come new TreeSet(comparator), quindi l'ordine di memorizzazione degli elementi (strategia di memorizzazione) cambierà in quello specificato nel comparatore. È interessante notare che esiste quasi lo stesso modello chiamato " Stato ". Il modello "Stato" dice che se abbiamo un comportamento nell'oggetto principale che dipende dallo stato di questo oggetto, allora possiamo descrivere lo stato stesso come un oggetto e modificare lo stato dell'oggetto. E delegare le chiamate dall'oggetto principale allo stato. Un altro modello a noi noto studiando le basi del linguaggio Java è il modello “ Command ”. Questo modello di progettazione suggerisce che comandi diversi possono essere rappresentati come classi diverse. Questo modello è molto simile al modello Strategia. Ma nel pattern Strategy, stavamo ridefinendo il modo in cui un'azione specifica sarebbe stata eseguita (ad esempio, ordinando TreeSet). Nel modello "Comando", ridefiniamo quale azione verrà eseguita. Il comando pattern è con noi ogni giorno quando usiamo i thread:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Runnable command = () -> {
      System.out.println("Command action");
    };
    Thread th = new Thread(command);
    th.start();
  }
}
Come puoi vedere, comando definisce un'azione o un comando che verrà eseguito in un nuovo thread. Vale anche la pena considerare il modello della “ Catena di responsabilità ”. Anche questo modello è molto semplice. Questo modello dice che se qualcosa deve essere elaborato, puoi raccogliere i gestori in una catena. Ad esempio, questo modello viene spesso utilizzato nei server Web. All'ingresso, il server ha qualche richiesta da parte dell'utente. Questa richiesta passa poi attraverso la catena di elaborazione. Questa catena di gestori include filtri (ad esempio, non accetta richieste da una lista nera di indirizzi IP), gestori di autenticazione (consenti solo utenti autorizzati), un gestore di intestazione della richiesta, un gestore di memorizzazione nella cache, ecc. Ma c'è un esempio più semplice e comprensibile in Java java.util.logging:
import java.util.logging.*;
class Main {
  public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());
    ConsoleHandler consoleHandler = new ConsoleHandler(){
		@Override
            public void publish(LogRecord record) {
                System.out.println("LogRecord обработан");
            }
        };
    logger.addHandler(consoleHandler);
    logger.info("test");
  }
}
Come puoi vedere, i gestori vengono aggiunti all'elenco dei gestori dei logger. Quando un logger riceve un messaggio per l'elaborazione, ciascuno di questi messaggi passa attraverso una catena di gestori (da logger.getHandlers) per quel logger. Un altro modello che vediamo ogni giorno è “ Iterator ”. La sua essenza è separare una raccolta di oggetti (ovvero una classe che rappresenta una struttura dati. Ad esempio List) e l'attraversamento di questa raccolta.
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Iterator<String> iterator = data.iterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}
Come puoi vedere, l'iteratore non fa parte della raccolta, ma è rappresentato da una classe separata che attraversa la raccolta. L'utente dell'iteratore potrebbe non sapere nemmeno su quale raccolta sta eseguendo l'iterazione, ad es. che collezione sta visitando? Vale la pena considerare anche il modello Visitor . Il modello visitatore è molto simile al modello iteratore. Questo modello ti aiuta a bypassare la struttura degli oggetti ed eseguire azioni su questi oggetti. Differiscono piuttosto nel concetto. L'iteratore attraversa la raccolta in modo che al client che utilizza l'iteratore non importi cosa contenga la raccolta, sono importanti solo gli elementi nella sequenza. Il visitatore significa che esiste una certa gerarchia o struttura degli oggetti che visitiamo. Ad esempio, possiamo utilizzare l'elaborazione separata delle directory e l'elaborazione separata dei file. Java ha un'implementazione pronta all'uso di questo modello nel formato java.nio.file.FileVisitor:
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
class Main {
  public static void main(String[] args) {
    SimpleFileVisitor visitor = new SimpleFileVisitor() {
      @Override
      public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
        System.out.println("File:" + file.toString());
        return FileVisitResult.CONTINUE;
      }
    };
    Path pathSource = Paths.get(System.getProperty("java.io.tmpdir"));
    try {
      Files.walkFileTree(pathSource, visitor);
    } catch (AccessDeniedException e) {
      // skip
    } catch (IOException e) {
      // Do something
    }
  }
}
A volte è necessario che alcuni oggetti reagiscano ai cambiamenti in altri oggetti, quindi il modello "Osservatore" ci aiuterà . Il modo più conveniente è fornire un meccanismo di sottoscrizione che consenta ad alcuni oggetti di monitorare e rispondere agli eventi che si verificano in altri oggetti. Questo modello viene spesso utilizzato in vari ascoltatori e osservatori che reagiscono a eventi diversi. Come semplice esempio, possiamo ricordare l'implementazione di questo pattern dalla prima versione di JDK:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Observer observer = (obj, arg) -> {
      System.out.println("Arg: " + arg);
    };
    Observable target = new Observable(){
      @Override
      public void notifyObservers(Object arg) {
        setChanged();
        super.notifyObservers(arg);
      }
    };
    target.addObserver(observer);
    target.notifyObservers("Hello, World!");
  }
}
Esiste un altro modello comportamentale utile: il “ mediatore ”. È utile perché nei sistemi complessi aiuta a rimuovere la connessione tra diversi oggetti e delegare tutte le interazioni tra oggetti a qualche oggetto, che funge da intermediario. Una delle applicazioni più sorprendenti di questo pattern è Spring MVC, che utilizza questo pattern. Puoi leggere di più a riguardo qui: " Spring: Mediator Pattern ". Spesso puoi vedere lo stesso negli esempi java.util.Timer:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Timer mediator = new Timer("Mediator");
    TimerTask command = new TimerTask() {
      @Override
      public void run() {
        System.out.println("Command pattern");
        mediator.cancel();
      }
    };
    mediator.schedule(command, 1000);
  }
}
L'esempio assomiglia più a un modello di comando. E l'essenza del modello "Mediatore" è nascosta nell'implementazione di Timer'a. All'interno del timer c'è una coda di attività TaskQueue, c'è un thread TimerThread. Noi, come clienti di questa classe, non interagiamo con loro, ma interagiamo con Timerl'oggetto, che, in risposta alla nostra chiamata ai suoi metodi, accede ai metodi di altri oggetti di cui è intermediario. Esternamente può sembrare molto simile a "Facade". Ma la differenza è che quando si utilizza una facciata, i componenti non sanno che la facciata esiste e dialogano tra loro. E quando viene utilizzato "Mediatore", i componenti conoscono e utilizzano l'intermediario, ma non si contattano direttamente. Vale la pena considerare il modello “ Template Method ”, come si evince dal nome. La conclusione è che il codice è scritto in modo tale che agli utenti del codice (sviluppatori) venga fornito un modello di algoritmo, i cui passaggi possono essere ridefiniti. Ciò consente agli utenti del codice di non scrivere l'intero algoritmo, ma di pensare solo a come eseguire correttamente l'uno o l'altro passaggio di questo algoritmo. Ad esempio, Java ha una classe astratta AbstractListche definisce il comportamento di un iteratore tramite List. Tuttavia, l'iteratore stesso utilizza metodi foglia come: get, set, remove. Il comportamento di questi metodi è determinato dallo sviluppatore dei discendenti AbstractList. Pertanto, l'iteratore in AbstractList- è un modello per l'algoritmo per l'iterazione su un foglio. E gli sviluppatori di implementazioni specifiche AbstractListmodificano il comportamento di questa iterazione definendo il comportamento di passaggi specifici. L’ultimo dei pattern che analizziamo è il pattern “ Snapshot ” (Momento). La sua essenza è preservare un certo stato dell'oggetto con la capacità di ripristinare questo stato. L'esempio più riconoscibile del JDK è la serializzazione degli oggetti, ad es. java.io.Serializable. Diamo un'occhiata ad un esempio:
import java.io.*;
import java.util.*;
class Main {
  public static void main(String[] args) throws IOException {
    ArrayList<String> list = new ArrayList<>();
    list.add("test");
    // Save State
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
      out.writeObject(list);
    }
    // Load state
    byte[] bytes = stream.toByteArray();
    InputStream inputStream = new ByteArrayInputStream(bytes);
    try (ObjectInputStream in = new ObjectInputStream(inputStream)) {
      List<String> listNew = (List<String>) in.readObject();
      System.out.println(listNew.get(0));
    } catch (ClassNotFoundException e) {
      // Do something. Can't find class fpr saved state
    }
  }
}
Modelli di progettazione in Java - 8

Conclusione

Come abbiamo visto dalla recensione, esiste un'enorme varietà di modelli. Ognuno di loro risolve il proprio problema. E la conoscenza di questi modelli può aiutarti a capire in tempo come scrivere il tuo sistema in modo che sia flessibile, manutenibile e resistente al cambiamento. E infine, alcuni link per un approfondimento: #Viacheslav
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION