Реализация интерфейсов в Golang

Интерфейсы — это инструменты для определения наборов действий и поведения. Они позволяют объектам опираться на абстракции, а не фактические реализации других объектов. При этом для компоновки различных поведений можно группировать несколько интерфейсов.

Что такое интерфейс?

Интерфейс — это набор методов, представляющих стандартное поведение для различных типов данных.

С помощью интерфейсов можно организовывать разные группы методов, применяемых к разным объектам. Таким образом, программа вместо фактических реализаций сможет опираться на более высокие абстракции (интерфейсы), позволяя методам работать с различными объектами, реализующими один и тот же интерфейс. В мире ООП этот принцип называется инверсией зависимостей.

В Go наилучшей практикой считается создание именно небольших интерфейсов с последующим их совмещением для расширения функциональности объектов. Это позволяет поддерживать код в чистоте и повышает его переиспользуемость.

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

В Go можно автоматически сделать вывод, что структура (объект) реализует интерфейс, когда она реализуется все его методы.

Определение простого интерфейса

Начнем с создания интерфейса, после чего изучим принцип его работы.

type Printer interface {
Print()
}

Это очень простой интерфейс, который определяет метод Print(). Данный метод представляет действие или поведение, которые могут реализовывать другие объекты.

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

Далее создадим два объекта, реализующих интерфейс Printer:

type User struct {
name string
age int
lastName string
}
type Document struct {
name string
documentType string
date time.Time
}

// функция Print для структуры Document
func (d Document) Print() {
fmt.Printf("Document name: %s, type: %s, date: %s \n", d.name, d.documentType, d.date)
}

// функция Print для структуры User
func (u User) Print() {
fmt.Printf("Hi I am %s %s and I am %d years old \n", u.name, u.lastName, u.age)
}

В примере выше мы определили два типа структуры — User и Document.

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

Теперь обе структуры реализуют интерфейс Printer.

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

func Process(obj Printer) {
obj.Print()
}

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

В функции main мы напишем следующие инструкции для вывода подробностей о каждом объекте:

func main() {
u := User{name: "John", age: 24, lastName: "Smith"}
doc := Document{name: "doc.csv", documentType: "csv", date: time.Now()}
Process(u)
Process(doc)
}

Вывод получится такой:

Hi I am John Smith and I am 24years old 
Document name: doc.csv, type: csv, date: 2009-11-10 23:00:00 +0000 UTC m=+0.000000001

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

Описание проекта

В этом проекте мы займемся обработкой заказов клиентов. Программа будет поддерживать заказы National и International. Поведение обоих этих интерфейсов будет опираться на абстракцию интерфейса.

Для проекта мы используем модули Go, и я предполагаю, что вы обладаете базовыми знаниями их работы. Если же нет, то это не критично.

Начнем с создания каталога Interfaces.

Далее создадим в нем модуль:

go mod init interfaces

Эта команда сгенерирует файл go.mod, включающий имя модуля и версию Go, которой в моем случае будет go 1.15.

Далее создайте каталог order, а внутри него следующие файлы:

  • intenationalOrder.go
  • nationalOrder.go
  • order.go
  • helpers.go

В корневой директории создайте файл main.go. Общая структура каталогов получится следующей:

Далее рассмотрим реализацию файла main.go. Определение функции main будет простым, так как мы импортируем пакет Order и просто вызовем из него функцию New. Этот пакет, в свою очередь, будет содержать логику примера:

package main

import (
"interfaces/order"
)

func main() {
order.New()
}

Здесь очевидно, что файл достаточно прост, так как мы просто импортируем пакет и вызываем в нем функцию New. Этот пакет еще не существует, но сейчас мы это исправим.

Для него нужно будет создать разные типы структур и интерфейсы. Вот как он будет выглядеть:

package order

import (
"fmt"
)

// New ProcessOrder
func New() {
fmt.Println("Order package!!")
natOrd := NewNationalOrder()
intOrd := NewInternationalOrder()
ords := []Operations{natOrd, intOrd}
ProcessOrder(ords)
}

// Product struct
type Product struct {
name string
price int
}

// ProductDetail struct
type ProductDetail struct {
Product
amount int
total float32
}

// Summary struct
type Summary struct {
total float32
subtotal float32
totalBeforeTax float32
}

// ShippingAddress struct
type ShippingAddress struct {
street string
city string
country string
cp string
}

// Client struct
type Client struct {
name string
lastName string
email string
phone string
}

// Order struct
type Order struct {
products []*ProductDetail
Summary
ShippingAddress
Client
}

// Processer interface
type Processer interface {
FillOrderSummary()
}

// Printer interface
type Printer interface {
PrintOrderDetails()
}

// Notifier interface
type Notifier interface {
Notify()
}

// Operations interface
type Operations interface {
Processer
Printer
Notifier
}

