Parameterized Factories - InjectQ Documentation
Author: InjectQ Contributors
Description: Modern Python dependency injection library

Skip to content

# Parameterized Factories

InjectQ supports two types of factory functions:

1. Dependency Injection (DI) Factories - Factories whose parameters are automatically resolved from the container
2. Parameterized Factories - Factories that accept custom arguments passed by the caller

This flexibility allows you to choose the right pattern for your use case.

## 📋 Table of Contents

- DI Factories (Auto-Resolution)
- Parameterized Factories (Manual Arguments)
- API Methods
- Use Cases
- Best Practices

## DI Factories (Auto-Resolution)

These are the traditional factory functions where InjectQ automatically resolves all parameters from the container.

### Basic Example

    from injectq import InjectQ
    from datetime import datetime

    container = InjectQ()

    # Factory with no parameters
    container.bind_factory("timestamp", lambda: datetime.now().isoformat())

    # Get the value - factory is automatically invoked
    timestamp = container.get("timestamp")
    print(timestamp)  # "2024-01-01T12:00:00.000000"

### With Dependencies

    class Database:
        def query(self, sql: str) -> list:
            return [{"id": 1, "name": "Alice"}]

    class Cache:
        def get(self, key: str) -> Any:
            return None

    # Factory that depends on Database and Cache
    def create_user_service(db: Database, cache: Cache) -> UserService:
        """Factory with DI - parameters resolved automatically."""
        return UserService(db, cache)

    container.bind(Database, Database).singleton()
    container.bind(Cache, Cache).singleton()
    container.bind_factory(UserService, create_user_service)

    # Dependencies are automatically injected
    service = container.get(UserService)

## Parameterized Factories (Manual Arguments)

When you need to pass custom arguments to a factory function at call time, use parameterized factories.

### Basic Example

    from injectq import InjectQ

    container = InjectQ()

    # Data store
    data = {
        "user:1": {"name": "Alice", "age": 30},
        "user:2": {"name": "Bob", "age": 25},
        "user:3": {"name": "Charlie", "age": 35},
    }

    # Bind a parameterized factory
    container.bind_factory("data_store", lambda key: data.get(key))

    # Method 1: Get factory then call
    factory = container.get_factory("data_store")
    user1 = factory("user:1")
    print(user1)  # {"name": "Alice", "age": 30}

    # Method 2: Use call_factory shorthand
    user2 = container.call_factory("data_store", "user:2")
    print(user2)  # {"name": "Bob", "age": 25}

    # Method 3: Chain the calls
    user3 = container.get_factory("data_store")("user:3")
    print(user3)  # {"name": "Charlie", "age": 35}

### Multiple Parameters

    def calculator(operation: str, a: float, b: float) -> float:
        """Factory that performs calculations."""
        operations = {
            "add": a + b,
            "subtract": a - b,
            "multiply": a * b,
            "divide": a / b if b != 0 else 0,
        }
        return operations.get(operation, 0)

    container.bind_factory("calculator", calculator)

    # Use with different operations
    result_add = container.call_factory("calculator", "add", 10, 5)
    result_multiply = container.call_factory("calculator", "multiply", 10, 5)

    print(f"10 + 5 = {result_add}")      # 15
    print(f"10 * 5 = {result_multiply}") # 50

### Keyword Arguments

    def create_config(env: str = "dev", debug: bool = False, port: int = 8000) -> dict:
        """Factory that creates configuration objects."""
        return {
            "environment": env,
            "debug": debug,
            "port": port,
            "url": f"http://localhost:{port}",
        }

    container.bind_factory("config", create_config)

    # Call with different configurations
    dev_config = container.call_factory("config", env="dev", debug=True)
    prod_config = container.call_factory("config", env="prod", debug=False, port=80)

    print(dev_config)   # {"environment": "dev", "debug": True, ...}
    print(prod_config)  # {"environment": "prod", "debug": False, ...}

## API Methods

### `get_factory(service_type)`

Returns the raw factory function without invoking it.

Signature:

    def get_factory(self, service_type: ServiceKey) -> ServiceFactory:
        """Get the raw factory function without invoking it."""

Parameters: - `service_type`: The service type or key for the factory

Returns: - The factory function

Raises: - `DependencyNotFoundError`: If no factory is registered

Example:

    container.bind_factory("data_store", lambda key: data[key])

    # Get the factory function
    factory = container.get_factory("data_store")

    # Call it with your own arguments
    result = factory("key1")

### `call_factory(service_type, *args, **kwargs)`

Convenience method that gets and calls a factory in one step.

Signature:

    def call_factory(self, service_type: ServiceKey, *args: Any, **kwargs: Any) -> Any:
        """Get and call a factory function with custom arguments."""

Parameters: - `service_type`: The service type or key for the factory - `*args`: Positional arguments to pass to the factory - `**kwargs`: Keyword arguments to pass to the factory

Returns: - The result of calling the factory function

Raises: - `DependencyNotFoundError`: If no factory is registered

Example:

    container.bind_factory("calculator", lambda op, a, b: operations[op](a, b))

    # Call with arguments directly
    result = container.call_factory("calculator", "add", 10, 5)

