Patrones de diseño de software en Go

sábado, 14 de ene. de 2023

Patrones de diseño de software en Go

Los patrones de diseño son planos que nos permiten dar soluciones a problemas comunes que ocurren en la programación. En este post aprenderemos los patrones de diseño más importantes y cómo aplicarlos en Go.
Volver

Patrones creacionales

Establecen mecanismos de creación de objetos que aumentan la flexibilidad y reutilización de código. Los patrones creacionales más importantes son:

  • Abstract Factory. Proporciona una interfaz para crear familias de objetos relacionados o dependientes sin especificar sus clases concretas.

Abstract Factory

En Go, podemos implementar el patrón Abstract Factory de la siguiente manera:

package main
import (
"fmt"
"math"
)
type SphericalFactory struct {
radius float64
}
type PyramidFactory struct {
base float64
height float64
}
type AbstractFactory interface {
GetVolume() float64
GetArea() float64
}
func (s SphericalFactory) GetVolume() float64 {
return (4 / 3) * math.Pi * math.Pow(s.radius, 3)
}
func (s SphericalFactory) GetArea() float64 {
return 4 * math.Pi * math.Pow(s.radius, 2)
}
func (p PyramidFactory) GetVolume() float64 {
return (p.base * p.height) / 3
}
func (p PyramidFactory) GetArea() float64 {
return p.base * p.height
}
func GetFactory(shape string) AbstractFactory {
switch shape {
case "sphere":
return SphericalFactory{radius: 10}
case "pyramid":
return PyramidFactory{base: 10, height: 10}
default:
return nil
}
}
func PrinData(factory AbstractFactory) {
fmt.Println("Area:", factory.GetArea())
fmt.Println("Volume:", factory.GetVolume())
}
func main() {
sphere := GetFactory("sphere")
PrinData(sphere)
pyramid := GetFactory("pyramid")
PrinData(pyramid)
}
  • Singleton. Asegura que una clase sólo tenga una instancia y proporciona un punto de acceso global a ella.

En Go, podemos implementar el patrón Singleton de la siguiente manera:

package main
// singleton example
import (
"fmt"
"time"
)
type Database struct{}
var instance *Database
func (d *Database) CreateConnection() {
fmt.Println("Creating connection...")
time.Sleep(5 * time.Second)
fmt.Println("Connection created")
}
func GetInstance() *Database {
if instance == nil {
instance = &Database{}
instance.CreateConnection()
}
return instance
}
func main() {
conection1 := GetInstance()
conection2 := GetInstance()
fmt.Println(conection1 == conection2)
}

Patrones estructurales

Los patrones estructurales se preocupan por cómo se componen los objetos y clases. Con el fin de lograr una mayor flexibilidad y reutilización de código.

  • Adapter. Convierte la interfaz de una clase en otra interfaz que el cliente espera. Permite que clases trabajen juntas que de otra forma no podrían debido a interfaces incompatibles.

En Go, podemos implementar el patrón Adapter de la siguiente manera:

package main
import "fmt"
type Payment interface {
Pay(amount float32) string
}
type Cash struct{}
type BankPayment struct{}
func (c *Cash) Pay(amount float32) string {
return fmt.Sprintf("%0.2f paid using cash", amount)
}
func (b *BankPayment) Pay(amount float32, account string) string {
return fmt.Sprintf("%0.2f paid using bank(%s)", amount, account)
}
func processPayment(p Payment, amount float32) {
fmt.Println(p.Pay(amount))
}
type BankPaymentAdapter struct {
bankPayment *BankPayment
account string
}
func (b *BankPaymentAdapter) Pay(amount float32) string {
return b.bankPayment.Pay(amount, b.account)
}
func main() {
cash := &Cash{}
processPayment(cash, 100)
backPayment := &BankPaymentAdapter{
bankPayment: &BankPayment{},
account: "123456789",
}
processPayment(backPayment, 200)
}

Patrones de comportamiento

Estos patrones se preocupan por cómo los objetos interactúan y distribuyen la responsabilidad entre ellos.

  • Observer. Define una dependencia de uno a muchos entre objetos de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.

En Go, podemos implementar el patrón Observer de la siguiente manera:

package main
import "fmt"
type Observer interface {
notify(string)
}
type Product struct {
observers []Observer
name string
available bool
}
type EmailClient struct {
email string
}
func NewProduct(name string) *Product {
return &Product{
name: name,
available: false,
}
}
func (prod *Product) register(observer Observer) {
prod.observers = append(prod.observers, observer)
}
func (prod *Product) updateAvailability(available bool) {
prod.available = available
prod.boradcast()
}
func (prod *Product) boradcast() {
if !prod.available {
return
}
for _, observer := range prod.observers {
observer.notify(prod.name)
}
}
func (client *EmailClient) notify(prodName string) {
fmt.Printf("*) Item %s is now available , please check your account %s \n", prodName, client.email)
}
func main() {
product1 := NewProduct("iPhone X")
product2 := NewProduct("iPhone X2")
observer1 := &EmailClient{email: "carlos@gmail.com"}
observer2 := &EmailClient{email: "mila@gmail.com"}
observer3 := &EmailClient{email: "joe@gmail.com"}
product1.register(observer1)
product1.register(observer2)
product2.register(observer3)
product1.updateAvailability(true)
}
  • Strategy. Define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables. Permite que el algoritmo varíe independientemente de los clientes que lo usan.

En Go, podemos implementar el patrón Strategy de la siguiente manera:

package main
type Algorithm interface {
hash(p *PasswordProtector)
}
type PasswordProtector struct {
password string
algorithm Algorithm
}
func (p *PasswordProtector) SetAlgorithm(a Algorithm) {
p.algorithm = a
}
func (p *PasswordProtector) Hash() {
p.algorithm.hash(p)
}
type SHA256 struct{}
type MD5 struct{}
func (s *SHA256) hash(p *PasswordProtector) {
p.password = "SHA256->" + p.password
}
func (m *MD5) hash(p *PasswordProtector) {
p.password = "MD5->" + p.password
}
func main() {
pass := &PasswordProtector{password: "123456"}
pass.SetAlgorithm(&SHA256{})
pass.Hash()
println(pass.password)
pass.SetAlgorithm(&MD5{})
pass.Hash()
println(pass.password)
}