| status | draft |
|---|
This document analyzes how the Keybinding library implements SOLID and DRY principles and supports both standalone and dependency injection usage patterns.
Each class and interface has a single, well-defined responsibility:
ICommandRegistry/CommandRegistry: Manages command registration and retrieval onlyIProfileManager/ProfileManager: Handles profile lifecycle management onlyIKeybindingService/KeybindingService: Coordinates keybinding operations onlyIKeybindingRepository/JsonKeybindingRepository: Handles data persistence onlyKeybindingManager: Acts as a facade coordinating all services
The library is open for extension but closed for modification:
- Repository Pattern: New storage mechanisms can be implemented via
IKeybindingRepositorywithout modifying existing code - Service Interfaces: All core services are interface-based, allowing custom implementations
- Factory Pattern:
IKeybindingManagerFactoryallows different creation strategies - Configuration Abstraction:
IKeybindingConfigurationenables different configuration sources
Example Extension:
// Extend with custom repository without modifying existing code
public class DatabaseKeybindingRepository : IKeybindingRepository
{
// Custom implementation for database storage
}
// Register with DI
services.AddKeybinding<DatabaseKeybindingRepository>();All implementations can be substituted for their interfaces without breaking functionality:
- Any
IKeybindingRepositoryimplementation works withKeybindingManager - Any
ICommandRegistryimplementation maintains expected behavior - Mock implementations work seamlessly in unit tests
Interfaces are focused and clients depend only on what they need:
ICommandRegistry: Only command-related operationsIProfileManager: Only profile-related operationsIKeybindingService: Only keybinding coordinationIKeybindingRepository: Only persistence operationsIKeybindingConfiguration: Only configuration propertiesIKeybindingManagerFactory: Only factory methods
No interface forces clients to depend on methods they don't use.
High-level modules depend on abstractions, not concretions:
KeybindingManagerdepends on interfaces (ICommandRegistry,IProfileManager,IKeybindingRepository)KeybindingServicedepends on abstractions (ICommandRegistry,IProfileManager)- Dependency Injection: All dependencies are injected via constructor
- Factory Pattern: Creates objects through abstractions
The ServiceCollectionExtensions eliminates repetitive DI registration:
// DRY: Single method registers all services
services.AddKeybinding("./data");
// Instead of repeating registration for each service
services.AddSingleton<ICommandRegistry, CommandRegistry>();
services.AddSingleton<IProfileManager, ProfileManager>();
// ... etcThe IKeybindingConfiguration abstraction eliminates repeated configuration handling:
// DRY: Configuration is centralized and reusable
var config = new KeybindingConfiguration(dataDirectory, profileId, profileName);The IKeybindingManagerFactory eliminates duplicate creation logic:
// DRY: Factory handles creation complexity
var manager = factory.CreateManager();
// Instead of repeating constructor calls with dependency resolutionService registration extensions eliminate repetitive DI setup code:
// DRY: Multiple registration patterns available
services.AddKeybinding(); // Default singleton
services.AddKeybindingScoped(); // Scoped services
services.AddKeybinding<CustomRepository>(); // Custom repositoryThe library works perfectly without any DI container:
// Simple constructor-based instantiation
var manager = new KeybindingManager("./data");
// Or with custom services
var manager = new KeybindingManager(
new CommandRegistry(),
new ProfileManager(),
new JsonKeybindingRepository("./data"));Benefits:
- Zero dependencies on DI frameworks
- Simple instantiation
- Full control over object lifetime
- Suitable for console apps, simple applications
Full DI support with multiple registration patterns:
// Simple registration
services.AddKeybinding("./data");
// Custom repository
services.AddKeybinding<DatabaseRepository>();
// Scoped services (for multi-tenant scenarios)
services.AddKeybindingScoped();Benefits:
- Automatic dependency resolution
- Configurable lifetimes (singleton, scoped, transient)
- Integration with ASP.NET Core, Generic Host
- Easy mocking for unit tests
- Configuration binding support
Direct Instantiation:
var manager = new KeybindingManager("./data");Service Locator:
var manager = serviceProvider.GetRequiredService<KeybindingManager>();Factory Pattern:
var manager = factory.CreateManager("./tenant-data");Default Dependencies:
services.AddKeybinding(); // Uses JsonKeybindingRepositoryCustom Repository:
services.AddKeybinding<DatabaseKeybindingRepository>();Custom Factory:
services.AddKeybinding<CustomRepository>(provider =>
new CustomRepository(provider.GetService<IOptions<DbConfig>>()));Singleton (Default):
services.AddKeybinding(); // All services as singletonsScoped:
services.AddKeybindingScoped(); // All services as scopedMixed Lifetimes:
services.AddSingleton<ICommandRegistry, CommandRegistry>();
services.AddScoped<KeybindingManager>();The architecture enables comprehensive testing strategies:
var mockRepository = new Mock<IKeybindingRepository>();
var mockCommands = new Mock<ICommandRegistry>();
var manager = new KeybindingManager(mockCommands.Object, mockProfiles.Object, mockRepository.Object);var services = new ServiceCollection();
services.AddKeybinding();
var provider = services.BuildServiceProvider();
var manager = provider.GetRequiredService<KeybindingManager>();public class InMemoryKeybindingRepository : IKeybindingRepository
{
// In-memory implementation for testing
}
services.AddKeybinding<InMemoryKeybindingRepository>();✅ Single Responsibility: Each class has one reason to change
✅ Open/Closed: Extensible without modification
✅ Liskov Substitution: Implementations are truly substitutable
✅ Interface Segregation: Focused, cohesive interfaces
✅ Dependency Inversion: Depends on abstractions, not concretions
✅ No Repeated Logic: Common patterns are abstracted into helper classes
✅ Reusable Components: Services can be composed differently
✅ Configuration Centralization: Settings managed in one place
✅ Extension Methods: Eliminate repetitive setup code
✅ Disposal Patterns: Centralized disposal checking via DisposalHelper
✅ Async Batch Operations: Reusable async iteration patterns via AsyncBatchHelper
✅ Operation Counting: Standardized error-handling loops via OperationHelper
✅ Validation Patterns: Common validation logic via ValidationHelper
✅ Standalone Usage: Works without DI containers
✅ DI Integration: Full support for modern DI patterns
✅ Multiple Lifetimes: Singleton, scoped, transient support
✅ Custom Implementations: Easy to extend and customize
✅ Testing Friendly: Mockable interfaces and test implementations
The Keybinding library successfully implements SOLID and DRY principles while providing maximum flexibility for both standalone and dependency injection usage patterns.