There are only two hard things in Computer Science: cache invalidation and naming things.

Design Patterns

Design Patterns: La Cassetta degli Attrezzi dello Sviluppatore 🛠️

Ah, i design pattern! Quei piccoli gioielli di saggezza che ci fanno sembrare più intelligenti di quanto siamo realmente. Ma cosa sono esattamente? E perché dovresti preoccupartene? Beh, immagina di costruire una casa senza un progetto. Certo, potresti farcela, ma probabilmente finirai con un tetto che perde e una porta che non si chiude. I design pattern sono i progetti per il tuo codice: soluzioni collaudate a problemi comuni.


Un Po’ di Storia 📜

Il concetto di design pattern nasce nel libro Design Patterns: Elements of Reusable Object-Oriented Software, scritto da quattro luminari del software (la famosa “Gang of Four”): Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides. Questo libro ha introdotto un linguaggio comune per descrivere soluzioni a problemi ricorrenti nello sviluppo software.

Ogni design pattern identifica un problema generalizzato e associa ad esso una soluzione. Spesso, questi pattern sono rappresentati con diagrammi UML per facilitarne la comprensione.


Perché Usare i Design Pattern? 🤔

  1. Riutilizzabilità: Perché reinventare la ruota quando puoi prendere in prestito una soluzione già testata?
  2. Manutenibilità: Codice più leggibile e organizzato significa meno mal di testa per te (e per chi erediterà il tuo progetto).
  3. Comunicazione: Dire “Usiamo il Singleton” è molto più veloce che spiegare tutto il concetto da zero.
  4. Soluzioni Provate: I design pattern sono frutto di anni di esperienza e casi d’uso reali, quindi puoi fidarti che funzionano.

Le Tre Categorie Principali

Il libro della Gang of Four suddivide i design pattern in tre categorie principali:

1. Pattern Creazionali 🏗️

Questi pattern si concentrano sulla creazione degli oggetti, isolando la logica di costruzione dal resto del codice.

  • Singleton: Garantisce che una classe abbia una sola istanza.
  • Factory Method: Definisce un’interfaccia per creare oggetti, lasciando alle sottoclassi la decisione su quale classe istanziare.
  • Abstract Factory: Permette di creare famiglie di oggetti correlati senza specificare le loro classi concrete.
  • Builder: Consente di costruire oggetti complessi passo dopo passo.
  • Prototype: Crea nuovi oggetti clonando un’istanza esistente.

Esempio di Factory Method in GoLang

 1// filepath: /Users/sandrolain/work/fullstackdeveloper/content/docs/development/design-patterns.md
 2package main
 3
 4import "fmt"
 5
 6type Product interface {
 7  Use() string
 8}
 9
10type ConcreteProductA struct{}
11
12func (p ConcreteProductA) Use() string {
13  return "Using Product A"
14}
15
16type ConcreteProductB struct{}
17
18func (p ConcreteProductB) Use() string {
19  return "Using Product B"
20}
21
22func FactoryMethod(productType string) Product {
23  switch productType {
24  case "A":
25    return ConcreteProductA{}
26  case "B":
27    return ConcreteProductB{}
28  default:
29    return nil
30  }
31}
32
33func main() {
34 product := FactoryMethod("A")
35 fmt.Println(product.Use()) // Using Product A
36}

2. Pattern Strutturali 🧱

Questi pattern si occupano della composizione delle classi e degli oggetti, rendendo il sistema più flessibile ed efficiente.

  • Adapter: Permette a due interfacce incompatibili di lavorare insieme.
  • Bridge: Divide un’astrazione dalla sua implementazione, permettendo loro di evolvere indipendentemente.
  • Decorator: Aggiunge dinamicamente funzionalità a un oggetto.
  • Façade: Fornisce un’interfaccia semplificata a un sistema complesso.
  • Flyweight: Riduce il consumo di memoria condividendo dati tra oggetti simili.
  • Proxy: Fornisce un surrogato o un placeholder per controllare l’accesso a un oggetto.

Esempio di Decorator in GoLang

 1// filepath: /Users/sandrolain/work/fullstackdeveloper/content/docs/development/design-patterns.md
 2package main
 3
 4import "fmt"
 5
 6// Component interface
 7type Coffee interface {
 8  Cost() int
 9  Description() string
10}
11
12// Concrete Component
13type SimpleCoffee struct{}
14
15func (c SimpleCoffee) Cost() int {
16  return 5
17}
18
19func (c SimpleCoffee) Description() string {
20  return "Simple Coffee"
21}
22
23// Decorator
24type MilkDecorator struct {
25  coffee Coffee
26}
27
28func (m MilkDecorator) Cost() int {
29  return m.coffee.Cost() + 2
30}
31
32func (m MilkDecorator) Description() string {
33  return m.coffee.Description() + ", Milk"
34}
35
36type SugarDecorator struct {
37  coffee Coffee
38}
39
40func (s SugarDecorator) Cost() int {
41  return s.coffee.Cost() + 1
42}
43
44func (s SugarDecorator) Description() string {
45  return s.coffee.Description() + ", Sugar"
46}
47
48func main() {
49  coffee := SimpleCoffee{}
50  fmt.Println(coffee.Description(), "Cost:", coffee.Cost()) // Simple Coffee Cost: 5
51
52  coffeeWithMilk := MilkDecorator{coffee: coffee}
53  fmt.Println(coffeeWithMilk.Description(), "Cost:", coffeeWithMilk.Cost()) // Simple Coffee, Milk Cost: 7
54
55  coffeeWithMilkAndSugar := SugarDecorator{coffee: coffeeWithMilk}
56  fmt.Println(coffeeWithMilkAndSugar.Description(), "Cost:", coffeeWithMilkAndSugar.Cost()) // Simple Coffee, Milk, Sugar Cost: 8
57}

3. Pattern Comportamentali 🎭

Questi pattern si concentrano sulle interazioni tra gli oggetti e sulla distribuzione delle responsabilità.

  • Observer: Notifica automaticamente gli osservatori quando lo stato di un oggetto cambia.
  • Strategy: Permette di selezionare dinamicamente un algoritmo tra diversi disponibili.
  • Command: Incapsula una richiesta come oggetto, permettendo di parametrizzare i client con richieste diverse.
  • State: Permette a un oggetto di cambiare comportamento in base al suo stato interno.
  • Template Method: Definisce il “guscio” di un algoritmo, delegando i dettagli alle sottoclassi.
  • Visitor: Separa un algoritmo dalla struttura dati su cui opera.

Esempio di Strategy in GoLang

 1// filepath: /Users/sandrolain/work/fullstackdeveloper/content/docs/development/design-patterns.md
 2package main
 3
 4import "fmt"
 5
 6type Strategy interface {
 7  Execute(a, b int) int
 8}
 9
10type Add struct{}
11
12func (Add) Execute(a, b int) int {
13  return a + b
14}
15
16type Multiply struct{}
17
18func (Multiply) Execute(a, b int) int {
19  return a * b
20}
21
22func main() {
23  var strategy Strategy
24
25  strategy = Add{}
26  fmt.Println("Add:", strategy.Execute(3, 4)) // Add: 7
27
28  strategy = Multiply{}
29  fmt.Println("Multiply:", strategy.Execute(3, 4)) // Multiply: 12
30}

Risorse Utili 📚


Conclusione

I design pattern sono strumenti potenti, ma come ogni strumento, vanno usati con giudizio. Non complicare inutilmente il tuo codice solo per “usare un pattern”. Ricorda: il buon senso è il miglior design pattern di tutti. 😉

Ultimo aggiornamento il