В догонку к предыдущему посту про гибкость vs простота. В языке Go очень интересно сделаны интерфейсы.
Например, есть какой-то класс (точнее структура с методами) с методом
getWeather(city)
, который является обёрткой над внешним сервисом погоды.
type WeatherService struct {
apiKey string
}
func (w *WeatherService) GetWeather(city string) (string, error) {
// http вызов внешнего API
return "Sunny", nil
}
Мы можем такой класс использовать в каком-то коде, где на основании погоды происходят различные вычисления.
type WeatherAnalyzer struct {
weatherService *WeatherService // конкретный класс
}
func (wa *WeatherAnalyzer) AnalyzeWeather(city string) string {
weather, err := wa.weatherService.GetWeather(city)
if err != nil {
return "Unable to analyze weather"
}
if weather == "Sunny" {
return "Great day for a picnic!"
}
return "Maybe stay indoors"
}
// Использование
func main() {
ws := &WeatherService{apiKey: "some-api-key"}
wa := &WeatherAnalyzer{weatherService: ws}
result := wa.AnalyzeWeather("New York")
fmt.Println(result)
}
И тут мы ВНЕЗАПНО поняли, что покрывать тестами это неудобно (ибо внешние вызовы), и лучше бы написать интерфейс, чтобы потом его замокать. Или у нас вдруг появился еще один сервис погоды, и нужен интерфейс. Так вот, в Go достаточно просто ПО МЕСТУ добавить интерфейс с описанием нужных в этом месте методов, а сам класс погоды останется неизменным.
Вот как это будет выглядеть:
// WeatherService остается без изменений
type WeatherProvider interface {
GetWeather(city string) (string, error)
}
type WeatherAnalyzer struct {
weatherProvider WeatherProvider
}
func (wa *WeatherAnalyzer) AnalyzeWeather(city string) string {
weather, err := wa.weatherProvider.GetWeather(city)
if err != nil {
return "Unable to analyze weather"
}
if weather == "Sunny" {
return "Great day for a picnic!"
}
return "Maybe stay indoors"
}
Потому что в отличие от Java/PHP/etc не нужно писать что класс имплементирует что-то там. Если у класса есть методы такие же как в интерфейсе, то он подходит под сигнатуру
Поэтому ничего заранее не надо придумывать, никакую гибкость. Не надо продумывать, а где нам может понадобиться интерфейс, а какие методы в нём нужны. Ты просто создаешь интерфейс прям там, где тебе надо отвязаться от конкретики ровно с теми методами, которые нужно отвязать.
Теперь мы можем легко создать мок для тестирования (упрощенный пример)
type MockWeatherProvider struct {
MockWeather string
}
func (m *MockWeatherProvider) GetWeather(city string) (string, error) {
return m.MockWeather, nil
}
func TestWeatherAnalyzer(t *testing.T) {
mockProvider := &MockWeatherProvider{MockWeather: "Sunny"}
analyzer := &WeatherAnalyzer{weatherProvider: mockProvider}
result := analyzer.AnalyzeWeather("Test City")
expected := "Great day for a picnic!"
if result != expected {
t.Errorf("Expected %s, but got %s", expected, result)
}
}
У этого конечно есть и недостатки.