// ProcessOrder function
func ProcessOrder(orders []Operations) {
for _, order := range orders {
order.FillOrderSummary()
order.Notify()
order.PrintOrderDetails()
}
}

Пройдемся по этому файлу и рассмотрим все его функции, интерфейсы и объекты структур.

Первой идет функция New. Как вам известно, мы называем функции с верхнего регистра, так как хотим экспортировать их, сделав доступными для других пакетов. Цель данной функции в создании нового экземпляра внутреннего (national) заказа и международного (international). Далее мы передаем эти два экземпляра в функцию ProcessOrder, находящуюся в срезе типа Operations. Тип Operations мы вскоре тоже рассмотрим.

Следующие типы структур представляют различные объекты, необходимые для создания заказа: Product, ProductDetail, Summary, ShippingAddress, Client и Order.

Тип структуры Order будет содержать свойства Summary, Shipping address, Client. В нем также будет находится массив товаров типа ProductDetail.

Помимо этого, мы создали три небольших интерфейса: Processer, Printer и Notifier. Каждый из них содержит функцию, определяющую, какое поведение должны реализовывать другие объекты для соответствия этим интерфейсам.

У нас также есть интерфейс Operations. Для его создания мы компонуем несколько других интерфейсов, что оказывается очень кстати, поскольку позволяет программе объединять объекты и делает код удобным для повторного использования.

Завершает рассматриваемый файл функция ProcessOrder, которая получает массив заказов. Здесь у нас интересный момент. Вместо того, чтобы получать массив фактических объектов, эта функция получает их абстракцию. Поэтому, пока объекты, передаваемые в этот массив, реализуют интерфейс Operations, функция будет работать корректно. В таких ситуациях интерфейсы поистине проявляют свою пользу, потому что позволяют программе опираться на абстракцию, а не на фактические реализации.

Далее мы реализуем файл InternationalOrder:

package order

import (
"fmt"
)

// структура international
var international = &InternationalOrder{}

// структура InternationalOrder
type InternationalOrder struct {
Order
}

// функция NewInternationalOrder
func NewInternationalOrder() *InternationalOrder {
international.products = append(international.products, GetProductDetail("Lap Top", 450, 1, 450.50))
international.products = append(international.products, GetProductDetail("Video Game", 600, 2, 1200.50))
international.Client = SetClient("Carl", "Smith", "carlsmith@gmail.com", "9658521365")
international.ShippingAddress = SetShippingAddress("Colfax Avenue", "Seattle", "USA", "45712")
return international
}

// функция FillOrderSummary
func (into *InternationalOrder) FillOrderSummary() {
var extraFee float32 = 0.5
var taxes float32 = 0.25
var shippingCost float32 = 35
subtotal = CalculateSubTotal(into.products)

totalBeforeTax = (subtotal + shippingCost)
totalTaxes = (taxes * subtotal)
totalExtraFee = (totalTaxes * extraFee)
total = (subtotal + totalTaxes) + totalExtraFee
into.Summary = Summary{
total: total,
subtotal: subtotal,
totalBeforeTax: totalBeforeTax,
}

}

// функция Notify
func (into *InternationalOrder) Notify() {
email := into.Client.email
name := into.Client.name
phone := into.Client.phone

fmt.Println()
fmt.Println("---International Order---")
fmt.Println("Notifying: ", name)
fmt.Println("Sending email notification to :", email)
fmt.Println("Sending sms notification to :", phone)
fmt.Println("Sending whatsapp notification to :", phone)
}

// функция PrintOrderDetails
func (into *InternationalOrder) PrintOrderDetails() {
fmt.Println()
fmt.Println("International Summary")
fmt.Println("Order details: ")
fmt.Println("-- Total Before Taxes: ", into.Summary.totalBeforeTax)
fmt.Println("-- SubTotal: ", into.Summary.subtotal)
fmt.Println("-- Total: ", into.Summary.total)
fmt.Printf("-- Delivery Address to: %s %s %s \n", into.ShippingAddress.street, into.ShippingAddress.city, into.ShippingAddress.country)
fmt.Printf("-- Client: %s %s \n", into.Client.name, into.Client.lastName)
}

Этот файл является первой фактической реализацией интерфейса Operations. Сначала мы создали тип структуры InternationalOrder, определив с помощью структуры Order его свойства и объекты. Далее идет функция инициализации NewInternationalOrder, которая будет устанавливать товары для заказа, информацию о клиенте и адрес доставки.

Для инициализации ProductDetail, Client и ShippingAddress мы используем вспомогательную функцию, которую вскоре тоже реализуем.

В оставшейся части файла мы объявляем фактическую реализацию функций FillOrderSummary, Notify и PrintOrderDetails. Теперь можно сказать, что тип структуры InternationalOrder реализует интерфейс Operations, потому что содержит определения всех его методов. Круто!

