Адаптер интерфейса в Go
Так получилось, что в Go нет общепринятого подхода для написания обобщенных алгоритмов, типа темплейтов на которых строится STL в C++. Видимо, поэтому алгоритмические возможности стандартной библиотеки Go довольно ограничены. Для того что бы к коллекции применить какой-то алгоритм обычно требуется определять некоторый интерфейс, например, как это происходит при использовании сортировки sort.Sort
из стандартной библиотеки. На примере этого интерфейса sort.Interface
можно создать структуру-адаптер которая позволит применять функцию без явного определения новых методов.
##Стандартный путь
Давайте рассмотрим некоторый тип, User
у которого определены два поля - имя и баланс:
type User struct{
Name string
Balance float32
}
Допустим у нас есть некоторый массив с пользователями, и нам нужно отстортировать его по количеству денег на балансе. Это должно выглядеть примерно так:
func main(){
users := Users{{"Петя", 100}, {"Коля", 50},
{"Катя", 900}, {"Маша", 200}, {"Вася", 400}}
sort.Sort(users)
fmt.Println(users)
}
К сожалению, т.к. в Go отсутсвует параметрический полиморфизм, который позволил бы адаптировать сортировку к нашему типу, то единственный путь писать обобщенные функции без использования рефлексии это потребовать определения некоторого интерфейса, который бы предоставил быстрый доступ к данным. Именно таким путем пошли разработчики библиотеки sort
, т.к. для того что бы наша программа скомпилировалась требуется определить интерфейс sort.Interface
.
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
Т.к. в Go нельзя задать методы для композитного типа, вроде []User, то заодно следует определить и новый тип, коллекцию Users
:
type Users []User
func (users Users) Len() int {
return len(users)
}
func (users Users) Less(i, j int) bool {
return users[i].Balance < users[j].Balance
}
func (users Users) Swap(i, j int) {
users[i], users[j] = users[j], users[i]
}
Итак, описав, три метода Len, Less
и Swap
мы получили корректную сортировку:
$go run main.go
[{Коля 50} {Петя 100} {Маша 200} {Вася 400} {Катя 900}]
(Код примера можно найти здесь https://play.golang.org/p/_7Ip7E9ZJ6)
##Адаптер интерфейса
Теперь допустим что в структуре User
появилось новое поле MonthOutlay
которое указывает на траты пользователя за последний месяц. И мы хотим отобрать пользователей которые тратят больше всего денег.
type User struct{
Name string
Balance float32
MonthOutlay float32
}
Очевидно, что методы Len, Less
и Swap
описанные для сортировки по балансу уже не подходят для сортировки по новому полю. Если следовать стандартному подходу, то нам следует определить новый тип, например, OutlayUsers
с новыми методами.
Т.к. создавать новый тип, каждый раз когда требуется отсортировать коллекцию по некоторому признаку является не очень практичным решением, то следовало бы поискать другой путь.
Одним из решением проблемы является создание структуры-адаптера для интерфейса sort.Interface
, назовем эту структуру SortAdapter
:
type SortAdapter struct {
len func() int
less func(i, j int) bool
swap func(i, j int)
}
func (s SortAdapter) Len() int{
return s.len()
}
func (s SortAdapter) Less(i, j int) bool{
return s.less(i, j)
}
func (s SortAdapter) Swap(i, j int) {
s.swap(i, j)
}
Как видно из определение структуры, она хранит в себе функции которые необходимы для сортировки и выдает их за свои собственные методы.
Теперь для того что бы передать объект в функцию sort.Sort
вовсе не обязательно создавать новый тип и описывать методы, достаточно инстанцировать структуру с соответсвующими прямо в момент вызова:
sort.Sort(SortAdapter{func() int {
return len(users)
}, func(i, j int) bool {
return users[i].Balance < users[j].Balance
}, func(i, j int) {
users[i], users[j] = users[j], users[i]
}})
fmt.Println("Sorted by balance: \n", users)
sort.Sort(SortAdapter{func() int {
return len(users)
}, func(i, j int) bool {
return users[i].MonthOutlay < users[j].MonthOutlay
}, func(i, j int) {
users[i], users[j] = users[j], users[i]
}})
fmt.Println("Sorted by month outlay: \n", users)
Результат:
Sorted by balance:
[{Коля 50 2000} {Петя 100 1000} {Маша 200 0} {Вася 400 700} {Катя 900 500}]
Sorted by month outlay:
[{Маша 200 0} {Катя 900 500} {Вася 400 700} {Петя 100 1000} {Коля 50 2000}]
(Исходный код примера лежит здесь https://play.golang.org/p/EJ1s-tBweX)
Итак, адаптер интерфейса позволяет обращаться к функциям с произвольным интерфейсом без создания новых структур удовлетворящих этому интерфейсу. Другими словами, SortAdapter можно исопльзовать с любой коллекцией, если существует возможность определить функции len, less, swap
.
##Смешанный подход
Итак, имея адаптер интерфейса SortAdapter
мы можем подготовить любую коллекцию для передачи в функцию sort.Sort
. С другой стороны, если мы работает только с одной коллекцией, но разными признаками сортировки, то мы можем определить новую структуру-адаптер, которая уже не требует указания функций len
и swap
т.к. для коллекций одного типа эти функции одинаковы:
type UserSortAdapter struct {
Users []User
less func(i, j int) bool
}
func (s UserSortAdapter) Len() int{
return len(s.Users)
}
func (s UserSortAdapter) Less(i, j int) bool{
return s.less(i, j)
}
func (s UserSortAdapter) Swap(i, j int) {
s.Users[i], s.Users[j] = s.Users[j], s.Users[i]
}
Как видно из кода, теперь только метод Less
определяется динамически, методы Len
и Swap
работаю с коллекцией напрямую, это позволяет упростить вызов метода sort.Sort
:
func main() {
users := []User{{"Петя", 100, 1000}, {"Коля", 50, 2000},
{"Катя", 900, 500}, {"Маша", 200, 0}, {"Вася", 400, 700}}
sort.Sort(UserSortAdapter{users, func(i, j int) bool {
return users[i].Balance < users[j].Balance
}})
fmt.Println("Sorted by balance:\n", users)
sort.Sort(UserSortAdapter{users, func(i, j int) bool {
return users[i].MonthOutlay < users[j].MonthOutlay
}})
fmt.Println("Sorted by month outlay:\n", users)
}
Результат ничем не отличается от "стандартного подхода":
Sorted by balance:
[{Коля 50 2000} {Петя 100 1000} {Маша 200 0} {Вася 400 700} {Катя 900 500}]
Sorted by month outlay:
[{Маша 200 0} {Катя 900 500} {Вася 400 700} {Петя 100 1000} {Коля 50 2000}]
(Исходный код примера лежит здесь https://play.golang.org/p/gMywPseLKA)
##Заключение
Итак, адаптер интерфейса позволяет динамически сконстрируровать объект удволетворящий требуемому интерфейсу из объекта с произвольным набором методов за счет подмена методов на функции хранящиеся как поля структуры.
Такие адаптеры полезно создавать в следующих случаях:
При проектировании функций реализующих обобщенную работу с данными, т.к. адаптер интерфейса позволяет пользователям не плодить новые типы и методы при обращение к таким функциям.
При создании однотипных структур, которые будут обращаться к функциям со специфичным интерфейсом. Что бы не создавать новые типы и методы для каждой структуры.
Для того что бы производить "настройку" объекта для передачи в функцию без создания нового типа, который бы менял поведение методов предоставляющих доступе к его данным.