Introduzione ai modelli di design creativi

1. Introduzione

Nell'ingegneria del software, un Design Pattern descrive una soluzione consolidata ai problemi più comunemente riscontrati nella progettazione del software. Rappresenta le migliori pratiche sviluppate in un lungo periodo attraverso tentativi ed errori da parte di sviluppatori di software esperti.

Design Patterns ha guadagnato popolarità dopo che il libro Design Patterns: Elements of Reusable Object-Oriented Software è stato pubblicato nel 1994 da Erich Gamma, John Vlissides, Ralph Johnson e Richard Helm (noto anche come Gang of Four o GoF).

In questo articolo, esploreremo i modelli di design creazionali e i loro tipi. Esamineremo anche alcuni esempi di codice e discuteremo le situazioni in cui questi modelli si adattano al nostro design.

2. Modelli di design creativi

I Creational Design Patterns riguardano il modo in cui vengono creati gli oggetti. Riducono complessità e instabilità creando oggetti in modo controllato.

Il nuovo operatore è spesso considerato dannoso in quanto disperde gli oggetti in tutta l'applicazione. Nel tempo può diventare difficile modificare un'implementazione perché le classi diventano strettamente collegate.

Creational Design Patterns risolve questo problema scollegando completamente il client dal processo di inizializzazione effettivo.

In questo articolo, discuteremo quattro tipi di pattern di design creazionale:

  1. Singleton: assicura che esista al massimo una sola istanza di un oggetto in tutta l'applicazione
  2. Metodo Factory: crea oggetti di diverse classi correlate senza specificare l'oggetto esatto da creare
  3. Fabbrica astratta: crea famiglie di oggetti dipendenti correlati
  4. Builder : costruisce oggetti complessi utilizzando un approccio graduale

Discutiamo ora in dettaglio ciascuno di questi modelli.

3. Singleton Design Pattern

Il Singleton Design Pattern mira a mantenere un controllo sull'inizializzazione degli oggetti di una particolare classe assicurando che esista solo un'istanza dell'oggetto in Java Virtual Machine.

Una classe Singleton fornisce anche un punto di accesso globale univoco all'oggetto in modo che ogni chiamata successiva al punto di accesso restituisca solo quel particolare oggetto.

3.1. Esempio di pattern singleton

Sebbene il pattern Singleton sia stato introdotto da GoF, l'implementazione originale è nota per essere problematica negli scenari multithread.

Quindi qui, seguiremo un approccio più ottimale che utilizza una classe interna statica:

public class Singleton { private Singleton() {} private static class SingletonHolder { public static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.instance; } }

Qui abbiamo creato una classe interna statica che contiene l'istanza della classe Singleton . Crea l'istanza solo quando qualcuno chiama il metodo getInstance () e non quando viene caricata la classe esterna.

Questo è un approccio ampiamente utilizzato per una classe Singleton in quanto non richiede la sincronizzazione, è thread-safe, impone l'inizializzazione lenta e ha relativamente meno boilerplate.

Inoltre, tieni presente che il costruttore ha il modificatore di accesso privato . Questo è un requisito per la creazione di un singleton poiché un costruttore pubblico significherebbe che chiunque potrebbe accedervi e iniziare a creare nuove istanze.

Ricorda, questa non è l'implementazione originale di GoF. Per la versione originale, visita questo articolo di Baeldung su Singletons in Java.

3.2. Quando utilizzare Singleton Design Pattern

  • Per risorse costose da creare (come gli oggetti di connessione al database)
  • È buona norma mantenere tutti i logger come Singleton, il che aumenta le prestazioni
  • Classi che forniscono accesso alle impostazioni di configurazione dell'applicazione
  • Classi che contengono risorse a cui si accede in modalità condivisa

4. Factory Method Design Pattern

Il Factory Design Pattern o Factory Method Design Pattern è uno dei design pattern più utilizzati in Java.

Secondo GoF, questo modello “definisce un'interfaccia per la creazione di un oggetto, ma lascia che le sottoclassi decidano quale classe istanziare. Il metodo Factory consente a una classe di differire l'istanza di sottoclassi ”.

This pattern delegates the responsibility of initializing a class from the client to a particular factory class by creating a type of virtual constructor.

To achieve this, we rely on a factory which provides us with the objects, hiding the actual implementation details. The created objects are accessed using a common interface.