Далее разберем реализацию файла nationalOrder.go:

package order

import (
"fmt"
)

// экземпляр national
var national = &NationalOrder{}

// структура NationalOrder
type NationalOrder struct {
Order
}

// функция NewNationalOrder
func NewNationalOrder() *NationalOrder {
national.products = append(national.products, GetProductDetail("Sugar", 12, 3, 36))
national.products = append(national.products, GetProductDetail("Cereal", 16, 2, 36))
national.Client = SetClient("Phill", "Heat", "phill@gmail.com", "8415748569")
national.ShippingAddress = SetShippingAddress("North Ave", "San Antonio", "USA", "854789")
return national
}

// функция FillOrderSummary
func (nato *NationalOrder) FillOrderSummary() {
var taxes float32 = 0.20
var shippingCost float32 = 5
subtotal = CalculateSubTotal(nato.products)

totalBeforeTax = (subtotal + shippingCost)
totalTaxes = (taxes * subtotal)
total = (subtotal + totalTaxes)

nato.Summary = Summary{
total,
subtotal,
totalBeforeTax,
}
}

// функция Notify
func (nato *NationalOrder) Notify() {
email := nato.Client.email
fmt.Println("---National Order---")
fmt.Println("Sending email notification to:", email)
}

// функция PrintOrderDetails
func (nato *NationalOrder) PrintOrderDetails() {
fmt.Println()
fmt.Println("National Summary")
fmt.Println("Order details: ")
fmt.Println("Total: ", nato.Summary.total)
fmt.Printf("Delivery Address to: %s %s %s \n", nato.ShippingAddress.street, nato.ShippingAddress.city, nato.ShippingAddress.country)
}

Этот файл представляет вторую фактическую реализацию интерфейса Operations. Здесь содержится тип структуры NationalOrder, который тоже использует тип структуры Order.

Далее идет функция инициализации, устанавливающая товары, информацию о клиенте и адрес доставки конкретного заказа внутри страны.

Затем, как и в предыдущем файле, следуют определения всех методов, необходимых для реализации интерфейса. Теперь структура NationalOrder тоже реализует интерфейс Operations, так как отвечает на все его методы.

Создав две этих реализации, можно передавать любые их экземпляры любым методам, опирающимся на интерфейс Operations.

Для завершения этого примера осталось только прописать вспомогательную функцию в файле helpers.go:

package order

var (
subtotal float32
total float32
totalBeforeTax float32
totalTaxes float32
totalExtraFee float32
)

// функция GetProductDetail, получающая в качестве аргументов поля, // необходимые для создания новой структуры ProductDetail, // и возвращающая ее.
func GetProductDetail(name string, price, amount int, total float32) (pd *ProductDetail) {
pd = &ProductDetail{
amount: amount,
total: total,
Product: Product{
name: name,
price: price,
},
}
return
}

// функция SetClient, получающая в качестве аргументов поля, // необходимые для создания новой структуры Client, и возвращающая // ее.
func SetClient(name, lastName, email, phone string) (cl Client) {
cl = Client{
name: name,
lastName: lastName,
email: email,
phone: phone,
}
return
}

// функция SetShippingAddress, получающая в качестве аргументов // поля, необходимые для создания новой структуры ShippingAddress, и // возвращающая ее.
func SetShippingAddress(street, city, country, cp string) (spa ShippingAddress) {
spa = ShippingAddress{
street,
city,
country,
cp,
}
return
}

// функция CalculateSubTotal
func CalculateSubTotal(products []*ProductDetail) (subtotal float32) {
for _, v := range products {
subtotal += v.total
}
return
}

Как уже говорилось, этот файл включает ряд вспомогательных функций для указания товаров, установки клиента и адреса доставки, а также вычисления предварительной суммы заказа.

Теперь программу можно запускать!

Если перейти в корневой каталог проекта и выполнить go run main.go, должен отобразиться следующий вывод:

Познакомившись с работой интерфейсов в Go, далее вы можете создавать больше функций, которые будут опираться на другие интерфейсы.

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

// функция PrintOrder
func PrintOrder(orders []Printer) {
for _, order := range orders {
order.PrintOrderDetails()
}
}

// функция NotifyClient
func NotifyClient(orders []Notifier) {
for _, order := range orders {
order.Notify()
}
}

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

Заключение

Интерфейсы — это отличный способ создания определяющих поведение абстракций. Теперь вы можете начать писать программы с их участием, абстрагируя поведение и задавая набор стандартных действий для разных объектов. При этом интерфейсы делают программу более гибкой и повышают переиспользуемость кода.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен

--

--

Дмитрий ПереводIT
NOP::Nuances of Programming

Также перевожу на Хабре под ником Bright_Translate. Тематика — электроника, программирование, технологии, DIY, научпоп и пр.