5.3 KiB
ADR-010: AppFactory Pattern for Feature Integration
Status: Accepted
Date: 2025-01-11
Context:
With a modular architecture where each feature is a separate package, I needed a consistent way to integrate features into the main application. The key challenges were:
- Encapsulation: Feature internals (view models, repositories, services) should remain private
- Dependency injection: Features need various dependencies from the main app
- Multiple entry points: Features may provide main views, scenes, and settings views
- SwiftUI integration: Need to provide both views and scenes (WindowGroup, etc.)
- Testability: Integration points should be mockable
Decision:
I adopted the AppFactory pattern where each feature package exposes a single public factory class with three standardized methods:
makeAppView()- Creates the main feature view for display in the sidebar or main content areamakeScene()- Returns window scenes (WindowGroup) private to the modulemakeSettingsView()- Creates settings views when applicable (optional)
Factories receive dependencies via their initializer and internally manage a feature-specific Environment that handles further dependency resolution.
Implementation Pattern:
public final class StatusAppFactory {
private let environment: StatusEnvironment
public init(
sessionService: any SessionServiceProtocol,
authSessionService: any AuthSessionServiceProtocol,
networkClient: NetworkClientProtocol
) {
environment = .init(
sessionService: sessionService,
authSessionService: authSessionService,
networkClient: networkClient
)
}
@MainActor
@ViewBuilder
public func makeAppView() -> some View {
let viewModel = environment.viewModelFactory
.makeStatusAppViewModel()
StatusApp(viewModel: viewModel)
.environment(\.viewModelFactory, environment.viewModelFactory)
.modelContainer(environment.modelContainer)
}
@MainActor
@ViewBuilder
public func makeSettingsView() -> some View {
let viewModel = environment.viewModelFactory
.makeStatusSettingsViewModel()
StatusSettingsView(viewModel: viewModel)
.modelContainer(environment.modelContainer)
}
@MainActor
public func makeScene() -> some Scene {
ComposeStatusScene(environment: environment)
}
}
Integration with TritonEnvironment:
struct TritonEnvironment: TritonEnvironmentProtocol {
private let container = DependencyContainer()
var statusAppFactory: StatusAppFactory { container.resolve() }
init() {
container.register(
type: StatusAppFactory.self,
allocation: .static
) { container in
StatusAppFactory(
sessionService: container.resolve(),
authSessionService: container.resolve(),
networkClient: container.resolve()
)
}
}
}
Why Factories Over Direct Instantiation:
- Encapsulation: Keeps view models, repositories, and internal dependencies private
- Single public interface: Only the factory is public, everything else is internal
- Dependency coordination: Factory manages complex dependency graphs internally
- Consistent integration: All features integrate the same way
- Testability: Can mock factories for testing main app integration
- Lazy initialization: Features only initialize when actually used
Consequences:
Positive
- Clear boundaries: Features expose minimal public API surface
- Consistent pattern: All features integrate identically
- Dependency management: Complex graphs handled internally by each feature
- Flexibility: Features can evolve internals without affecting main app
- Testability: Integration points are mockable
- SwiftUI native: Returns proper SwiftUI types (some View, some Scene)
Negative
- Boilerplate: Each feature needs a factory class
- Indirect access: Can't directly instantiate feature components
- Factory proliferation: One factory per feature adds code
Neutral
- Learning curve: Team members need to understand factory pattern
- Factory responsibilities: Ongoing decisions about what belongs in factory vs environment
Method Contracts:
makeAppView()- REQUIRED: Must return main feature view, marked @MainActormakeScene()- OPTIONAL: Returns scenes for auxiliary windows, marked @MainActormakeSettingsView()- OPTIONAL: Returns settings view when feature has preferences, marked @MainActor
Related Decisions:
- ADR-003: Feature-Based Package Organization - Factories are part of feature structure
- ADR-008: MicroContainer for Dependency Injection - Factories registered in container
- ADR-002: Layered Architecture and Dependency Direction - Factories are integration layer
Notes:
The AppFactory pattern provides a clean, consistent way to integrate modular features while maintaining strong encapsulation. It's the primary public interface for each feature package.