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
Animaldefines a methodspeak. DogandCatclasses implement theAnimalinterface.AnimalFactoryclass has a static methodcreateAnimalthat returns an instance ofDogorCatbased 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
OldSystemhas a methodoldMethod.NewSystemhas a methodnewMethod.AdapterimplementsOldSystemand uses an instance ofNewSystemto callnewMethodwhenoldMethodis 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
Observeris an abstract class with anupdatemethod.ConcreteObserverimplementsObserverand defines theupdatemethod.Subjectmaintains 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
