Software Design Patterns, Principles & Architectures - robbiehume/CS-Notes GitHub Wiki
Links
- Design Patterns: reusable, lower-level solutions to common coding problems (focused on object/class design)
- Design Principles: higher-level guidelines to help developers write clean, maintainable code (focused on code quality and maintainability)
- Software Architecture: the highest-level, structural design of an entire system, focusing on its components, their relationships, and how they meet system-wide requirements (focused on system organization)
- The 7 Most Important Software Design Patterns
- Definition: design patterns are reusable solutions to common problems that software developers face. They are best practices that help structure code in a way that addresses specific issues, such as object creation or object interaction
- Focus: patterns offer solutions at the class and object level and are more concrete than design principles
- Use Case: design patterns are often implemented within a system’s design to solve particular coding problems, such as how objects should interact, be created, or behave
-
Benefits:
- Flexibility: they provide reusable solutions to common design problems, making code more adaptable to future changes.
- Separation of Concerns: many of these patterns promote separating responsibilities, which improves maintainability and readability.
- Scalability: proper use of patterns can help systems scale effectively by ensuring modularity and reducing tight coupling between components
-
Design patterns in programming paradigms:
- OOP: most design patterns are more common in OOP because they are based on concepts like inheritance, encapsulation, and polymorphism
-
Functional programming (FP): some patterns (like Strategy or Builder) can be adapted for use in FP, where functions are treated as first-class citizens
- However, patterns like Singleton and Factory are less relevant because FP tends to avoid mutable state and emphasizes function composition over object management
- Procedural programming: while patterns like Command and Template Method can be applied, procedural programming doesn't inherently encourage the same abstraction levels as OOP, so fewer patterns are needed
- Singleton Design Pattern Overview (Digital Ocean)
- Purpose: ensures a class has only one instance and provides a global point of access to it
- Common Use Cases: managing shared resources (e.g., configuration settings), database connections, logging
- Example: you might use a Singleton in a logger class where you want to ensure only one instance of the logger is used throughout the application
-
class Logger: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(Logger, cls).__new__(cls) return cls._instance def log(self, message): print(f"Log: {message}")
- Purpose: provides an interface for creating objects but allows subclasses to alter the type of objects that will be created
-
Common Use Cases:
- When object creation logic is complex or changes dynamically at runtime
- Ensuring flexibility in object creation
-
Example: a
DocumentFactory
class can be used to create different types of documents (e.g.,PDFDocument
,WordDocument
) without the client needing to know the specific class that gets instantiated -
class DocumentFactory: def create_document(self, type): if type == 'pdf': return PDFDocument() elif type == 'word': return WordDocument()
- The Factory Method and Factory Pattern are related, but they differ in scope and structure
- Both are design patterns, but Factory Method is a more specific, method-level pattern compared to the broader Factory Pattern
-
Key difference:
- Factory Method refers specifically to the method inside a class that handles object creation, allowing subclasses to override object creation
- Factory Pattern can refer to any broader factory approach that abstracts object creation, including the use of a factory class
- A single method in a class responsible for creating objects
- It defines an interface for creating objects, but allows subclasses to alter the type of object that will be created
- It allows subclasses to decide which class to instantiate, providing flexibility in object creation
- It promotes loose coupling by allowing a class to defer instantiation to the subclasses
-
Example: A superclass defines a method
createObject()
, but the subclass decides the specific type to instantiate
- A broader design pattern that abstracts object creation into a dedicated factory class or interface
- The factory class handles the entire process of creating objects, often without the client needing to know the exact type of object being created
- Purpose: defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from clients that use it
-
Common Use Cases:
- Implementing different payment methods (credit card, PayPal, etc.)
- Swapping algorithms (e.g., different sorting strategies) at runtime
- Example: you could use the Strategy pattern to switch between different sorting algorithms based on user input
-
class SortingStrategy: def sort(self, data): raise NotImplementedError class QuickSort(SortingStrategy): def sort(self, data): return sorted(data) class BubbleSort(SortingStrategy): def sort(self, data): # Implement Bubble Sort
- Purpose: defines a one-to-many relationship where one object (the subject) changes state and all its dependents (observers) are notified and updated automatically
-
Common Use Cases:
- Event-driven systems like GUIs
- Real-time systems that notify multiple subscribers of changes
- Example: you might use the Observer pattern to implement a stock market ticker where several clients subscribe to get updates on stock prices
-
class Stock: def __init__(self): self._observers = [] def subscribe(self, observer): self._observers.append(observer) def notify(self, price): for observer in self._observers: observer.update(price)
- Pub-Sub (Publisher-Subscriber) is closely related to the Observer Pattern, but there are key differences between the two, making Pub-Sub a distinct pattern in its own right
-
Similarities to Observer Pattern:
- Both patterns deal with communication between components, where one component (the publisher or subject) sends notifications to others (subscribers or observers)
- In both patterns, when something happens (like a state change), multiple components get notified and respond to it
-
Key differences:
-
Coupling:
- Observer Pattern: the observer is tightly coupled to the subject. Observers know the subject they are subscribed to, and the subject directly notifies the observers
- Pub-Sub Pattern: publishers and subscribers are decoupled. They don't know about each other directly. Instead, a message broker or event bus acts as a mediator. Publishers send messages to a channel or topic, and subscribers receive those messages from the channel
-
Communication Model:
- Observer Pattern: the subject directly notifies all observers when an event occurs
- Pub-Sub Pattern: the publisher sends messages to a message broker, which then distributes them to all subscribers interested in that topic. This allows for more flexibility, as publishers and subscribers don’t need to know about each other
-
Coupling:
-
Pub-Sub use cases:
- Distributed systems or microservices where components need to communicate but should remain decoupled
- Messaging systems (e.g., Apache Kafka, RabbitMQ) where components publish events and other components subscribe to and process these events
-
Observer Use Cases:
- More commonly used in object-oriented applications for event-driven programming within a single application (e.g., UI updates in response to user actions)
-
Conclusion:
- Pub-Sub is a more general communication pattern used in distributed systems, while Observer is often used in more tightly coupled, object-oriented scenarios
- Though related, Pub-Sub is distinct from the Observer Pattern and offers greater decoupling, making it more suitable for large, distributed applications
- Purpose: separates the construction of a complex object from its representation, allowing the same construction process to create different representations
-
Common Use Cases:
- Constructing objects with many optional parameters
- Avoiding complex constructor overloads
-
Example: the Builder pattern is useful when creating complex objects like a
Pizza
where different combinations of toppings or sizes are possible -
class PizzaBuilder: def __init__(self): self.pizza = Pizza() def add_cheese(self): self.pizza.add("cheese") def add_pepperoni(self): self.pizza.add("pepperoni") def get_pizza(self): return self.pizza
- Purpose: allows behavior to be added to individual objects dynamically without modifying their structure
- Common Use Cases:
- Adding features like logging, authentication, or caching to objects
- Extending functionality of objects without altering their classes
- Example: use the Decorator pattern to add features like authentication to a service object without modifying the base service class
-
class AuthDecorator: def __init__(self, service): self.service = service def request(self, user): if user.is_authenticated(): return self.service.request(user) else: return "Authentication required"
- Purpose: allows incompatible interfaces to work together by wrapping one interface with another
-
Common Use Cases:
- Integrating legacy systems with new systems
- Allowing classes with different interfaces to communicate
- Example: suppose you have a legacy media player that uses an old interface. You can create an adapter to make it compatible with a modern media library
-
class MediaAdapter: def __init__(self, legacy_player): self.legacy_player = legacy_player def play(self, file): self.legacy_player.play_media(file)
- Purpose: allows an object to alter its behavior when its internal state changes. The object will appear to change its class by delegating its behavior to different state objects
-
Common Use Cases:
- When an object’s behavior depends on its state, and it must change behavior at runtime based on internal changes
- Implementing finite state machines (e.g., a media player that behaves differently when in play, pause, or stop states)
-
Example: the State Pattern can be used for an online order system where an order can be in different states like
NewOrder
,Processing
,Shipped
, andDelivered
. Each state might have different behavior for actions like canceling or modifying the order -
class OrderState: def handle(self, order): raise NotImplementedError class NewOrder(OrderState): def handle(self, order): print("Processing new order...") class Shipped(OrderState): def handle(self, order): print("Order already shipped. Cannot cancel.") class Order: def __init__(self): self.state = NewOrder() def change_state(self, state): self.state = state def process(self): self.state.handle(self)
- In this example, the behavior of the
Order
object changes dynamically based on its internal state. This makes the code cleaner and easier to manage compared to having complexif-else
orswitch
statements inside theOrder
class
- In this example, the behavior of the
- Definition: design principles are fundamental guidelines that lead to better code structure and system maintainability. These are high-level rules that help prevent common problems in software development, such as tightly coupled code or hard-to-modify systems
- Focus: they address the quality of software and help developers write more maintainable, scalable, and flexible code
- Use Case: design principles guide developers in making informed choices when writing or refactoring code, ensuring it is clean, maintainable, and follows best practices
- Loose-coupling is where one object is not totally dependent on another object and it could be replaced easily in the future
- It is the preferred way
SOLID Design Principle (link)
-
S
ingle-responsibility principle:- There should never be more than one reason for a class to change. In other words, every class should have only one responsibility / job
-
O
pen-closed principle:- Objects or entities should be open for extension, but closed for modification
-
L
iskov substitution principle:- Every subclass or derived class should be substitutable for their base or parent class
-
I
nterface segregation principle:- A client should never be forced to implement an interface that it doesn't use, or clients shouldn't be forced to depend on methods that they don't use
-
D
ependency inversion principle:- Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, they should depend on abstractions
- Definition: architecture is the high-level structure of an entire system. It defines how software components (subsystems, services, etc.) interact, how they are organized, and how they meet both functional and non-functional requirements
- Focus: architecture handles the overall system design and its interaction with external systems, ensuring scalability, performance, security, and fault tolerance
- Use Case: software architecture defines how the components of a system are organized and interact, ensuring the system meets its goals, such as being scalable, resilient, or performant. It's critical when designing large, distributed systems like web applications or enterprise-level software
- Represents the data and business logic of the application
- Handles data manipulation, interactions with the database, and any calculations or rules that define how data is processed
- Acts as the central source of truth for the application's data
-
Example: In a blog application, the
Post
model would handle the structure and behavior of a blog post (e.g., title, content, author) and how it’s stored or retrieved from the database
- Handles the presentation layer and defines how data is displayed to the user
- Displays information from the model but does not directly modify it
- Can be represented by templates, HTML, or any visual elements that the user interacts with
- Example: The view would render a list of blog posts on a page, showing the title and content in a user-friendly format
- Handles input from the user and updates the Model and/or View accordingly
- Acts as an intermediary between the Model and View
- Receives user input, processes it (often by interacting with the Model), and then updates the View accordingly
- Defines the application's flow, routing requests to the appropriate model and view
- Example: The controller takes a request to view a specific blog post, retrieves the relevant data from the Post model, and then passes it to the view to display
- Separation of concerns: Each component has a specific role, which makes the application easier to manage and maintain.
- Scalability and flexibility: MVC is widely used because it allows for modular design, making it easier to scale and modify individual components without affecting the entire application
- Separates concerns into different layers, typically:
- Presentation Layer: handles user interaction (similar to View)
- Business Logic Layer: processes data (similar to Model)
- Data Access Layer: manages database interaction
- YouTube video
-
Monolith
- Server-side system based on single application
- Easy to develop, deploy, and manage
- Challenges:
- Each part is highly dependent on other parts, something tends to break over time
- Each part must be written in the same language / framework
- Adding features becomes harder over time
- Hard to scale certain parts
-
Microservices
- Each app function is its own independent containerized component and they interact with each other through APIs
- Advantages
- Allows you to use different languages / frameworks for different components
- Because they are independent components, they can be deployed at different times
- If one microservice goes down, the rest of the app is still functional
- Can scale each component individually