Behavioral Patterns
- Focus: How objects communicate and collaborate with each other.
- Goal: Define algorithms and communication patterns between objects.
- Examples:
- Observer: Allows objects to be notified when another object changes state.
- Strategy: Allows dynamic selection of algorithms at runtime.
- Template Method: Defines an algorithm skeleton, allowing subclasses to customize specific steps.
Chain of Responsibility
The chain of responsibility pattern, a powerful behavior pattern in object-oriented programming, aims to pass requests along a chain of handlers until one of them handles it. It essentially creates a pipeline where each handler gets a chance to process the request, offering flexibility and distributed responsibility.
Imagine: You have a customer service department with different tiers of support. A customer's request starts with the basic level agent, and if they can't resolve it, it moves to a supervisor, then to a technical specialist, and so on, until someone can handle it.
Here's how the chain of responsibility pattern works:
- Handler: This is the base interface for all objects in the chain. It defines a method like "handle" that takes the request and decides whether to process it or pass it down the chain.
- ConcreteHandler: This is a specific implementation of the "Handler" interface that can handle certain types of requests. It checks if the request is suitable for its capabilities and processes it if so.
- Client: This is the object that sends the request to the first handler in the chain. It doesn't know which handler will ultimately handle it.
Benefits of using the chain of responsibility pattern:
- Decoupled request handling: The client doesn't need to know how the request is handled or which handler will process it.
- Flexible request processing: You can easily add or remove handlers based on your needs, extending the chain's capabilities.
- Distributed responsibility: Divides the handling logic among different objects, promoting modularity and maintainability.
When to use the chain of responsibility pattern:
- When you have multiple potential handlers for different types of requests.
- When you want to avoid tight coupling between the client and the specific handler.
- When you want to dynamically add or remove handlers at runtime.
Remember:
- The chain of responsibility pattern can add some complexity to your code.
- Ensure the benefits outweigh the added complexity before applying it.
- Design the chain carefully to avoid infinite loops or unclear responsibility.
Examples:
- A customer service system with different tiers of support.
- A validation pipeline that checks data against different rules.
- An authorization system that checks access permissions at different levels.
class Handler:
"""Abstract handler interface"""
def __init__(self, successor=None):
self.successor = successor
def handle_request(self, request):
# Check if the handler can handle the request
if self.can_handle(request):
self.process_request(request)
# Pass the request to the successor if available
elif self.successor is not None:
self.successor.handle_request(request)
def process_request(self, request):
# Implement specific processing logic for the handler
raise NotImplementedError
def can_handle(self, request):
# Implement specific logic to check if the handler can handle the request
raise NotImplementedError
class ConcreteHandler1(Handler):
"""Concrete handler 1, handles requests of type 'A'"""
def can_handle(self, request):
return request == "A"
def process_request(self, request):
print("ConcreteHandler1 handled request:", request)
class ConcreteHandler2(Handler):
"""Concrete handler 2, handles requests of type 'B'"""
def can_handle(self, request):
return request == "B"
def process_request(self, request):
print("ConcreteHandler2 handled request:", request)
class DefaultHandler(Handler):
"""Default handler, handles any request not handled by others"""
def can_handle(self, request):
return True
def process_request(self, request):
print("DefaultHandler handled request:", request)
if __name__ == "__main__":
# Create a chain of handlers
handler1 = ConcreteHandler1(ConcreteHandler2(DefaultHandler()))
# Send different requests to the chain
for request in ["A", "B", "C"]:
handler1.handle_request(request)
Command
The Command pattern is a crucial behavior pattern in object-oriented programming that revolves around transforming a request into an independent object, allowing you to:
Key Idea:
- Encapsulate requests: Instead of directly calling methods to execute actions, you create "Command" objects that hold all the information needed to perform the action. These objects become self-contained units of functionality.
- Queue requests: Store and manage commands in a queue or other data structure, enabling delayed execution, undo/redo capabilities, and easier logging or analysis.
- Parameterized actions: Commands can accept parameters, making them adaptable to different scenarios without rewriting the logic within the command itself.
Components:
- Command: This defines the interface for encapsulating a request, usually with an "execute" method that carries out the action.
- ConcreteCommand: This implements the "Command" interface and provides the specific logic for the action.
- Invoker: This is the object that triggers the execution of a command. It doesn't know the concrete implementation of the command, only that it adheres to the "Command" interface.
- Receiver: This is the actual object that performs the action when the command is executed. The command interacts with the receiver to carry out its functionality.
Benefits:
- Decoupling: Separates the request from its execution, leading to looser coupling and easier maintenance.
- Parameterization: Enables dynamic execution with different parameters, making commands more versatile.
- Queueing: Allows delaying, scheduling, or reordering requests for flexibility and control.
- Undo/Redo: Commands can be stored and executed backward or forward, facilitating undo/redo functionality.
- Logging and Monitoring: Commands can be logged or traced easily, providing valuable insights into system behavior.
When to Use It:
- When you need to decouple the request from its execution.
- When you want to parameterize actions for dynamic behavior.
- When queueing, scheduling, or undo/redo functionality is needed.
- When logging and monitoring specific actions are important.
Examples:
- Buttons in a graphical user interface that trigger commands.
- Macro recordings in software applications.
- Transactions in database systems.
- Undo/redo functionality in text editors.
Remember:
- The Command pattern adds some overhead compared to direct method calls.
- Evaluate the complexity of your actions before applying this pattern.
- Design your commands thoughtfully to avoid tight coupling with specific contexts.
class Order:
def __init__(self, item, quantity):
self.item = item
self.quantity = quantity
class PlaceOrderCommand:
def __init__(self, receiver, order):
self.receiver = receiver
self.order = order
def execute(self):
self.receiver.place_order(self.order)
class Receiver:
def place_order(self, order):
print(f"Processing order for {order.item} (quantity: {order.quantity})")
# Example usage
receiver = Receiver()
order = Order("Pizza", 2)
command = PlaceOrderCommand(receiver, order)
command.execute() # Output: Processing order for Pizza (quantity: 2)
Iterator
The Iterator pattern, a valuable behavior pattern in object-oriented programming, provides a way to access the elements of an aggregate object sequentially without exposing its underlying structure. It essentially acts as a tour guide that navigates through the collection, one element at a time, without you needing to know how the collection is internally implemented.
Imagine: You have a bookshelf with many books. Instead of directly accessing each book by its index or position, you use an iterator to move from one book to the next, reading them sequentially. The iterator handles the details of navigating through the books, while you focus on reading their content.
Key Points:
- Iterator: This interface defines the methods to access elements sequentially, typically
hasNext()
andnext()
. - ConcreteIterator: This implements the
Iterator
interface for a specific aggregate type (e.g., array, list, tree). It maintains the current position and provides methods to access the next element. - Aggregate: This is the collection object that holds the elements. It provides a method to create an
Iterator
object, allowing you to start the traversal. - Client: This code uses the
Iterator
to access elements of the aggregate without knowing its internal structure.
Benefits:
- Abstraction: Hides the internal structure of the aggregate, simplifying client code and promoting loose coupling.
- Flexibility: Supports different traversal methods (e.g., forward, backward, random) through different iterator implementations.
- Efficiency: Allows for optimized iteration strategies based on the aggregate type.
When to Use It:
- When you need to iterate over elements of different collection types in a similar way.
- When you want to hide the internal structure of the collection from client code.
- When you need flexibility in how you traverse the collection.
Remember:
- The Iterator pattern adds some overhead compared to direct access.
- Use it when the benefits of abstraction and flexibility outweigh the added complexity.
- Consider the performance implications of different iterator implementations.
Examples:
- Looping through elements in a list or array.
- Traversing a tree data structure.
- Implementing custom iteration logic for specific collection types.
class StringIterator:
def __init__(self, string):
self.string = string
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.string):
raise StopIteration
char = self.string[self.index]
self.index += 1
return char
for char in StringIterator("Hello"):
print(char) # Output: H e l l o
Mediator
The Mediator pattern, a vital behavioral pattern in object-oriented programming, aims to centralize communication between objects, promoting loose coupling and simplifying complex interactions. It essentially acts as a traffic controller, coordinating and managing how objects interact with each other, avoiding direct dependencies.
Imagine: You have a busy city with several intersections. Instead of cars directly communicating and negotiating passage, they rely on traffic lights and traffic controllers to manage the flow and avoid collisions. The Mediator pattern does the same thing for your object interactions.
Key Points:
- Mediator: This object acts as the central communication hub. It knows about all participating objects and facilitates their interactions without them needing direct references to each other.
- Concrete Mediator: This implements the Mediator interface for a specific communication scenario, defining how objects interact and how the mediator handles those interactions.
- Colleagues: These are the objects that participate in the communication. They interact with the mediator to send requests and receive responses.
Benefits:
- Reduced complexity: Simplifies code by centralizing communication logic and avoiding direct dependencies between objects.
- Loose coupling: Objects only depend on the mediator, promoting flexibility and easier maintenance.
- Improved control: The mediator can enforce rules and manage communication flow, enhancing system stability.
- Testability: Easier to test individual objects and the mediator independently.
When to Use It:
- When multiple objects need to communicate in complex ways.
- When you want to avoid tight coupling between objects.
- When you need centralized control over object interactions.
- When you want to simplify testing and maintainability.
Remember:
- The Mediator pattern introduces an extra layer of abstraction, which might add some overhead.
- Use it when the benefits of centralized communication outweigh the added complexity.
- Design the mediator carefully to avoid becoming a bottleneck or single point of failure.
Examples:
- A chat room application where the chat room acts as the mediator for user interactions.
- A graphical user interface where the main window mediates interactions between different components.
- A system where a central controller manages communication between different devices.
class Chat:
def __init__(self):
self.participants = []
def register_participant(self, participant):
self.participants.append(participant)
def send_message(self, sender, message):
for participant in self.participants:
if participant != sender:
participant.receive_message(sender, message)
class Participant:
def __init__(self, name, chat):
self.name = name
self.chat = chat
self.chat.register_participant(self)
def send_message(self, message):
self.chat.send_message(self, message)
def receive_message(self, sender, message):
print(f"{sender.name}: {message}")
# Example usage
chat = Chat()
user1 = Participant("Alice", chat)
user2 = Participant("Bob", chat)
user1.send_message("Hello everyone!")
user2.send_message("Hi Alice!")
class Airplane:
name = "Delta"
def land():
print('landed')
class Runway:
clear = True
# Mediator
class Tower:
def clearForLanding(runway=Runway,plane=Airplane):
if runway.clear:
print(f"Plane '{plane.name}' is clear for landing")
else:
print('Not clear for landing')
Airplane.land()
Tower.clearForLanding()
class Request:
pass
class Response:
pass
#Mediator
class Middleware:
def formatted(req=Request,res=Response):
print(req,res)
Middleware.formatted()
Memento
class TextEditor:
def __init__(self, content):
self.content = content
def create_memento(self):
return Memento(self.content)
def set_content(self, content):
self.content = content
def restore_from_memento(self, memento):
self.content = memento.content
class Memento:
def __init__(self, content):
self.content = content
# Example usage
editor = TextEditor("Initial content")
memento = editor.create_memento()
editor.set_content("Modified content")
editor.restore_from_memento(memento)
print(editor.content) # Output: Initial content
Observer
Concept:
The Observer pattern, a fundamental behavioral pattern in object-oriented programming, sets up a one-to-many dependency between objects. Essentially, one object (called the subject or publisher) notifies multiple other objects (called observers or subscribers) whenever its state changes. This enables observers to react to those changes automatically, promoting loose coupling and efficient communication.
Think of it like this:
- You're a weather service (subject).
- Several news channels (observers) subscribe to your weather updates.
- When the weather changes, you notify all subscribed channels, and they update their reports accordingly.
Components:
- Subject: Defines an interface for adding and removing observers and notifying them about changes.
- ConcreteSubject: Implements the subject interface, managing a list of observers and notifying them when its state changes.
- Observer: Defines an interface for receiving notifications from subjects.
- ConcreteObserver: Implements the observer interface, reacting to notifications from specific subjects by updating its own state or performing actions.
Benefits:
- Decoupling: Observers don't depend on the internal details of the subject, facilitating code reuse and easier maintenance.
- Flexibility: You can easily add or remove observers without affecting the subject or other observers.
- Scalability: The pattern works well with many observers, allowing for efficient communication in large systems.
When to Use It:
- When multiple objects need to be notified about changes in another object.
- When you want to avoid tight coupling between objects.
- When you need flexibility in how objects are notified and respond to changes.
Remember:
- The Observer pattern can introduce some complexity compared to direct communication.
- Use it when the benefits of decoupling and flexibility outweigh the added complexity.
- Consider performance implications, as notifying many observers can be resource-intensive.
Examples:
- News websites using RSS feeds to notify subscribers about new articles.
- Event-driven programming frameworks using listeners to respond to events.
- User interfaces updating automatically when underlying data changes.
class Subject:
def __init__(self):
self._observers = []
def register_observer(self, observer):
self._observers.append(observer)
def unregister_observer(self, observer):
self._observers.remove(observer)
def notify_observers(self, *args, **kwargs):
for observer in self._observers:
observer.update(self, *args, **kwargs)
class Observer:
def update(self, subject, *args, **kwargs):
raise NotImplementedError
class WeatherStation(Subject):
def __init__(self, temperature, humidity):
super().__init__()
self.temperature = temperature
self.humidity = humidity
def set_measurements(self, new_temperature, new_humidity):
self.temperature = new_temperature
self.humidity = new_humidity
self.notify_observers(new_temperature, new_humidity)
class PhoneDisplay(Observer):
def update(self, subject, temperature, humidity):
print(f"Phone display: Temperature: {temperature}°C, Humidity: {humidity}%")
class TabletDisplay(Observer):
def update(self, subject, temperature, humidity):
print(f"Tablet display: Temperature: {temperature}°C, Humidity: {humidity}%")
# Usage example
weather_station = WeatherStation(25, 60)
phone_display = PhoneDisplay()
tablet_display = TabletDisplay()
weather_station.register_observer(phone_display)
weather_station.register_observer(tablet_display)
weather_station.set_measurements(30, 70) # Output:
# Phone display: Temperature: 30°C, Humidity: 70%
# Tablet display: Temperature: 30°C, Humidity: 70%
weather_station.unregister_observer(phone_display)
weather_station.set_measurements(28, 65) # Output:
# Tablet display: Temperature: 28°C, Humidity: 65%
# An interface is a class that defines methods that can be overriden
class Interface:
def load_data_source(path:str,file_name: str):
"""Load in the file for extracting text"""
pass
def extract_text(full_file_name:str) -> dict:
"""Extract text from the currently loaded file"""
pass
class Subject(Interface):
def add(iobserver):
pass
def remove(iobserver):
pass
def notify(self):
pass
class IObservable(Subject):
def load_data_source(self,path:str,file_name: str):
"""Overide Load in the file for extracting text"""
pass
def extract_text(self,full_file_name:str) -> dict:
"""Overide Extract text from the currently loaded file"""
pass
class IObserver(Interface):
def update(self):
pass
class ConcreteObservable(IObservable):
# Observer stuff
def add():
pass
def remove():
pass
def notify(self):
pass
# Everything it already does
def getState():
pass
class ConcreteObserver(IObserver):
def update(self):
pass
#observable = ConcreteObservable()
#observer = ConcreteObserver(observable)
#poll(observer asking) vs push(subject tells)
#Subscriptions,weather,Rss,push notifications?
IObserver.load_data_source('path/','hello.txt')
# %""
#https://youtu.be/_BpmfnqjgzQ?si=WANoRKpoMzWHQOmB
# IOS examples of these design patterns?
class WeatherStation(IObservable):
def __init__(self):
self.observers = []
self.temperature = 45.0
def add(self,iobserver):
self.observers.append(iobserver)
def remove(self,iobserver):
pass
def notify(self):
for o in self.observers:
o.update()
def getTemperature(self):
return self.temperature
class PhoneDisplay(IObserver):
def __init__(self,station):
self.station = station
def update(self):
print(f"{self.station} was updated to {self.station.getTemperature()}")
class WindowDisplay(IObserver):
def __init__(self,station):
self.station = station
def update(self):
print(f"{self.station} was updated to {self.station.getTemperature()}")
station = WeatherStation()
phone1 = PhoneDisplay(station)
phone2 = PhoneDisplay(station)
window = WindowDisplay(station)
station.add(phone1)
station.add(phone2)
station.add(window)
print(station.observers)
station.notify()
class IObservable():
def add(iobserver):
pass
def remove(iobserver):
pass
def notify(self):
pass
class IObserver():
def update(self):
pass
class Channel(IObservable):
def __init__(self):
self.observers = []
self.video = {"url":"http://www.youtube.com"}
def subscribe(self,iobserver):
self.observers.append(iobserver)
def unsubscribe(self,iobserver):
pass
def notify(self):
for o in self.observers:
o.update()
def getLatestVideo(self):
return self.video
class iPhone(IObserver):
def __init__(self,channel):
self.channel = channel
def update(self):
print(f"{self.channel} was updated. Watch the new video {self.channel.getLatestVideo()['url']}")
class iMac(IObserver):
def __init__(self,channel):
self.channel = channel
def update(self):
print(f"{self.channel} was updated. Watch the new video {self.channel.getLatestVideo()['url']}")
channel = Channel()
phone1 = iPhone(channel)
phone2 = iPhone(channel)
window = iMac(channel)
channel.subscribe(phone1)
channel.subscribe(phone2)
channel.subscribe(window)
print(channel.observers)
channel.notify()
State
The State pattern, a crucial behavioral pattern in object-oriented programming, allows an object to alter its behavior based on its internal state. This is achieved by defining a set of state classes, each encapsulating the behavior specific to a particular state of the object. The object then dynamically changes its state object based on internal or external events, effectively changing its behavior.
Think of it like this:
- You have a light switch (object).
- It can be in two states: on (light is shining) and off (light is not shining).
- Each state has specific behavior: the "on" state lets you turn the light off, and the "off" state lets you turn it on.
- When you flip the switch, it changes its internal state object, triggering the corresponding behavior change.
Components:
- Context: This represents the object whose behavior changes based on its state. It holds a reference to the current state object.
- State: This defines an interface that all state classes implement. It defines methods related to the behavior of the state.
- ConcreteState: This implements the
State
interface for a specific state, encapsulating its specific behavior.
Benefits:
- Modular behavior: Separates state-specific behavior into distinct classes, improving code organization and maintainability.
- Dynamic behavior change: Allows objects to change their behavior dynamically based on state transitions.
- Encapsulated state logic: Simplifies reasoning about the object's behavior by keeping state-specific logic in its own class.
When to Use It:
- When an object has multiple distinct states that significantly impact its behavior.
- When you want to avoid complex conditional logic within the object itself.
- When you need to easily add or modify state-specific behavior.
Remember:
- The State pattern can add some complexity compared to simpler approaches.
- Use it judiciously when the benefits of modular and dynamic behavior outweigh the added complexity.
- Consider the number of states and the complexity of their behavior before applying this pattern.
Examples:
- A light switch with on and off states.
- A traffic light with red, yellow, and green states.
- A game character with different states like idle, running, and attacking.
class Shape:
def accept(self, visitor):
visitor.visit(self)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
visitor.visit_circle(self)
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def accept(self, visitor):
visitor.visit_rectangle(self)
class AreaCalculator:
def visit(self, shape):
raise NotImplementedError
def visit_circle(self, circle):
print(f"Circle area: {circle.radius**2 * math.pi}")
def visit_rectangle(self, rectangle):
print(f"Rectangle area: {rectangle.width * rectangle.height}")
# Example usage
circle = Circle(2)
rectangle = Rectangle(3, 4)
calculator = AreaCalculator()
circle.accept(calculator)
rectangle.accept(calculator)
class TrafficLight:
def __init__(self):
self.state = RedState(self)
def change_state(self, new_state):
self.state = new_state
def run(self):
self.state.run()
class TrafficLightState:
def __init__(self, light):
self.light = light
class RedState(TrafficLightState):
def run(self):
print("Red light")
self.light.change_state(YellowState(self.light))
class YellowState(TrafficLightState):
def run(self):
print("Yellow light")
self.light.change_state(GreenState(self.light))
class GreenState(TrafficLightState):
def run(self):
print("Green light")
self.light.change_state(RedState(self.light))
# Example usage
light = TrafficLight()
light.run() # Output: Red light, Yellow light, Green light, Red light
Strategy
Concept:
The Strategy pattern, a powerful behavioral pattern in object-oriented programming, allows you to dynamically select and use different algorithms at runtime. Essentially, it separates the algorithm implementation from the context that uses it, promoting flexibility and decoupling.
Imagine:
- You're a sorting algorithm competition organizer (context).
- Different algorithms (strategies) like bubble sort, insertion sort, and quicksort compete.
- Your competition can dynamically use different sorting algorithms based on factors like data size or desired speed.
Components:
- Strategy: This defines the interface for all algorithms, specifying the method they implement (e.g.,
sort
). - ConcreteStrategy: This implements the
Strategy
interface, providing the specific algorithm logic for a particular variation (e.g., BubbleSort, InsertionSort). - Context: This uses the
Strategy
interface to interact with the algorithms without knowing their specific implementations. It can dynamically change theStrategy
object to use different algorithms.
Benefits:
- Flexibility: You can easily add, remove, or change algorithms without affecting the context or other strategies.
- Decoupling: Context and strategies are decoupled, promoting loose coupling and easier maintenance.
- Reusability: Strategies can be reused in different contexts.
- Open/Closed Principle: Context remains open for extension (new strategies) while being closed for modification.
When to Use It:
- When you need to use different algorithms in different situations.
- When you want to avoid tight coupling between the context and specific algorithms.
- When you want to make the algorithm selection dynamic and adaptable.
Remember:
- The Strategy pattern can add some complexity compared to embedding algorithms directly.
- Use it when the benefits of flexibility and decoupling outweigh the added complexity.
- Consider the performance implications of different strategies and the cost of switching between them.
Examples:
- A sorting algorithm framework that allows plugging in different sorting strategies.
- A compression algorithm library that supports different compression formats.
- A game AI that dynamically changes its behavior based on the situation.
class TextFormatter:
def __init__(self, strategy):
self.strategy = strategy
def format(self, text):
return self.strategy.format(text)
class UppercaseFormatter:
def format(self, text):
return text.upper()
class LowercaseFormatter:
def format(self, text):
return text.lower()
class TitlecaseFormatter:
def format(self, text):
return text.title()
# Usage example
text = "This is some text to format."
uppercase_formatter = TextFormatter(UppercaseFormatter())
print(uppercase_formatter.format(text)) # Output: THIS IS SOME TEXT TO FORMAT.
lowercase_formatter = TextFormatter(LowercaseFormatter())
print(lowercase_formatter.format(text)) # Output: this is some text to format.
titlecase_formatter = TextFormatter(TitlecaseFormatter())
print(titlecase_formatter.format(text)) # Output: This Is Some Text To Format.
# You can easily change the strategy at runtime
text_formatter = TextFormatter(LowercaseFormatter())
text_formatter.strategy = UppercaseFormatter()
print(text_formatter.format(text)) # Output: THIS IS SOME TEXT TO FORMAT.
Template Method
The Template Method pattern is a key behavioral pattern in object-oriented programming that defines the skeleton of an algorithm in a base class, while allowing subclasses to override specific steps without changing the overall structure. It's like a recipe template where you can customize certain ingredients or steps while following the general cooking process.
Here's how it works:
- Abstract Class: This defines the overall algorithm structure with methods for each step. Some methods can be marked as abstract, meaning they must be implemented in subclasses.
- Concrete Steps: Some methods in the abstract class provide the default implementation for common steps.
- ConcreteSubclasses: These inherit from the abstract class and can override specific abstract methods to customize the behavior for certain steps. They also implement any non-abstract methods.
Benefits:
- Reusability: The common algorithm structure is defined once and reused by all subclasses.
- Flexibility: Subclasses can customize specific steps to suit their needs.
- Maintainability: Changes to the overall algorithm only require modifying the base class, while specific variations remain in subclasses.
- Decoupling: Subclasses don't need to know the details of the entire algorithm, only their specific parts.
When to Use It:
- When you have a common algorithm with variations in specific steps.
- When you want to promote code reusability and maintainability.
- When you need flexibility for subclasses to customize their behavior.
Remember:
- The Template Method pattern adds some complexity compared to simpler approaches.
- Use it judiciously when the benefits of code reuse, flexibility, and maintainability outweigh the added complexity.
- Overusing it can lead to a hierarchy of complex subclasses.
Examples:
- A sorting algorithm framework with different sorting strategies implemented as subclasses.
- A banking application with different account types (checking, savings) that inherit from a base "Account" class.
- A game character AI with different enemy types that inherit from a base "Enemy" class and override specific behavior methods.
class CaffeineBeverage:
def prepare_recipe(self):
self.boil_water()
self.brew()
self.pour_in_cup()
if self.wants_condiments():
self.add_condiments()
def boil_water(self):
print("Boiling water")
def brew(self):
raise NotImplementedError
def pour_in_cup(self):
print("Pouring into cup")
def wants_condiments(self):
return True
def add_condiments(self):
print("Adding condiments")
class Coffee(CaffeineBeverage):
def brew(self):
print("Brewing coffee grounds")
class Tea(CaffeineBeverage):
def brew(self):
print("Steeping the tea leaves")
def wants_condiments(self):
return False
# Example usage
coffee = Coffee()
coffee.prepare_recipe()
tea = Tea()
tea.prepare_recipe()
Visitor
Here's a breakdown of the Visitor pattern:
Concept:
The Visitor pattern, a versatile behavioral pattern in object-oriented programming, separates an algorithm from the object structure it operates on. This allows you to define various operations on a set of objects without modifying the objects themselves. Think of it like a visitor who explores different houses (objects) using the same key to access different rooms (operations).
Components:
- Element: This represents an object in the structure. It defines an "accept" method that takes a visitor object as an argument.
- ConcreteElement: This implements the "Element" interface for specific object types.
- Visitor: This defines the interface for all operations. It has methods for each type of "Element" it can visit.
- ConcreteVisitor: This implements the "Visitor" interface and provides specific implementations for operations on different "Element" types.
Benefits:
- Decoupling: Operations are decoupled from object structure, promoting flexibility and maintainability.
- Extendability: You can easily add new operations or object types without modifying existing code.
- Reusability: Visitors can be reused across different object structures.
- Double dispatch: Enables dynamic selection of operations based on both the visitor and the element type.
When to Use It:
- When you need to perform different operations on a set of objects with varying structures.
- When you want to avoid modifying the object classes to add new operations.
- When you need flexibility to extend both the operations and the object structure.
Remember:
- The Visitor pattern can add some complexity compared to simpler approaches.
- Use it judiciously when the benefits of decoupling, extendability, and reusability outweigh the added complexity.
- Overusing it can lead to a proliferation of visitor and element classes, increasing complexity.
Examples:
- A compiler with different visitor objects for code analysis, optimization, and code generation.
- A shape drawing application with different visitors for drawing shapes in different styles.
- A database query system with different visitors for executing queries on different data types.
class Shape:
def accept(self, visitor):
visitor.visit(self)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def accept(self, visitor):
visitor.visit_circle(self)
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def accept(self, visitor):
visitor.visit_rectangle(self)
class AreaCalculator:
def visit(self, shape):
raise NotImplementedError
def visit_circle(self, circle):
print(f"Circle area: {circle.radius**2 * math.pi}")
def visit_rectangle(self, rectangle):
print(f"Rectangle area: {rectangle.width * rectangle.height}")
# Example usage
circle = Circle(2)
rectangle = Rectangle(3, 4)
calculator = AreaCalculator()
circle.accept(calculator)
rectangle.accept(calculator)
Resources
https://refactoring.guru/design-patterns/factory-method
https://github.com/wesdoyle/design-patterns-explained-with-food
Behavioral extended
Behavioral design patterns focus on how objects communicate and collaborate to achieve specific goals, ultimately enhancing the user experience in your restaurant environment. Here are some examples:
Core Concept: These patterns define how objects interact with each other to share information, handle requests, and respond to events. Implementing them effectively can enhance efficiency, communication, and customer satisfaction in your restaurant.
Specific Patterns and Restaurant Analogies:
Command: Imagine using a menu as a representation of this pattern. Each menu item acts as a "command" that triggers a specific action in the kitchen (preparing the dish).
- Example: Choosing "Spaghetti Carbonara" from the menu sends a "cook Spaghetti Carbonara" command to the kitchen staff.
Observer: Think of a table-management system that notifies waiters when a table is ready. This pattern allows one object (the table) to notify other objects (waiters) about changes in its state.
- Example: When a table is vacated and marked as "clean" in the system, all available waiters are notified, ensuring faster service for new customers.
Strategy: Imagine offering different payment options (cash, credit card, etc.). This pattern allows you to dynamically change the payment processing "strategy" based on the customer's choice.
- Example: Depending on the chosen payment method (selected on the POS system), the appropriate payment processing algorithm (strategy) is triggered.
Template Method: Think of a standard "service flow" for handling customer requests. This pattern defines a general framework (template) with specific steps that can be customized for different scenarios.
- Example: A general "order handling" template might involve taking the order, sending it to the kitchen, preparing the food, delivering it to the table, and finally processing the payment. Specific staff members would then fill in the details for each step based on their roles.
Iterator: Imagine browsing through a menu on a mobile app. This pattern allows you to access elements (dishes) in a collection (menu) sequentially without exposing the underlying structure of the collection.
- Example: The menu app uses an iterator to display dishes on the screen one by one, allowing users to navigate through the menu without needing to know the internal organization of the menu data.
Benefits of Behavioral Patterns in Restaurants:
- Improved communication: Efficient flow of information between different aspects of your operation (staff, systems, etc.).
- Reusability: Easily adapt existing communication patterns to different scenarios.
- Flexibility: Dynamically manage changing situations and user interactions.
These are just a few examples, and several other behavioral patterns can be applied in a restaurant setting. If you have a specific scenario you'd like to explore or want to know more about a particular pattern, feel free to ask!