We covered 3 types of design patterns (in python language)
- Creational
- Structural
- Behavioural
Patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation
Ensures a class has only one instance and provides a global access point to it. Commonly used for shared resources such as databases, loggers, or network connections
PROS:
-
Controlled access to shared resources
-
Reduced memory footprint (only one instance)
-
Consistent state across system
CONS:
-
Breaks Single Responsibility Principle (SRP)
-
Harder to test (hidden dependencies)
-
State persists for application lifetime
Output of singleton.py:
$ uv run singleton.py
<__main__.DatabaseDriver object at 0x1022e35f0>
<__main__.DatabaseDriver object at 0x1022e35f0>
Output of singleton_multithread.py
$ uv run python singleton_multithread.py
<__main__.DatabaseDriverThreaded object at 0x102ca7350>
<__main__.DatabaseDriverThreaded object at 0x102ca7350>
A method that provides a separation between the client (user) and the actual object creation
-
The client only calls the factory method.
-
The concrete factory class decides which product (object) to instantiate.
-
This hides the creation logic from the client
PROS:
- Requirements change
- Dynamic Switching
- Seperation of Concern
CONS:
- Increased complexity
- More class and abstraction
Output of factory_method.py
$ uv run factory_method.py
USD
Bitcoin
EUR
Etherium
It's one level above of the Factory Method; instead of creating a single product, it can create multiple related products it dosen't directly create objects itself, but rather acts as a factory of factories
PROS:
- provides a way to access functionality without caring about implementation
- Seperation of concerns
- Allows for testability
- Consistency in related products
CONS:
- Increased Complexity
- Difficult to add a new product of families (all concrete factories must be updated)
quivalent: Factory method: creates a single object Abstract Factory: creates a family of related objects
Output of abstract_factory.py
$ uv run abstract_factory.py
Persian Restaurant: GhormeSabzi! doogh Abali
American Restaurant: Hamburger Coca Cola
Used to construct complex objects step by step, especially when objects have many optional or required parameters. Separates the construction of an object from its representation, allowing the same construction process to create different representations.
PROS:
- Clarity
- Immutability
- Flexibility
- Separation of concerns
CONS:
- More code
- Overkill for simple objects
- Harder to trace/debug
Output of builder.py (classic_builder.py has same output)
$ uv run creational/builder/builder.py
{'URL': 'google.com'}
{'URL': 'youtube.com', 'Authorization': 'abc123', 'Cache-Control': 100000}
Used when you need to copy an existing object without depending on its concrete class The copied object must provide proper cloning implementation Useful in testing, pre-production or when object creation is expensive
PROS:
- Avoids subclassing for creating copies
- Improves performance by reusing existing objects
- Dynamic object creation at runtime
CONS:
- Shallow copies may lead to shared references
- Objects must provide proper cloning implementation
- May increase complexity if object graph is deep
- Tight coupling
output of prototype.py:
uv run prototype.py
Background color is red
Drawing a square of size: 5
Drawing a square of size: 3
Drawing a Circle of radius: 8
Background color is red
Drawing a square of size: 5
Drawing a square of size: 3
Drawing a Circle of radius: 8
Patterns that focus on how classes and objects are composed to form larger structures, improving flexibility, testability, and scalability
The Adapter is a structural design pattern that makes two incompatible classes work together. It is especially useful when you cannot or do not want to modify the existing classes
PROS:
- Reuse existing code without modification
- Improves flexibility when integrating with third-party or legacy code
- Separates concerns — client doesn’t need to know about underlying incompatible code
- Makes systems more extensible with multiple adapters
CONS:
- Adds extra complexity and classes to the system
- Too many adapters may make the architecture harder to maintain
- Can reduce performance slightly due to the additional indirection
output of adapter.py:
uv run adapter.py
3rd party functionality 2.0 - 2
3rd party functionality 3.0 - 2
3rd party functionality 2.0 - 5
3rd party functionality 6.0 - 1
The Bridge is an abstraction from its implementation so that the two can vary independently. It’s especially useful when you have multiple orthogonal hierarchies (e.g., different devices and different types of remotes) and want to avoid a combinatorial explosion of classes.
Key Idea: Instead of creating subclasses for every possible combination of abstraction and implementation, you bridge them via composition.
PROS
- Decouples abstraction from implementation → easier to extend both independently
- Increases flexibility and scalability
- Improves maintainability (each hierarchy evolves separately)
- Avoids combinatorial class explosion
CONS
- Adds extra complexity with more classes and indirection
- Harder for beginners to understand the relationship between abstraction and implementation
- May be overkill for small/simple hierarchies
Output of bridge.py:
Radio <__main__.Radio object at 0x100550110> volume up: 1
Radio <__main__.Radio object at 0x100550110> volume up: 2
Radio <__main__.Radio object at 0x100550110> volume down: 1
TV <__main__.TV object at 0x100550200> volume up: 1
TV <__main__.TV object at 0x100550200> volume down: 0
TV <__main__.TV object at 0x100550200> volume up: 1
TV <__main__.TV object at 0x100550200> volume down: 0
The Composite pattern allows you to compose objects into tree-like structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly, so you can manipulate many objects as a single entity.
Use cases:
Represent hierarchical structures (e.g., file system, organization charts). Treat single objects and groups of objects in the same way. Reduce client-side complexity when working with recursive structures.
PROS:
- Simplifies client code by treating individual and composite objects uniformly.
- Makes it easy to add new components.
- Can represent complex hierarchies clearly.
CONS:
- Can make the design more complex if overused.
- Might require careful handling of recursion and tree traversal.
- Equivalent concepts:
- Tree structures in data.
- Polymorphism is often used to allow uniform treatment of leaf and composite nodes.
output of composite.py:
$ uv run composite.py
Total price of PC: 1425
The Decorator (or Wrapper) pattern is used to extend or modify the behavior of a class without changing its source code. It is especially useful when dealing with third-party classes or libraries over which you have no control, but you want to alter their functionality or add new features.
Key ideas:
- Attach new behavior to an object dynamically at runtime.
- Override existing behavior without modifying the original class.
- Respect the Open/Closed Principle — open for extension, closed for modification.
- Can be used for personalization, logging, caching, or security features.
PROS:
- Add new features without modifying original code.
- Combine multiple decorators to build complex functionality.
- Improves code flexibility and reusability.
CONS:
- Increases number of classes and complexity.
- Can make the object graph harder to follow.
- Too many nested decorators can be difficult to debug.
- Usage example: wrapping a basic coffee machine to add milk coffee preparation, while still keeping original small and large coffee functionality intact.
output of wrapper.py:
Basic CoffeeMachine making small coffee
Enhanced Coffee Machine making large coffee
Enhanced coffee machine making milk coffee
Basic CoffeeMachine making small coffee
Enhanced coffee machine adding milk
The Facade design pattern provides a simple interface to a larger body of complex code, such as a library, framework, or subsystem. It hides internal complexities and exposes only the parts that are necessary for the client.
PROS:
- Hides away complexity behind a single, simple interface
- Provides a clear and unified entry point to complex functionality
- Removes the need for direct object/memory management in the client
- Simplifies client implementation and usage
- Encourages separation of concerns (client doesn’t need to know system details)
CONS:
- Can become a god-object if too much responsibility is centralized
- Risk of reducing flexibility by exposing only limited functionality
output of facade.py:
Storing cache data to file ./default.prefs
John
The Flyweight design pattern is used when a system needs to handle a large number of similar objects. Instead of creating a new object for every instance, it reuses (shares) existing objects to reduce memory footprint and improve efficiency.
PROS:
- Reduces memory usage by sharing common state between objects
- Improves performance when handling many similar objects
- Decouples intrinsic (shared) state from extrinsic (unique) state
CONS:
- Increases code complexity (requires splitting state into intrinsic vs. extrinsic)
- May complicate object lifecycle management
- Can reduce clarity if overused
output of flyweight.py
$ uv run structural/flyweight/flyweight.py
S S P M S S S M S S S M S P P M P P P M M P S P M S S S M M S S S P P S P S P S M M P S M S S M S S M M M P M M P M M M M P P P M S M M P S S P P S P M M S P S P S P M S M P S P P P S P S S M S P M S P M P P S M M S P M S S P S S M S P M S P P S S S S S P P P S S P P S P P S P P S P M P P P M S M M P S P P S M M P M P M P S P S S M M S M M P S P S S S S P S P S M P M S P M S M P M P M M M P S M P M S P S M P P S M M M S M M M P S P S P M P M P P S M P P S S P P P S P M S S S M P P M P M P S M S M P M P P M P M P S M P S M S M M S M S M P S S P S P M S S P P S S S P M P S M P P S M P S M M S M M P M S M S P P S S P M M S M P M S M P P M S S S M P M S S S P S S S P S M M M M P S M M P S P S P S S S M M P M M P M P P S P P M P S P S S P M M P P M P P M P P P M P M S S S P M P S P P S M M P S M P S M M S P P S S M M M S P P M M P P M S M P S S M M P P M P P S M M P M S S S S P S S P S P S S P P S P M M P P P M P M P M P M S M S S S P S M P P P S S S S P M P S S S P M M S P M M M M P P M P %
A Proxy is a structural design pattern that provides a placeholder or surrogate object that controls access to another object. It implements the same interface as the real object so that it can be used interchangeably by clients.
Key points:
- Controls access to the real object (lazy initialization, security checks, logging, caching, etc.)
- Manages the lifecycle of the real object (create on demand, destroy, reset, etc.)
- Clients don’t need to know whether they are talking to the proxy or the real object.
PROS
- Can add access control, caching, logging, or lazy loading transparently
- Protects the real object from unauthorized access or heavy load
- Makes system more flexible (different proxies for different needs: remote proxy, virtual proxy, protection proxy, etc.)
- Keeps the client code unchanged (since proxy has the same interface)
CONS
- Adds an extra layer of indirection (slight overhead)
- Can increase complexity if overused
- Must carefully ensure proxy and real subject remain consistent with the same interface
Difference with Similar Patterns:
- Facade → provides a simplified interface to a complex subsystem. It doesn’t mimic the real object’s interface.
- Decorator → extends or modifies behavior of an object without managing its lifecycle.
- Proxy → looks like the real object (same interface) but manages its access/lifecycle.
output of proxy.py
$ uv run proxy.py
Proxy image: Displaying : test.jpg
From disk
Real image: Loading test.jpg
Real Image: Displaying test.jpg
Proxy image: Displaying : test.jpg
From Cache
Real Image: Displaying test.jpg
Patterns that define how objects and classes interact and distribute responsibility
allows you to pass a request along a chain of handlers. Each handler either processes the request or forwards it to the next handler in the chain
This helps decouple the sender of the request from the receiver since the sender does not need to know which handler will take care of it
PROS
- Loose coupling between sender and receiver.
- Easy to add or remove handlers without modifying the client code.
- Increases flexibility in assigning responsibilities to different handlers.
- Makes code open for extension (add new handlers) but closed for modification (don’t touch existing ones).
CONS
- Debugging can be hard: not always clear which handler will process a request.
- If no handler processes the request, it might get lost silently.
- Performance overhead if the chain is long (request travels through many handlers).
Use Cases
- Event handling systems (UI frameworks where events are passed from child → parent)
- Middleware pipelines (e.g., Django/Express middlewares, logging chains)
- Customer support systems (L1 → L2 → Manager)
- Request validation pipelines
output of chain_of_responsibility.py:
$ uv run chain_of_responsibility.py
Header with authentication
Authorization: 123456
ContentType: json
Body: {"Username":"John"}
Header without authentication
ContentType: json
A Command pattern turns a request into a standalone object containing all information about the request. This object can then be passed, queued, logged, or assigned to different handlers without knowing who or how it will be executed
Use Cases
- The invoker only knows it should call execute() on a command
- The receiver is the actual object that knows how to perform the action
- The command object sits in between and fully decouples sender from receiver
- Useful for Undo/Redo, task scheduling, logging, and macro recording
PROS
- Decouples sender (client) from receiver (executor)
- Requests can be queued, logged, undone, or redone easily
- Supports ordering, scheduling, and prioritization
- Improves flexibility when adding new commands
CONS
- Increases number of classes (command per action)
- May add complexity for simple actions
output command.py:
$ uv run command.py
Adding order with id: 1
Adding order with id: 2
Paying for order with id : 1
Paying for order with id : 2
Interpreter Design Pattern
Interprets a language or expression based on a defined grammar. Useful for parsing, evaluating expressions, and building small domain-specific languages (DSLs). Can be implemented recursively to evaluate nested expressions
Components:
-
Terminal Expression – the simplest, indivisible elements of the grammar (e.g., numbers, keywords)
-
Non-Terminal Expression – composed of one or more Terminal or Non-Terminal expressions; typically evaluated recursively (e.g., addition, subtraction, conditional statements)
PROS:
- Simplifies the design of complex parsing logic
- Makes it easy to extend the language by adding new expressions
- Provides a clear separation between grammar and evaluation logic
CONS:
- Can become inefficient for large grammars or deep recursive structures
- Hard to maintain if the grammar is complex
- May require significant memory and stack space for recursion
- Not suitable for general-purpose programming languages with large syntax
Usage Examples:
- Simple expression evaluators (math calculators)
- nterpreting configuration files or DSL scripts
- Building rule engines
output of interpreter.py:
$ uv run interpreter.py
19.4
Provides a way to access the elements of a collection sequentially without exposing its underlying representation
Key points:
- Traverse a collection (array, list, tree, etc.)
- Implements two main methods:
- has_next() – checks if there are more elements to iterate over
- next() – returns the next element in the sequence
Use Cases:
- When you want to iterate over a collection but hide its internal structure
- When you need multiple ways of iterating over the same collection
PROS:
- Decouples collection from traversal logic
- Multiple iterators can coexist for the same collection
- Simplifies client code
CONS:
- Adds extra classes/interfaces for iteration
- Slight overhead if used for very simple or small collections
output of iterator.py:
$ uv run iterator.py
Alex
Carol
John
Michael
Michael
John
Carol
Alex
Mediator is a central object that defines how components interact Instead of components talking directly to each other, they communicate through the mediator
This is common in:
-
Microservices, where services don’t call each other directly but communicate via message broker / mediator (e.g., Kafka, RabbitMQ).
-
UI Components, where buttons, dropdowns, text fields communicate through a central controller.
PROS
- Reduces coupling → components don’t depend on each other
- Centralized communication → easier to maintain and extend
- Improves scalability → new components can be added without breaking others
- Encourages single responsibility → each component only handles its own job
CONS
- Mediator can become very complex (God object) if it handles too many * interactions
- Performance bottleneck → all communication passes through the mediator
- Hard to debug → indirect communication makes tracing harder
output of mediator.py
$ uv run mediator.py
Carol Sending message Hi Everyone!
Alice Recieving message Hi Everyone!
Bob Recieving message Hi Everyone!
Allows an object to save and restore its internal state without exposing its implementation details. Useful for Undo/Redo functionality, snapshots, or state rollback
Components:
- Memento – stores the internal state of the object
- Originator – creates a memento containing a snapshot of its current state and can restore its state from a memento
- Caretaker – manages mementos (e.g., keeps history, decides when to save/restore)
Use cases:
- Undo/Redo in text editors or drawing apps
- Saving checkpoints in games
- Rolling back configuration or transaction states
PROS:
- Encapsulates state without exposing implementation
- Simplifies undo/redo operations
- Keeps history clean and separated from business logic
CONS:
- Can consume a lot of memory if state snapshots are large
- Complexity grows if there are many states or objects to track
- Only stores state, not behavior; restoring may require careful handling of dependencies
output of memento.py:
$ uv run memento.py
Current state is initial-state
Current state is : state-1
Current state is : state-2
Current state is : state-1
Current state is : initial-state
Current state is : state-2
The Observer is a behavioral design pattern that defines a subscription mechanism. When the state of one object (the Subject) changes, all subscribed objects (Observers) are automatically notified
Key Points:
- One-to-many relationship: one Subject, multiple Observers
- Observers are informed of changes in the Subject
- Reduces coupling between Subject and Observers
Pros:
- Loose coupling between Subject and Observers
- New Observers can be added or removed easily
- Suitable for implementing event-driven systems
Cons:
- A large number of Observers can affect performance
- The order of notifications may be important and hard to control
- Debugging can become challenging in complex systems
output of observer.py:
$ uv run observer.py
Editor: Opening file log_new.log
Save to log log.log Someone has performed open operation on the file log_new.log
Editor: Saving file log_new.log
Save to log log.log Someone has performed save operation on the file log_new.log
Email to test@gmail.com: Someone has performed event type: save operation on the file : log_new.log
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class. At any point a program can be in a finite number of states, and each state can be encapsulated in a separate object
PROS
- Eliminates large conditional statements (if-else) for behavior changes
- Improves maintainability and scalability
- Each state is encapsulated and can evolve independently
CONS
- Can increase the number of classes in the system
- If the number of states is large, the overall design can become complex
output of state.py
$ uv run state.py
Curently in <__main__.WelcomeScreenState object at 0x102a0cfe0> state
1
Move to playing
Curently in <__main__.PlayingState object at 0x102a0f5f0> state
0
Move to Welcome
From playing to welcome screen not allowed
2
Move to break
Curently in <__main__.BreakState object at 0x102a0cfe0> state
0
Move to Welcome
From break to welcome not allowed
0
Move to Welcome
From break to welcome not allowed