## Use Cases

### 1. Data Access Patterns

    # Repository pattern with dynamic queries
    class UserRepository:
        def __init__(self, db: Database):
            self.db = db

        def find_by_id(self, user_id: int):
            return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")

    container.bind(Database, Database).singleton()

    # Bind as parameterized factory
    container.bind_factory(
        "user_repo_query",
        lambda user_id: container.get(UserRepository).find_by_id(user_id)
    )

    # Use it
    user = container.call_factory("user_repo_query", 123)

### 2. Configuration Management

    # Environment-specific configurations
    def create_db_config(env: str, pool_size: int = 10):
        configs = {
            "dev": {"host": "localhost", "port": 5432, "db": "dev_db"},
            "prod": {"host": "prod-db.example.com", "port": 5432, "db": "prod_db"},
        }
        config = configs.get(env, configs["dev"])
        config["pool_size"] = pool_size
        return config

    container.bind_factory("db_config", create_db_config)

    # Get different configs
    dev_config = container.call_factory("db_config", "dev")
    prod_config = container.call_factory("db_config", "prod", pool_size=50)

### 3. Connection Pooling

    class ConnectionPool:
        def __init__(self, db_name: str, max_connections: int = 10):
            self.db_name = db_name
            self.max_connections = max_connections

    container.bind_factory(
        "db_pool",
        lambda db_name, max_conn=10: ConnectionPool(db_name, max_conn)
    )

    # Create different pools for different databases
    users_pool = container.call_factory("db_pool", "users_db", max_conn=20)
    orders_pool = container.call_factory("db_pool", "orders_db", max_conn=15)

### 4. Dynamic Service Creation

    # Service factory that creates services based on type
    def create_notification_service(channel: str):
        services = {
            "email": EmailNotificationService(),
            "sms": SMSNotificationService(),
            "push": PushNotificationService(),
        }
        return services.get(channel)

    container.bind_factory("notification", create_notification_service)

    # Get different notification channels
    email_service = container.call_factory("notification", "email")
    sms_service = container.call_factory("notification", "sms")

## Best Practices

### ✅ Do's

1. Use DI factories for services with dependencies

       # Good - dependencies auto-injected
       container.bind_factory(UserService, lambda db, cache: UserService(db, cache))
       service = container.get(UserService)

2. Use parameterized factories for dynamic data access

       # Good - allows custom parameters
       container.bind_factory("get_user", lambda user_id: fetch_user(user_id))
       user = container.call_factory("get_user", 123)

3. Combine both patterns when needed

       # DI factory
       container.bind_factory("logger", lambda: create_logger())
       logger = container.get("logger")

       # Parameterized factory
       container.bind_factory("data", lambda key: data[key])
       value = container.call_factory("data", "key1")

4. Use descriptive factory names

       # Good
       container.bind_factory("get_user_by_id", lambda id: ...)
       container.bind_factory("create_db_connection", lambda db_name: ...)

       # Bad
       container.bind_factory("factory1", lambda x: ...)
       container.bind_factory("f", lambda a, b: ...)

### ❌ Don'ts

1. Don't use `get()` for parameterized factories

       # Bad - will fail with DI resolution
       container.bind_factory("data", lambda key: data[key])
       value = container.get("data")  # ERROR: 'key' not found

       # Good
       value = container.call_factory("data", "key1")

2. Don't mix DI and manual parameters in the same factory

       # Confusing - some params from DI, others from caller
       def mixed_factory(db: Database, user_id: int):
           return db.get_user(user_id)

       # Better - separate concerns
       def di_factory(db: Database) -> UserRepository:
           return UserRepository(db)

       container.bind_factory("user_repo", di_factory)
       repo = container.get("user_repo")
       user = repo.get(123)  # Separate the parameter passing

3. Don't overuse parameterized factories

       # Bad - too many parameters
       container.bind_factory("create_everything",
           lambda a, b, c, d, e, f, g: ...)

       # Good - use a class or split into smaller factories
       class ConfigBuilder:
           def build(self, a, b, c, d, e, f, g):
               ...

       container.bind(ConfigBuilder, ConfigBuilder).singleton()

## Comparison Table

Feature | DI Factories | Parameterized Factories
--- | --- | ---
Binding | `bind_factory("service", factory)` | `bind_factory("service", factory)`
Resolution | `get("service")` | `call_factory("service", args)`
Parameters | Auto-resolved from container | Passed by caller
Use Case | Services with dependencies | Dynamic data access
Caching | Respects scope (singleton, transient) | Always transient
Example | User service with DB/cache deps | Get user by ID

## Migration Guide

If you have existing code that tries to use parameterized factories with `get()`:

Before:

    # This doesn't work
    container.bind_factory("data", lambda key: data[key])
    value = container.get("data")  # ERROR

After:

    # Use get_factory or call_factory
    container.bind_factory("data", lambda key: data[key])

    # Option 1: Get factory then call
    factory = container.get_factory("data")
    value = factory("key1")

    # Option 2: Use shorthand
    value = container.call_factory("data", "key1")

## See Also

- Factory Patterns
- Dict-like Interface
- Scopes