Factory Method (pick which object to create in a subclass)
When to use
- You have a base workflow, but creation of one part varies (e.g., Snowflake auth: key pair vs OAuth).
- You want to add new creation modes without touching the base workflow.
- You need testable swaps for the created thing.
Avoid when a plain function like make_connection(config) is enough.
Diagram (text)
SnowflakeService (base)
├─ execute(sql) uses → create_conn() ← Factory Method (overridden)
├─ (abstract) create_conn(): SnowflakeConn
└─▲
├── KeyPairService → returns KeyPairConn
└── OAuthService → returns OAuthConn
Step-by-step idea
- Put the shared workflow in the base class (
execute). - Make an abstract factory method in the base (
create_conn). - Each subclass overrides that method to build the right object.
- The base code calls
create_conn()and stays agnostic.
Python example (≤40 lines, type-hinted)
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Protocol
class SnowflakeConn(Protocol):
def run(self, sql: str) -> str: ...
@dataclass
class KeyPairConn:
user: str; key_path: str
def run(self, sql: str) -> str: return f"keypair:{self.user}:{sql}"
@dataclass
class OAuthConn:
token: str
def run(self, sql: str) -> str: return f"oauth:{len(self.token)}:{sql}"
class SnowflakeService(ABC):
def execute(self, sql: str) -> str:
conn = self.create_conn()
return conn.run(sql)
@abstractmethod
def create_conn(self) -> SnowflakeConn: ...
@dataclass
class KeyPairService(SnowflakeService):
user: str; key_path: str
def create_conn(self) -> SnowflakeConn: return KeyPairConn(self.user, self.key_path)
@dataclass
class OAuthService(SnowflakeService):
token: str
def create_conn(self) -> SnowflakeConn: return OAuthConn(self.token)
Tiny pytest (cements it)
def test_factory_method_switches_impl():
svc1 = KeyPairService(user="svc", key_path="/keys/id_rsa")
svc2 = OAuthService(token="abc123")
assert "keypair:svc" in svc1.execute("SELECT 1")
assert "oauth:" in svc2.execute("SELECT 1")
Trade-offs & pitfalls
- Pros: Shared workflow in one place; easy to add new connection types; good test seams.
- Cons: Extra classes; indirection may feel heavy for simple cases.
- Pitfalls:
- Putting business logic inside
create_conn—keep it just for creation. - Returning objects with incompatible interfaces—stick to a
Protocol(hereSnowflakeConn). - Overusing inheritance—if you have many tiny subclasses, consider a map of callables.
- Putting business logic inside
Pythonic alternatives
- Simple factory function:
def make_conn(cfg) -> SnowflakeConn:(often enough). - Constructor injection: Pass a callable
conn_factory: Callable[[], SnowflakeConn]into one service class. - Dataclass + registry:
REGISTRY = {"keypair": lambda cfg: KeyPairConn(...), "oauth": ...}. - Pydantic/attrs for config validation before calling the factory.
Mini exercise
Add PasswordService(user, password) that returns a PasswordConn. Confirm your existing execute() needs no changes and the new service passes a small test.
Checks (quick checklist)
- Base class holds the workflow; subclasses only decide what to create.
- Factory method returns a stable interface/Protocol.
- Adding a new product doesn’t touch base code.
- Tests verify different subclasses produce different behaviors with the same
execute. - Creation logic is thin; business logic stays in the workflow.