4.1. Factory Method Design Pattern Example

In this example, we'll create a Polygon interface which will be implemented by several concrete classes. A PolygonFactory will be used to fetch objects from this family:

Let's first create the Polygon interface:

public interface Polygon { String getType(); }

Next, we'll create a few implementations like Square, Triangle, etc. that implement this interface and return an object of Polygon type.

Now we can create a factory that takes the number of sides as an argument and returns the appropriate implementation of this interface:

public class PolygonFactory { public Polygon getPolygon(int numberOfSides) { if(numberOfSides == 3) { return new Triangle(); } if(numberOfSides == 4) { return new Square(); } if(numberOfSides == 5) { return new Pentagon(); } if(numberOfSides == 7) { return new Heptagon(); } else if(numberOfSides == 8) { return new Octagon(); } return null; } }

Notice how the client can rely on this factory to give us an appropriate Polygon, without having to initialize the object directly.

4.2. When to Use Factory Method Design Pattern

  • When the implementation of an interface or an abstract class is expected to change frequently
  • When the current implementation cannot comfortably accommodate new change
  • When the initialization process is relatively simple, and the constructor only requires a handful of parameters

5. Abstract Factory Design Pattern

In the previous section, we saw how the Factory Method design pattern could be used to create objects related to a single family.

By contrast, the Abstract Factory Design Pattern is used to create families of related or dependent objects. It's also sometimes called a factory of factories.

For a detailed explanation, check out our Abstract Factory tutorial.

6. Builder Design Pattern

The Builder Design Pattern is another creational pattern designed to deal with the construction of comparatively complex objects.

When the complexity of creating object increases, the Builder pattern can separate out the instantiation process by using another object (a builder) to construct the object.

This builder can then be used to create many other similar representations using a simple step-by-step approach.

6.1. Builder Pattern Example

The original Builder Design Pattern introduced by GoF focuses on abstraction and is very good when dealing with complex objects, however, the design is a little complicated.

Joshua Bloch, in his book Effective Java, introduced an improved version of the builder pattern which is clean, highly readable (because it makes use of fluent design) and easy to use from client's perspective. In this example, we'll discuss that version.

This example has only one class, BankAccount which contains a builder as a static inner class:

public class BankAccount { private String name; private String accountNumber; private String email; private boolean newsletter; // constructors/getters public static class BankAccountBuilder { // builder code } } 

Note that all the access modifiers on the fields are declared private since we don't want outer objects to access them directly.

The constructor is also private so that only the Builder assigned to this class can access it. All of the properties set in the constructor are extracted from the builder object which we supply as an argument.

We've defined BankAccountBuilder in a static inner class:

public static class BankAccountBuilder { private String name; private String accountNumber; private String email; private boolean newsletter; public BankAccountBuilder(String name, String accountNumber) { this.name = name; this.accountNumber = accountNumber; } public BankAccountBuilder withEmail(String email) { this.email = email; return this; } public BankAccountBuilder wantNewsletter(boolean newsletter) { this.newsletter = newsletter; return this; } public BankAccount build() { return new BankAccount(this); } } 

Notice we've declared the same set of fields that the outer class contains. Any mandatory fields are required as arguments to the inner class's constructor while the remaining optional fields can be specified using the setter methods.

This implementation also supports the fluent design approach by having the setter methods return the builder object.

Finally, the build method calls the private constructor of the outer class and passes itself as the argument. The returned BankAccount will be instantiated with the parameters set by the BankAccountBuilder.

Let's see a quick example of the builder pattern in action:

BankAccount newAccount = new BankAccount .BankAccountBuilder("Jon", "22738022275") .withEmail("[email protected]") .wantNewsletter(true) .build();

6.2. When to Use Builder Pattern

  1. When the process involved in creating an object is extremely complex, with lots of mandatory and optional parameters
  2. When an increase in the number of constructor parameters leads to a large list of constructors
  3. When client expects different representations for the object that's constructed

7. Conclusion

In questo articolo, abbiamo imparato a conoscere i modelli di progettazione creazionali in Java. Abbiamo anche discusso i loro quattro diversi tipi, vale a dire, Singleton, Factory Method, Abstract Factory e Builder Pattern, i loro vantaggi, esempi e quando dovremmo usarli.

Come sempre, gli snippet di codice completi sono disponibili su GitHub.