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:
- Creational Patterns: Deal with object creation mechanisms.
- Structural Patterns: Deal with object composition or the structure of classes.
- 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 methodspeak
. Dog
andCat
classes implement theAnimal
interface.AnimalFactory
class has a static methodcreateAnimal
that returns an instance ofDog
orCat
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 methodoldMethod
.NewSystem
has a methodnewMethod
.Adapter
implementsOldSystem
and uses an instance ofNewSystem
to callnewMethod
whenoldMethod
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 anupdate
method.ConcreteObserver
implementsObserver
and defines theupdate
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.
Dart Programming Course
Module 1: Introduction to Dart
- Introduction to Dart
- Setting Up the Development Environment
- Your First Dart Program
- Basic Syntax and Structure