Skip to content

Latest commit

 

History

History
155 lines (114 loc) · 5.06 KB

File metadata and controls

155 lines (114 loc) · 5.06 KB

Decorator Pattern: Dynamically Extending Objects

Hello, Dart enthusiasts and design pattern adventurers! 👋 Today, we're diving into the fascinating world of the Decorator pattern. Buckle up as we explore how this pattern can elegantly solve the challenge of extending object functionality without subclassing madness.

What's the Decorator Pattern All About?

The Decorator pattern allows you to add new behaviors to objects dynamically by placing these objects inside special wrapper objects. Imagine it as a way to "decorate" your objects with new capabilities, like adding toppings to your favorite ice cream 🍦 without changing the original ice cream.

Key points:

  • Adds functionality to objects without altering their structure
  • Provides a flexible alternative to subclassing
  • Follows the Open/Closed Principle: open for extension, closed for modification

When Should You Use It?

Consider the Decorator pattern when:

  1. You need to add responsibilities to objects dynamically and transparently
  2. You want to avoid a class hierarchy explosion
  3. You can't extend an object's behavior using inheritance (e.g., sealed classes)

The Anatomy of a Decorator

Let's break down the key components:

  1. Component Interface: Defines the interface for objects that can have responsibilities added to them dynamically.

  2. Concrete Component: The basic object to which new behaviors can be attached.

  3. Decorator: Maintains a reference to a Component object and defines an interface that conforms to Component's interface.

  4. Concrete Decorators: Add responsibilities to the component.

Show Me The Code!

Let's look at a simplified version of our logging example to illustrate the pattern:

// Base Interface for the Logger
abstract interface class Logger {
  void log(String message);
}

// Basic Implementation
class BasicLogger implements Logger {
  @override
  void log(String message) {
    print(message);
  }
}

// Base class for decorators
abstract class LoggerDecorator implements Logger {
  final Logger _wrappedLogger;

  const LoggerDecorator(this._wrappedLogger);

  @override
  void log(String message) {
    _wrappedLogger.log(message);
  }
}

// Decorator for adding timestamps
class TimestampDecorator extends LoggerDecorator {
  TimestampDecorator(Logger logger) : super(logger);

  @override
  void log(String message) {
    final timestamp = DateTime.now().toIso8601String();
    super.log('$timestamp: $message');
  }
}

// Decorator for adding log levels
class LevelDecorator extends LoggerDecorator {
  final String level;

  LevelDecorator(Logger logger, this.level) : super(logger);

  @override
  void log(String message) {
    super.log('[$level] $message');
  }
}

// Decorator for writing to a file
class FileWriteDecorator extends LoggerDecorator {
  final String filePath;

  FileWriteDecorator(Logger logger, this.filePath) : super(logger);

  @override
  void log(String message) {
    super.log(message);
    // In a real implementation, we would write to a file here
    print('Also writing "$message" to $filePath');
  }
}

Putting It All Together

Here's how you might use these decorators:

void main() {
  // Create a basic logger
  final basicLogger = BasicLogger();

  // Create a decorated logger with multiple decorators
  final decoratedLogger = FileWriteDecorator(
    LevelDecorator(
      TimestampDecorator(basicLogger),
      'INFO'
    ),
    'log.txt'
  );

  // Use the decorated logger
  decoratedLogger.log('This is a test message');

  // Use it again to show caching won't apply here
  decoratedLogger.log('This is another test message');

  // Output: 
  // 2024-07-27T15:27:47.876490: [INFO] This is a test message
  // Also writing "This is a test message" to log.txt
  // 2024-07-27T15:27:47.877530: [INFO] This is another test message
  // Also writing "This is another test message" to log.txt
}

Why Should You Care?

  1. Flexibility: Add or remove responsibilities from objects at runtime.
  2. Composition Over Inheritance: Achieve behavior modification through composition, avoiding class explosion.
  3. Single Responsibility Principle: Each decorator class focuses on a single concern.
  4. Open/Closed Principle: Extend object behavior without modifying existing code.

Real-World Applications

The Decorator pattern isn't just for loggers! Here are some other use cases:

  • Adding compression or encryption to data streams
  • Extending UI component functionalities in Flutter
  • Adding caching or retry mechanisms to network requests
  • Enhancing database query objects with additional operations
  • etc.

Learn More

Remember, the key to mastering the Decorator pattern is practice and creative thinking. Look for opportunities in your code where you need to add behavior dynamically, and the Decorator might just be your new best friend! Happy coding! 🚀