Денис Крешихин

Денис
Крешихин

iOS-разработчик с 15+ летним опытом

Тимлид/сеньор по обстоятельствам

Интересы: swift, uikit, rxswift, oop/ood, devops, agile

2024 © Денис Крешихин

Адаптер интерфейса в 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)

##Заключение

Итак, адаптер интерфейса позволяет динамически сконстрируровать объект удволетворящий требуемому интерфейсу из объекта с произвольным набором методов за счет подмена методов на функции хранящиеся как поля структуры.

Такие адаптеры полезно создавать в следующих случаях:

  • При проектировании функций реализующих обобщенную работу с данными, т.к. адаптер интерфейса позволяет пользователям не плодить новые типы и методы при обращение к таким функциям.

  • При создании однотипных структур, которые будут обращаться к функциям со специфичным интерфейсом. Что бы не создавать новые типы и методы для каждой структуры.

  • Для того что бы производить "настройку" объекта для передачи в функцию без создания нового типа, который бы менял поведение методов предоставляющих доступе к его данным.