Introduzione al pattern di oggetti nulli

1. Panoramica

In questo breve tutorial, daremo uno sguardo al pattern oggetto nullo, un caso speciale del pattern strategico. Descriveremo il suo scopo e quando dovremmo effettivamente considerare di usarlo.

Come al solito, forniremo anche un semplice esempio di codice.

2. Pattern di oggetti nulli

Nella maggior parte dei linguaggi di programmazione orientati agli oggetti, non è consentito utilizzare un riferimento nullo . Ecco perché siamo spesso costretti a scrivere controlli nulli :

Command cmd = getCommand(); if (cmd != null) { cmd.execute(); }

A volte, se il numero di tali istruzioni if aumenta, il codice può diventare brutto, difficile da leggere e soggetto a errori. Questo è quando il pattern oggetto nullo può tornare utile.

Lo scopo del pattern oggetto nullo è ridurre al minimo questo tipo di controllo nullo . Invece, possiamo identificare il comportamento nullo e incapsularlo nel tipo previsto dal codice client. Il più delle volte no, una logica così neutra è molto semplice: non fare nulla. In questo modo non dobbiamo più occuparci della gestione speciale dei riferimenti nulli .

Possiamo semplicemente trattare gli oggetti nulli nello stesso modo in cui trattiamo qualsiasi altra istanza di un dato tipo che in realtà contiene una logica di business più sofisticata. Di conseguenza, il codice client rimane più pulito.

Poiché gli oggetti null non dovrebbero avere alcuno stato, non è necessario creare istanze identiche più volte. Pertanto, implementeremo spesso oggetti nulli come singleton .

3. Diagramma UML del modello oggetto nullo

Diamo un'occhiata al modello visivamente:

Come possiamo vedere, possiamo identificare i seguenti partecipanti:

  • Il client richiede un'istanza di AbstractObject
  • AbstractObject definisce il contratto che il client si aspetta - può anche contenere una logica condivisa per le classi di implementazione
  • RealObject implementa AbstractObject e fornisce un comportamento reale
  • NullObject implementa AbstractObject e fornisce un comportamento neutro

4. Implementazione

Ora che abbiamo un'idea chiara della teoria, diamo un'occhiata a un esempio.

Immagina di avere un'applicazione router di messaggi. A ogni messaggio dovrebbe essere assegnata una priorità valida. Il nostro sistema dovrebbe instradare i messaggi ad alta priorità a un gateway SMS mentre i messaggi con priorità media dovrebbero essere instradati a una coda JMS.

Di tanto in tanto, tuttavia, potrebbero arrivare messaggi con priorità "non definita" o vuota alla nostra applicazione. Tali messaggi dovrebbero essere scartati da ulteriori elaborazioni.

Innanzitutto, creeremo l' interfaccia del router :

public interface Router { void route(Message msg); }

Successivamente, creiamo due implementazioni dell'interfaccia sopra: quella responsabile dell'instradamento a un gateway SMS e quella che instraderà i messaggi alla coda JMS:

public class SmsRouter implements Router { @Override public void route(Message msg) { // implementation details } }
public class JmsRouter implements Router { @Override public void route(Message msg) { // implementation details } }

Infine, implementiamo il nostro oggetto null:

public class NullRouter implements Router { @Override public void route(Message msg) { // do nothing } }

Ora siamo pronti per mettere insieme tutti i pezzi. Vediamo come potrebbe apparire il codice client di esempio:

public class RoutingHandler { public void handle(Iterable messages) { for (Message msg : messages) { Router router = RouterFactory.getRouterForMessage(msg); router.route(msg); } } }

Come possiamo vedere, trattiamo tutti gli oggetti Router allo stesso modo, indipendentemente dall'implementazione restituita da RouterFactory. Questo ci permette di mantenere il nostro codice pulito e leggibile.

5. Quando utilizzare il pattern di oggetti nulli

Dovremmo usare il pattern oggetto nullo quando un client controllerebbe altrimenti la presenza di null solo per saltare l'esecuzione o eseguire un'azione predefinita. In questi casi, possiamo incapsulare la logica neutra all'interno di un oggetto nullo e restituirlo al client invece del valore nullo . In questo modo il codice del client non deve più essere a conoscenza se una determinata istanza è nulla o meno.

Un tale approccio segue principi generali orientati agli oggetti, come Tell-Don't-Ask.

To better understand when we should use the Null Object Pattern, let's imagine we have to implement CustomerDao interface defined as follows:

public interface CustomerDao { Collection findByNameAndLastname(String name, String lastname); Customer getById(Long id); }

Most of the developers would return Collections.emptyList() from findByNameAndLastname() in case none of the customers matches the provided search criteria. This is a very good example of following the Null Object Pattern.

In contrast, the getById() should return the customer with the given id. Someone calling this method expects to get the specific customer entity. In case no such customer exists we should explicitly return null to signal there is something wrong with the provided id.

Come con tutti gli altri pattern, dobbiamo considerare il nostro caso d'uso specifico prima di implementare ciecamente il Null Object Pattern . Altrimenti, potremmo introdurre involontariamente alcuni bug nel nostro codice che saranno difficili da trovare.

6. Conclusione

In questo articolo, abbiamo appreso cos'è il pattern a oggetti nulli e quando possiamo usarlo. Abbiamo anche implementato un semplice esempio del design pattern.

Come al solito, tutti gli esempi di codice sono disponibili su GitHub.