Reality Bytes: Building for the Real - Strategy design pattern

Let's do it!
Today, we're continuing to build our REST API using design patterns. Last time, we used the Bridge pattern to separate the DataFetcher abstraction from the BaseService abstraction, allowing them to work together to connect to different data sources in an extensible way. In this article, we're going to use the Strategy pattern to implement XML and JSON formatting on demand for our data. Cool!
Introduction to the Strategy Pattern
The Strategy pattern is a behavioral software design pattern that allows selecting an algorithm at runtime. Rather than implementing a single algorithm directly, the code receives runtime instructions to choose from a family of algorithms.
Why Use the Strategy Pattern?
The Strategy pattern is useful becuse it provides flexibility by allowing algorithms to be selected at runtime without changing the client code. It helps separate different behaviors into distinct classes, making the code cleaner, more maintainable, and easier to extend. By avoiding complex conditionals, the pattern promotes reusability and testability, as new algorithms can be added without modifying existing code. Overall, it improves the scalability and organization of the application.
Common Uses
The Strategy pattern is commonly used in situations where multiple algorithms or behaviors are available for a particular task, and the choice of which one to use depends on specific conditions. It’s useful in scenarios like:
Data Formatting: Different output formats (e.g., JSON, XML) that can be selected dynamically based on the client’s request. Payment Systems: Allowing users to select different payment methods (e.g., credit card, PayPal) without modifying the core processing logic. Sorting Algorithms: Choosing different sorting strategies (e.g., quicksort, mergesort) depending on the data characteristics. Authentication Methods: Selecting different authentication strategies (e.g., password-based, token-based) for security. In these cases, the Strategy pattern helps keep the code modular and flexible for future changes.
Components
The Strategy pattern consists of three main components:
Strategy Interface Defines a common interface for all supported algorithms or behaviors, ensuring they can be used interchangeably.
Concrete Strategies Implement the specific algorithms or behaviors defined by the Strategy interface.
Context The class that uses a reference to the Strategy interface to call the algorithm. It is responsible for selecting and switching between different strategies as needed.
Together, these components allow for dynamic switching of algorithms or behaviors without altering the code that uses them.
Bringing the Strategy Pattern to Life: Real-World API Response Formatting
Now that we’ve covered the concept of the Strategy Pattern, let’s make it concrete with a real-world example. We’ll demonstrate how to use this pattern in a REST API to dynamically format responses as either JSON or XML based on client preferences. For simplicity, we'll integrate two external services: CoinCap, for cryptocurrency data, and AnimeChan, which provides anime-related quotes. These APIs were chosen because they are free and require no authentication.
The Strategy Pattern shines here by separating the logic for formatting the API response (JSON or XML) from the service logic that fetches and processes the data. This decoupling allows us to easily extend the system to support new formats or adjust behavior without modifying the core service code.
Our implementation involves defining a ResponseContext that selects the appropriate formatting strategy—JSONResponseStrategy or XMLResponseStrategy—based on the client’s Accept header. Each strategy implements a consistent interface to ensure the ResponseContext can handle them interchangeably.
Defining the strategy interface and Its Subclasses
To begin, we define a strategy interface that outlines the behavior for formatting responses. This interface ensures that all strategies conform to a consistent structure, making them interchangeable. Here's the implementation:
from abc import ABC, abstractmethod
from fastapi.responses import JSONResponse
from fastapi import Response
from xml.etree.ElementTree import Element, tostring
class ResponseFormatStrategy(ABC):
@abstractmethod
def format_data(self, data: dict) -> Response:
"""
Abstract method that must be implemented by any concrete strategy.
It takes a dictionary of data and returns a formatted response.
"""
pass
Concrete strategy classes
Next, we define the concrete strategy classes that implement the ResponseFormatStrategy interface. These classes handle the actual formatting of data into specific response types, such as JSON or XML:
JSONResponseStrategy
class JSONResponseStrategy(ResponseFormatStrategy):
def format_data(self, data: dict) -> Response:
"""
Formats the given data as a JSON response.
"""
return JSONResponse(content=data
XMLResponseStrategy
class XMLResponseStrategy(ResponseFormatStrategy):
def format_data(self, data: dict) -> Response:
"""
Formats the given data as an XML response.
"""
root = Element("response")
for key, value in data.items():
child = Element(key)
child.text = str(value)
root.append(child)
return Response(content=tostring(root), media_type="application/xml")
ResponseContext
Now that we have implemented the strategy classes, let's define the context class, ResponseContext. This class acts as the orchestrator, determining which strategy to use based on the request's Accept header and delegating the formatting to the chosen strategy:
from fastapi import Request
from rest_api_bridge.strategies.response_strategy import JSONResponseStrategy, XMLResponseStrategy
class ResponseContext:
def __init__(self, request: Request):
self._strategy = self._get_strategy(request.headers.get("Accept"))
def _get_strategy(self, accept_header: str):
if 'application/xml' in accept_header:
return XMLResponseStrategy()
return JSONResponseStrategy()
def execute_strategy(self, data:dict):
return self._strategy.format_data(data)
Finally, we integrate the ResponseContext into our application to dynamically return data in the correct format, either JSON or XML, based on the client's Accept header.
Anime Quote Endpoint:
Here, the get_anime_quote endpoint fetches a random anime quote, processes the data, and delegates the response formatting to the ResponseContext:
from fastapi import APIRouter, Request from rest_api_bridge.apis.anime_api import AnimeApi from rest_api_bridge.services.anime_service import AnimeService from rest_api_bridge.strategies.response_context import ResponseContext
anime_api_impl = AnimeApi() anime_service = AnimeService(anime_api_impl)
anime_router = APIRouter()
@anime_router.get("/anime/quote/") async def get_anime_quote(request: Request): """ Fetch a random anime quote from the AnimeChan API and return it in the requested format. """ endpoint = "https://animechan.io/api/v1/quotes/random" raw_data = await anime_service.fetch_data(endpoint) processed_data = anime_service.process_data(raw_data) data = { "message": "Random Anime Quote", "data": processed_data if processed_data else "Failed to fetch Anime Quote" }
# Use ResponseContext to format the response correctly
context = ResponseContext(request)
return context.execute_strategy(data)
CoinCap Data Endpoint:
Similarly, the get_coincap_data endpoint fetches cryptocurrency data from CoinCap, processes it, and formats the response using the same
from fastapi import APIRouter, Request
from rest_api_bridge.apis.coincap_api import CoinCapAPI
from rest_api_bridge.services.coincap_service import CoinCapService
from rest_api_bridge.strategies.response_context import ResponseContext
coincap_api_impl = CoinCapAPI()
coincap_service = CoinCapService(coincap_api_impl)
coincap_router = APIRouter()
@coincap_router.get("/coincap/data/")
async def get_coincap_data(request: Request):
"""
Fetch data from the CoinCap API and return it in the requested format.
"""
endpoint = "/assets"
raw_data = await coincap_service.fetch_data(endpoint)
processed_data = coincap_service.process_data(raw_data)
data = {
"message": "CoinCap Data",
"data": processed_data if processed_data else "Failed to fetch CoinCap data"
}
# Use ResponseContext to format the response correctly
context = ResponseContext(request)
return context.execute_strategy(data)
The ResponseContext dynamically adapts the response format to the client's preferences by interpreting the Accept header. If the header specifies application/json, the response is returned in JSON format. Alternatively, if application/xml is specified, the response is formatted as XML. This design keeps the codebase clean and modular, allowing for easy addition of new response formats or integration with additional APIs. By leveraging the Strategy Pattern, we achieve a flexible and extendable solution for handling diverse client requirements.
To view the complete code, check out the branch https://github.com/gizmoGremlin/fast_api_python_bridge/tree/strategy-pattern