mirror of
https://github.com/otaviocc/Triton.git
synced 2026-01-29 19:54:27 +00:00
141 lines
5.3 KiB
Markdown
141 lines
5.3 KiB
Markdown
# 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:
|
|
|
|
1. **Encapsulation:** Feature internals (view models, repositories, services) should remain private
|
|
2. **Dependency injection:** Features need various dependencies from the main app
|
|
3. **Multiple entry points:** Features may provide main views, scenes, and settings views
|
|
4. **SwiftUI integration:** Need to provide both views and scenes (WindowGroup, etc.)
|
|
5. **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:
|
|
|
|
1. **`makeAppView()`** - Creates the main feature view for display in the sidebar or main content area
|
|
2. **`makeScene()`** - Returns window scenes (WindowGroup) private to the module
|
|
3. **`makeSettingsView()`** - 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:**
|
|
|
|
```swift
|
|
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:**
|
|
|
|
```swift
|
|
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:**
|
|
|
|
1. **Encapsulation:** Keeps view models, repositories, and internal dependencies private
|
|
2. **Single public interface:** Only the factory is public, everything else is internal
|
|
3. **Dependency coordination:** Factory manages complex dependency graphs internally
|
|
4. **Consistent integration:** All features integrate the same way
|
|
5. **Testability:** Can mock factories for testing main app integration
|
|
6. **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 @MainActor
|
|
- **`makeScene()`** - OPTIONAL: Returns scenes for auxiliary windows, marked @MainActor
|
|
- **`makeSettingsView()`** - OPTIONAL: Returns settings view when feature has preferences, marked @MainActor
|
|
|
|
**Related Decisions:**
|
|
|
|
- [ADR-003: Feature-Based Package Organization](ADR-003-feature-based-package-organization.md) - Factories are part of feature structure
|
|
- [ADR-008: MicroContainer for Dependency Injection](ADR-008-microcontainer-for-dependency-injection.md) - Factories registered in container
|
|
- [ADR-002: Layered Architecture and Dependency Direction](ADR-002-layered-architecture-and-dependency-direction.md) - 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.
|