Software Design Patterns, Principles & Architectures - robbiehume/CS-Notes GitHub Wiki

Links


Comparison (low to high level)

  • 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)

Design Patterns

  • 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 Pattern

  • 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}")

Factory Pattern

  • 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()

Factory Method / Factory Pattern

  • 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

Factory Method:

  • 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

Factory Pattern:

  • 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

Strategy Pattern

  • 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

Observer Pattern / Pub-Sub

Observer Pattern

  • 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

  • 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
  • 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

Builder Pattern

  • 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

Decorator Pattern

  • 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"

Adapter Pattern

  • 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)

State Pattern

  • 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, and Delivered. 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 complex if-else or switch statements inside the Order class

Design Principles

  • 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

Cohesion vs Coupling

Loose-coupling vs Tight-coupling

  • 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)

  • Single-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
  • Open-closed principle:
    • Objects or entities should be open for extension, but closed for modification
  • Liskov substitution principle:
    • Every subclass or derived class should be substitutable for their base or parent class
  • Interface 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
  • Dependency 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

Software Architecture

  • 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

MVC (Model-View-Controller)

Model (Data):

  • 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

View (UI):

  • 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

Controller (Logic):

  • 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

Key benefits

  • 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

Layered architecture

  • 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

Monolith vs Microservices

  • 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
⚠️ **GitHub.com Fallback** ⚠️