Реализация интерфейсов в 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", "[email protected]", "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", "[email protected]", "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 и Яндекс.Дзен


Перевод статьи Yair Fernando: Implementing Interfaces With Golang

Предыдущая статьяШесть фич YAML, о которых не знает большинство программистов
Следующая статьяРазработка виртуального помощника для удовлетворения основных потребностей пользователей