Los patrones de diseño son soluciones probadas y reutilizables para problemas comunes en el diseño de software. En Dart, como en otros lenguajes orientados a objetos, los patrones de diseño pueden ayudar a crear código más modular, flexible y mantenible. En esta sección, exploraremos algunos de los patrones de diseño más comunes y cómo implementarlos en Dart.

  1. Singleton

Descripción

El patrón Singleton asegura que una clase tenga una única instancia y proporciona un punto de acceso global a ella.

Implementación en Dart

class Singleton {
  // La instancia única de la clase
  static final Singleton _instance = Singleton._internal();

  // Constructor privado
  Singleton._internal();

  // Método para obtener la instancia única
  static Singleton get instance => _instance;

  // Ejemplo de método de la clase Singleton
  void someMethod() {
    print('Método del Singleton');
  }
}

void main() {
  var singleton1 = Singleton.instance;
  var singleton2 = Singleton.instance;

  singleton1.someMethod();

  // Verificar que ambas variables apuntan a la misma instancia
  print(singleton1 == singleton2); // true
}

Explicación

  • Constructor Privado: Singleton._internal() es un constructor privado que impide la creación de instancias desde fuera de la clase.
  • Instancia Única: _instance es una instancia estática y final de la clase Singleton.
  • Método de Acceso: instance es un getter estático que devuelve la instancia única.

  1. Factory Method

Descripción

El patrón Factory Method define una interfaz para crear un objeto, pero permite a las subclases alterar el tipo de objetos que se crearán.

Implementación en Dart

abstract class Product {
  void operation();
}

class ConcreteProductA implements Product {
  @override
  void operation() {
    print('Operación de ConcreteProductA');
  }
}

class ConcreteProductB implements Product {
  @override
  void operation() {
    print('Operación de ConcreteProductB');
  }
}

abstract class Creator {
  // Método Factory
  Product factoryMethod();

  void someOperation() {
    var product = factoryMethod();
    product.operation();
  }
}

class ConcreteCreatorA extends Creator {
  @override
  Product factoryMethod() {
    return ConcreteProductA();
  }
}

class ConcreteCreatorB extends Creator {
  @override
  Product factoryMethod() {
    return ConcreteProductB();
  }
}

void main() {
  Creator creatorA = ConcreteCreatorA();
  creatorA.someOperation(); // Operación de ConcreteProductA

  Creator creatorB = ConcreteCreatorB();
  creatorB.someOperation(); // Operación de ConcreteProductB
}

Explicación

  • Interfaz Product: Define la interfaz de los productos que se crearán.
  • ConcreteProductA y ConcreteProductB: Implementaciones concretas de Product.
  • Creator: Clase abstracta que declara el método factory factoryMethod.
  • ConcreteCreatorA y ConcreteCreatorB: Subclases que implementan el método factory para crear instancias de ConcreteProductA y ConcreteProductB.

  1. Observer

Descripción

El patrón Observer define una dependencia de uno a muchos entre objetos, de manera que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.

Implementación en Dart

abstract class Observer {
  void update(String state);
}

class ConcreteObserver implements Observer {
  final String name;

  ConcreteObserver(this.name);

  @override
  void update(String state) {
    print('$name recibió actualización: $state');
  }
}

class Subject {
  List<Observer> _observers = [];
  String _state;

  void attach(Observer observer) {
    _observers.add(observer);
  }

  void detach(Observer observer) {
    _observers.remove(observer);
  }

  void notify() {
    for (var observer in _observers) {
      observer.update(_state);
    }
  }

  void setState(String state) {
    _state = state;
    notify();
  }
}

void main() {
  var subject = Subject();

  var observer1 = ConcreteObserver('Observer 1');
  var observer2 = ConcreteObserver('Observer 2');

  subject.attach(observer1);
  subject.attach(observer2);

  subject.setState('Nuevo Estado'); // Observer 1 y Observer 2 reciben la actualización
}

Explicación

  • Observer: Interfaz que define el método update que los observadores deben implementar.
  • ConcreteObserver: Implementación concreta de Observer.
  • Subject: Clase que mantiene una lista de observadores y notifica a todos cuando su estado cambia.

Ejercicios Prácticos

Ejercicio 1: Implementar el Patrón Singleton

Crea una clase Database que siga el patrón Singleton. La clase debe tener un método query que imprima un mensaje indicando que se está ejecutando una consulta.

Ejercicio 2: Implementar el Patrón Factory Method

Crea una jerarquía de clases para representar diferentes tipos de documentos (PDFDocument, WordDocument). Implementa el patrón Factory Method para crear instancias de estos documentos.

Ejercicio 3: Implementar el Patrón Observer

Crea un sistema de notificaciones donde diferentes tipos de observadores (por ejemplo, EmailObserver, SMSObserver) reciben actualizaciones cuando hay un nuevo mensaje en el sistema.

Soluciones

Solución Ejercicio 1

class Database {
  static final Database _instance = Database._internal();

  Database._internal();

  static Database get instance => _instance;

  void query(String sql) {
    print('Ejecutando consulta: $sql');
  }
}

void main() {
  var db1 = Database.instance;
  var db2 = Database.instance;

  db1.query('SELECT * FROM users');

  print(db1 == db2); // true
}

Solución Ejercicio 2

abstract class Document {
  void open();
}

class PDFDocument implements Document {
  @override
  void open() {
    print('Abriendo documento PDF');
  }
}

class WordDocument implements Document {
  @override
  void open() {
    print('Abriendo documento Word');
  }
}

abstract class DocumentCreator {
  Document createDocument();

  void openDocument() {
    var doc = createDocument();
    doc.open();
  }
}

class PDFDocumentCreator extends DocumentCreator {
  @override
  Document createDocument() {
    return PDFDocument();
  }
}

class WordDocumentCreator extends DocumentCreator {
  @override
  Document createDocument() {
    return WordDocument();
  }
}

void main() {
  DocumentCreator pdfCreator = PDFDocumentCreator();
  pdfCreator.openDocument(); // Abriendo documento PDF

  DocumentCreator wordCreator = WordDocumentCreator();
  wordCreator.openDocument(); // Abriendo documento Word
}

Solución Ejercicio 3

abstract class Observer {
  void update(String message);
}

class EmailObserver implements Observer {
  @override
  void update(String message) {
    print('Email recibido: $message');
  }
}

class SMSObserver implements Observer {
  @override
  void update(String message) {
    print('SMS recibido: $message');
  }
}

class NotificationSystem {
  List<Observer> _observers = [];

  void attach(Observer observer) {
    _observers.add(observer);
  }

  void detach(Observer observer) {
    _observers.remove(observer);
  }

  void notify(String message) {
    for (var observer in _observers) {
      observer.update(message);
    }
  }

  void newMessage(String message) {
    notify(message);
  }
}

void main() {
  var notificationSystem = NotificationSystem();

  var emailObserver = EmailObserver();
  var smsObserver = SMSObserver();

  notificationSystem.attach(emailObserver);
  notificationSystem.attach(smsObserver);

  notificationSystem.newMessage('Tienes un nuevo mensaje'); // Email y SMS reciben la actualización
}

Conclusión

En esta sección, hemos explorado algunos de los patrones de diseño más comunes y cómo implementarlos en Dart. Los patrones de diseño son herramientas poderosas que pueden ayudarte a escribir código más limpio, modular y mantenible. A medida que avances en tu carrera como desarrollador, te encontrarás con estos patrones una y otra vez, y comprender cómo y cuándo usarlos será una habilidad invaluable.

© Copyright 2024. Todos los derechos reservados