This repository demonstrates a practical implementation of dependency injection and component design in Go. It showcases how these patterns create testable, maintainable, and flexible applications.
Rather than creating dependencies internally, components receive them through constructors:
// OpenWeatherAPI receives its dependencies
func NewOpenWeatherAPI(client doer, apiKey string, logger *log.Logger) *OpenWeatherAPI {
return &OpenWeatherAPI{
client: client,
apiKey: apiKey,
baseURL: "https://api.openweathermap.org/data/2.5",
logger: logger,
}
}
This explicit wiring in main.go
creates a clear dependency graph:
// Wire the application components
weatherAPI := openweather.NewOpenWeatherAPI(httpClient, apiKey, logger)
cachedAPI := openweather.NewCachedWeatherAPI(weatherAPI, cache, logger)
handler := weather.NewWeatherHandler(cachedAPI, logger)
Interfaces define behavior contracts for components:
// Consumer-defined interface
type WeatherAPI interface {
GetCurrentTemperature(ctx context.Context, city string) (float64, error)
}
// Multiple implementations can satisfy this interface
type CachedWeatherAPI struct { ... }
type OpenWeatherAPI struct { ... }
Each interface is defined where it's needed, focusing on required behavior rather than implementation details.
Our components each have focused responsibilities:
OpenWeatherAPI
: External API communicationCachedWeatherAPI
: Transparent caching decoratorInMemoryCache
: Data storage with expirationWeatherHandler
: HTTP request handling
This architecture enables precise, focused tests:
func TestGetCurrentTemperature_CacheHit(t *testing.T) {
// Mock dependencies with testify
mockAPI := new(MockOpenWeatherAPI)
mockCache := new(MockCache)
cachedAPI := &CachedWeatherAPI{
api: mockAPI,
cache: mockCache,
logger: logger,
}
// Test specific behaviors in isolation
mockCache.On("Get", "London").Return(20.5, true)
temp, err := cachedAPI.GetCurrentTemperature(context.Background(), "London")
require.NoError(t, err)
assert.Equal(t, 20.5, temp)
// Verify expected interactions
mockAPI.AssertNotCalled(t, "GetCurrentTemperature")
}
Testing each component in isolation helps identify issues precisely and simplifies test maintenance.
Our implementation demonstrates several key patterns:
- Decorator Pattern: The
CachedWeatherAPI
decorates the standard API with caching - Dependency Inversion: High-level modules depend on abstractions, not details
- Constructor Injection: Dependencies are provided at creation time
- Interface Segregation: Interfaces are small and focused on specific needs
These patterns create a codebase that's easy to understand, test, and extend with new features.