Introduction to Patterns in Programming
Patterns are fundamental solutions to commonly occurring problems within a given context in software design. By using patterns, developers can create scalable, reusable, and maintainable code. From simple structural patterns to advanced behavioral ones, understanding and utilizing patterns can significantly improve the development process.
Understanding Patterns
A pattern is essentially a template for solving a problem that can be used in multiple scenarios. Modern programming languages provide several frameworks and APIs that help in implementing such patterns. Below, we dive into dozens of examples and code snippets that demonstrate patterns and related APIs in action.
1. Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
class Singleton:
_instance = None
@staticmethod
def get_instance():
if Singleton._instance is None:
Singleton._instance = Singleton()
return Singleton._instance
Usage in a hypothetical API:
db_connection = Singleton.get_instance()
another_connection = Singleton.get_instance()
assert db_connection is another_connection # True since only one instance exists.
2. Factory Pattern
The Factory pattern is used for creating objects without specifying their exact class.
class ShapeFactory:
def get_shape(self, shape_type):
if shape_type.lower() == "circle":
return Circle()
elif shape_type.lower() == "square":
return Square()
return None
Example Usage:
factory = ShapeFactory()
circle = factory.get_shape("circle")
circle.draw()
3. Observer Pattern
The Observer pattern defines a one-to-many dependency between objects, where if one object changes state, all dependents are notified.
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def notify(self):
for observer in self._observers:
observer.update()
class Observer:
def update(self):
print("Observer notified!")
subject = Subject()
observer1 = Observer()
observer2 = Observer()
subject.attach(observer1)
subject.attach(observer2)
subject.notify()
4. Decorator Pattern
The Decorator pattern allows behavior to be added to an object dynamically without altering its structure.
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args: {args} kwargs: {kwargs}")
return func(*args, **kwargs)
return wrapper
@log_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
5. Strategy Pattern
The Strategy pattern defines a family of algorithms and lets you choose one at runtime.
class StrategyA:
def execute(self):
print("Using Strategy A")
class StrategyB:
def execute(self):
print("Using Strategy B")
class Context:
def __init__(self, strategy):
self._strategy = strategy
def execute_strategy(self):
self._strategy.execute()
context = Context(StrategyA())
context.execute_strategy()
context = Context(StrategyB())
context.execute_strategy()
Application Example: Task Automation App
Let’s use some of these patterns to build a basic task automation application. This app lets users schedule tasks and notify observers about task completions.
class Task:
def __init__(self, name):
self.name = name
def execute(self):
print(f"Executing task: {self.name}")
class TaskScheduler:
def __init__(self):
self.queue = []
self.observers = []
def schedule_task(self, task):
self.queue.append(task)
def attach_observer(self, observer):
self.observers.append(observer)
def execute_all(self):
while self.queue:
task = self.queue.pop(0)
task.execute()
for observer in self.observers:
observer.update(task)
class TaskObserver:
def update(self, task):
print(f"Task {task.name} completed!")
task_scheduler = TaskScheduler()
task_scheduler.schedule_task(Task("Backup"))
task_scheduler.schedule_task(Task("Clean Cache"))
observer = TaskObserver()
task_scheduler.attach_observer(observer)
task_scheduler.execute_all()
By applying the Observer and Task Scheduler patterns, we created a scalable and extensible application to handle automated tasks.
Conclusion
Patterns offer proven solutions to common design problems in software development. With proper usage, they can lead to clean, efficient, and maintainable code. Implementing patterns, as demonstrated above, can significantly enhance your applications and streamline your development workflow.