DEV Community

Maksym
Maksym

Posted on

Creational Design Patterns in Python. Part II

In previous article, we looked at patterns such as Singleton, Factory and Abstract Factory.
Today we would focus on remaining 2: Builder and Prototype.

Builder Pattern

The Builder pattern constructs complex objects step by step, allowing you to create different representations using the same construction process.

import requests

class APIRequest:
    def __init__(self):
        self.method = "GET"
        self.url = ""
        self.headers = {}
        self.params = {}
        self.json = None

    def __str__(self):
        return (
            f"{self.method} {self.url}\n"
            f"Headers: {self.headers}\n"
            f"Params: {self.params}\n"
            f"Body: {self.json}"
        )

class APIRequestBuilder:
    def __init__(self):
        self.request = APIRequest()

    def set_method(self, method: str):
        self.request.method = method.upper()
        return self

    def set_url(self, url: str):
        self.request.url = url
        return self

    def add_header(self, key: str, value: str):
        self.request.headers[key] = value
        return self

    def add_query_param(self, key: str, value: str):
        self.request.params[key] = value
        return self

    def set_json_body(self, data: dict):
        self.request.json = data
        return self

    def build(self):
        return self.request

    def send(self):
        print("Sending request:")
        print(self.request)
        print("-" * 50)
        response = requests.request(
            method=self.request.method,
            url=self.request.url,
            headers=self.request.headers,
            params=self.request.params,
            json=self.request.json
        )
        return response

class GitHubRequestDirector:
    def __init__(self, builder: APIRequestBuilder):
        self.builder = builder

    def get_user_repos(self, username):
        return (
            self.builder.set_method("GET")
                        .set_url(f"https://api.github.com/users/{username}/repos")
                        .add_header("Accept", "application/vnd.github.v3+json")
                        .build()
        )

if __name__ == "__main__":
    # Example 1: POST to JSONPlaceholder
    post_builder = APIRequestBuilder()

    response = (
        post_builder.set_method("POST")
                    .set_url("https://jsonplaceholder.typicode.com/posts")
                    .add_header("Content-Type", "application/json")
                    .set_json_body({
                        "title": "Builder Pattern",
                        "body": "Making API calls easier",
                        "userId": 42
                    })
                    .send()
    )

    print("Status:", response.status_code)
    print("Response JSON:", response.json())
    print("=" * 80)

    # Example 2: GET GitHub repos using a director
    github_builder = APIRequestBuilder()
    director = GitHubRequestDirector(github_builder)
    github_request = director.get_user_repos("octocat")

    response = requests.request(
        method=github_request.method,
        url=github_request.url,
        headers=github_request.headers
    )

    print("GitHub Repos for octocat:")
    for repo in response.json()[:3]:  # show only first 3
        print("-", repo["name"])

Enter fullscreen mode Exit fullscreen mode

Use Cases

  • SQL query builders
  • Configuration builders
  • Document builders (PDF, HTML)
  • Game character creation
  • API request builders

Prototype Pattern

The Prototype pattern creates objects by cloning existing instances rather than creating new ones from scratch.

import copy
import requests

class Prototype:
    def clone(self):
        return copy.deepcopy(self)

class APIRequest(Prototype):
    def __init__(self, method="GET", url="", headers=None, params=None, json=None):
        self.method = method
        self.url = url
        self.headers = headers or {}
        self.params = params or {}
        self.json = json

    def send(self):
        print(f"Sending {self.method} request to {self.url}")
        response = requests.request(
            method=self.method,
            url=self.url,
            headers=self.headers,
            params=self.params,
            json=self.json
        )
        return response

    def __str__(self):
        return (
            f"{self.method} {self.url}\n"
            f"Headers: {self.headers}\n"
            f"Params: {self.params}\n"
            f"Body: {self.json}"
        )

if __name__ == "__main__":
    # Step 1: Create a prototype for a POST request
    post_template = APIRequest(
        method="POST",
        url="https://jsonplaceholder.typicode.com/posts",
        headers={"Content-Type": "application/json"},
        json={"userId": 1}
    )

    # Step 2: Clone the template and customize each
    post1 = post_template.clone()
    post1.json["title"] = "Post 1"
    post1.json["body"] = "Body for post 1"

    post2 = post_template.clone()
    post2.json["title"] = "Post 2"
    post2.json["body"] = "Body for post 2"

    # Step 3: Send requests
    for post in [post1, post2]:
        print("=" * 40)
        print("Request:")
        print(post)
        print("-" * 40)
        response = post.send()
        print("Status:", response.status_code)
        print("Response:", response.json())

Enter fullscreen mode Exit fullscreen mode

Use Cases

  • Document templates
  • Game object spawning
  • Configuration templates
  • UI component templates
  • Test data creation

When to Use Each Pattern

  • Builder: When constructing complex objects with many optional parameters, or when the construction process must allow different representations.
  • Prototype: When object creation is expensive and you have similar objects, or when you need to create objects based on templates.

Feel free to share your opinion and criticize this article.

Top comments (0)