Design patterns are typical solutions to common problems in software design. They are like blueprints that you can customize to solve a particular design problem in your code. In this section, we will explore some of the most commonly used design patterns in Dart.

What are Design Patterns?

Design patterns are reusable solutions to common problems in software design. They are not finished designs that can be transformed directly into code but are templates for how to solve a problem in various situations.

Types of Design Patterns

Design patterns are generally categorized into three types:

  1. Creational Patterns: Deal with object creation mechanisms.
  2. Structural Patterns: Deal with object composition or the structure of classes.
  3. Behavioral Patterns: Deal with object collaboration and the delegation of responsibilities.

Creational Patterns

Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

Example

class Singleton {
  // Private constructor
  Singleton._privateConstructor();

  // The single instance of the class
  static final Singleton _instance = Singleton._privateConstructor();

  // Factory constructor to return the same instance
  factory Singleton() {
    return _instance;
  }

  void someMethod() {
    print('Singleton method called');
  }
}

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

  print(singleton1 == singleton2); // true
  singleton1.someMethod(); // Singleton method called
}

Explanation

  • The constructor is private to prevent direct instantiation.
  • A static instance of the class is created.
  • The factory constructor returns the same instance every time.

Factory Pattern

The Factory pattern defines an interface for creating an object but lets subclasses alter the type of objects that will be created.

Example

abstract class Animal {
  void speak();
}

class Dog implements Animal {
  @override
  void speak() {
    print('Woof!');
  }
}

class Cat implements Animal {
  @override
  void speak() {
    print('Meow!');
  }
}

class AnimalFactory {
  static Animal createAnimal(String type) {
    if (type == 'dog') {
      return Dog();
    } else if (type == 'cat') {
      return Cat();
    } else {
      throw Exception('Animal type not recognized');
    }
  }
}

void main() {
  var dog = AnimalFactory.createAnimal('dog');
  var cat = AnimalFactory.createAnimal('cat');

  dog.speak(); // Woof!
  cat.speak(); // Meow!
}

Explanation

  • An abstract class Animal defines a method speak.
  • Dog and Cat classes implement the Animal interface.
  • AnimalFactory class has a static method createAnimal that returns an instance of Dog or Cat based on the input.

Structural Patterns

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.

Example

class OldSystem {
  void oldMethod() {
    print('Old system method');
  }
}

class NewSystem {
  void newMethod() {
    print('New system method');
  }
}

class Adapter implements OldSystem {
  final NewSystem _newSystem;

  Adapter(this._newSystem);

  @override
  void oldMethod() {
    _newSystem.newMethod();
  }
}

void main() {
  var newSystem = NewSystem();
  var adapter = Adapter(newSystem);

  adapter.oldMethod(); // New system method
}

Explanation

  • OldSystem has a method oldMethod.
  • NewSystem has a method newMethod.
  • Adapter implements OldSystem and uses an instance of NewSystem to call newMethod when oldMethod is called.

Behavioral Patterns

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Example

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

class ConcreteObserver implements Observer {
  final String name;

  ConcreteObserver(this.name);

  @override
  void update(String message) {
    print('$name received: $message');
  }
}

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

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

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

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

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

  var subject = Subject();
  subject.addObserver(observer1);
  subject.addObserver(observer2);

  subject.notifyObservers('Hello Observers!'); // Observer 1 received: Hello Observers!
                                               // Observer 2 received: Hello Observers!
}

Explanation

  • Observer is an abstract class with an update method.
  • ConcreteObserver implements Observer and defines the update method.
  • Subject maintains a list of observers and notifies them of any changes.

Practical Exercises

Exercise 1: Implementing Singleton Pattern

Task: Create a Singleton class Database that has a method connect which prints "Database connected".

Solution:

class Database {
  Database._privateConstructor();

  static final Database _instance = Database._privateConstructor();

  factory Database() {
    return _instance;
  }

  void connect() {
    print('Database connected');
  }
}

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

  print(db1 == db2); // true
  db1.connect(); // Database connected
}

Exercise 2: Implementing Factory Pattern

Task: Create a factory class ShapeFactory that returns instances of Circle and Square classes based on the input.

Solution:

abstract class Shape {
  void draw();
}

class Circle implements Shape {
  @override
  void draw() {
    print('Drawing Circle');
  }
}

class Square implements Shape {
  @override
  void draw() {
    print('Drawing Square');
  }
}

class ShapeFactory {
  static Shape createShape(String type) {
    if (type == 'circle') {
      return Circle();
    } else if (type == 'square') {
      return Square();
    } else {
      throw Exception('Shape type not recognized');
    }
  }
}

void main() {
  var circle = ShapeFactory.createShape('circle');
  var square = ShapeFactory.createShape('square');

  circle.draw(); // Drawing Circle
  square.draw(); // Drawing Square
}

Conclusion

In this section, we explored some of the most commonly used design patterns in Dart, including Singleton, Factory, Adapter, and Observer patterns. Understanding and implementing these patterns can help you write more efficient, maintainable, and scalable code. In the next module, we will dive into the final project where you can apply these patterns in a real-world scenario.

© Copyright 2024. All rights reserved