Что возвращать в Go: структуры или интерфейсы?

Введение

На Go часто обнаруживается проблема выбора различных подходов. Какие значения возвращать из функций: структуры или интерфейсы  —  решение немаловажное. Мы будто на перекрестке двух путей с их преимуществами и особенностями. Исследуем их, отыскивая скрытые жемчужины структур и интерфейсов.

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

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

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

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


Возвращение структур

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

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

Эксплицитность

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

Получая прямой доступ к свойствам структуры, мы имеем четкое представление о том, что внутри нее, поэтому работа с данными, манипулирование ими упрощаются.

Рассмотрим пример:

type Person struct {
Name string
Age int
Email string
}

func NewPerson(name string, age int, email string) Person {
return Person{
Name: name,
Age: age,
Email: email,
}
}

func main() {
person := NewPerson("John Doe", 30, "[email protected]")
fmt.Println("Name:", person.Name)
fmt.Println("Age:", person.Age)
fmt.Println("Email:", person.Email)
}

В функции NewPerson возвращается структура Person с информацией о человеке. Возвращая структуру напрямую, мы получаем явный доступ к ее полям Name, Age, Email в вызывном коде.

Благодаря такой эксплицитности необходимые данные легко извлечь и использовать.

Гибкость

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

Благодаря возможности адаптировать ее к конкретным требованиям, код получается чище, целенаправленнее и с точным соответствием задачам приложения.

Продемонстрируем эту гибкость, обновив пример выше:

type Person struct {
Name string
Age int
Email string
}

func (p Person) Greet() {
fmt.Println("Hello, my name is", p.Name)
}

func main() {
person := Person{
Name: "John Doe",
Age: 30,
Email: "[email protected]",
}
person.Greet()
}

В структуру Person добавили метод Greet. Возвращая структуру, мы определяем конкретное поведение прямо в ее типе. В методе Greet приветствие персонализируется полем Name структуры Person.

Благодаря такой гибкости поведения инкапсулируются в самой структуре, а код чище и проще в сопровождении.

Сильная связанность

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

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

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

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

Возвращение интерфейсов

Совершенно новый уровень гибкости и модульности достигается при возвращении из функций Go интерфейсов. Рассмотрим преимущества возвращения интерфейсов и сравним их со структурами.

Абстракция

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

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

Проиллюстрируем примером:

type Shape interface {
Area() float64
}

type Circle struct {
Radius float64
}

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

type Rectangle struct {
Width float64
Height float64
}

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func GetShape() Shape {
if someCondition {
return Circle{Radius: 5}
} else {
return Rectangle{Width: 10, Height: 5}
}
}

Здесь в методе Area определяется интерфейс Shape, реализуемый двумя типами структур: Circle и Rectangle. Функцией GetShape возвращается фигура Shape: в зависимости от условия, это круг Circle или прямоугольник Rectangle. Когда возвращается интерфейс, возвращаемый объект в вызывном коде считается фигурой Shape, акцент делается на гарантируемом ею поведении в методе Area, не вдаваясь в конкретные детали реализации каждой фигуры.

Полиморфизм

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

Расширим пример выше:

func PrintArea(s Shape) {
fmt.Println("Area:", s.Area())
}

func main() {
circle := Circle{Radius: 5}
rectangle := Rectangle{Width: 10, Height: 5}

PrintArea(circle) // Полиморфно обращаемся с кругом «Circle» как с фигурой «Shape»
PrintArea(rectangle) // Полиморфно обращаемся с прямоугольником «Rectangle» как с фигурой «Shape»
}

Здесь в функции PrintArea в качестве параметра принимается интерфейс Shape. Передаем в нее две структуры Circle и Rectangle, полиморфно обращаясь с ними как с фигурой Shape. Так пишется переиспользуемый код для работы с общими поведениями, определенными интерфейсом: дублировать логику для каждого типа структуры не нужно.

Слабая связанность

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

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

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

Как выбрать?

Настал судьбоносный момент принятия решения. Выбрать между возвращением структур и интерфейсов  —  все равно что создать шедевр, адаптированный к задачам приложения. Добавим факторы идеального выбора.

Требования приложения

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

Знайте приложение свое, и откроется путь, по которому вам идти.

Переиспользуемость кода

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

Будущая гибкость

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

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

Итак, находясь на перекрестке структур и интерфейсов, руководствуйтесь задачами приложения. Наслаждайтесь переиспользуемостью кода и модульным дизайном, не забывая об изменчивой будущей гибкости. Вашим выбором сконфигурируется судьба кодовой базы, откроется путь к элегантности, сопровождаемости, неизменному успеху. Вперед, дорогой читатель, навстречу этому судьбоносному выбору структуры/интерфейса!

Прецеденты и примеры

Привнесем в наше исследование магию реальных сценариев, изучив практические примеры.

Возвращение структуры

Разрабатывая соцсеть, имеем функцию для извлечения пользовательских профилей. При возвращении структуры UserProfile обеспечиваются эксплицитность и четкая видимость. В вызывном коде получаем прямой доступ к полям Name, Age и Bio и отображаем их, контролируя детализированные данные профиля. Этими данными легко оперировать, форматировать их перед представлением пользователям:

type UserProfile struct {
Name string
Age int
Bio string
}

func GetUserProfile(userID int) UserProfile {
// Извлекаем профиль из базы данных
// Выполняем необходимые преобразования или проверки
return UserProfile{
Name: "John Doe",
Age: 30,
Bio: "Passionate about coding and exploring new technologies.",
}
}

Возвращение интерфейсов

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

При возвращении интерфейса ShippingProvider взаимодействие с доставщиками упрощается: вызывной код должен «знать» только об определенных в интерфейсе методах GetShippingCost или GenerateLabel. Обеспечиваются гибкость, переиспользуемость кода, и мы легко переключаемся между доставщиками, руководствуясь бизнес-задачами:

type ShippingProvider interface {
GetShippingCost(weight float64) float64
GenerateLabel(address string) string
}

type FedEx struct{}

func (f FedEx) GetShippingCost(weight float64) float64 {
// Реализуем логику для расчета стоимости доставки FedEx
return 10.99
}

func (f FedEx) GenerateLabel(address string) string {
// Реализуем логику для генерирования этикетки отгрузки FedEx
return "FedEx Label"
}

type UPS struct{}

func (u UPS) GetShippingCost(weight float64) float64 {
// Реализуем логику для расчета стоимости доставки UPS
return 8.99
}

func (u UPS) GenerateLabel(address string) string {
// Реализуем логику для генерирования этикетки отгрузки UPS
return "UPS Label"
}

func GetShippingProvider() ShippingProvider {
if someCondition {
return FedEx{}
} else {
return UPS{}
}
}

В этом примере определяется интерфейс ShippingProvider с методами получения стоимости доставки и генерирования этикеток, реализуемый двумя типами структур: FedEx и UPS. Функцией GetShippingProvider возвращается ShippingProvider, то есть в вызывном коде все доставщики взаимозаменяемы. Благодаря такой гибкости легко переключаться между доставщиками, не модифицируя вызывной код, чем обеспечивается адаптируемость приложения к изменчивым требованиям бизнеса.

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

Лучшие практики

Выбирая между структурами и интерфейсами, помните об этих рекомендациях.

Простота и ясность

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

Необходимый уровень абстракции

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

Потенциал переиспользуемости кода и модульности

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

Будущие расширяемость и изменения

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

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

Заключение

Вот и все. Мы исследовали возвращение структур и интерфейсов на Go. Подытожим ключевые моменты.

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

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

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

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

При написании кода учитывайте описанные выше преимущества и особенности. Стремитесь к простоте, ясности, переиспользуемости кода. Оценивайте уровень абстракции, планируйте будущие изменения, и ваш код будет развиваться, адаптируясь к веяниям времени.

Но на этом приключения не заканчиваются. В обширной области разработки ПО еще многое предстоит изучить и открыть. Так что не ждите: следите за нами, читайте и другие наши статьи в Telegram, VK и Дзене. Раскрывайте секреты изменчивого мира технологий вместе с нами.

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

Читайте нас в Telegram, VK и Дзен


Перевод статьи Mou Sam Dahal: Between Returning Structs and Interfaces in Go

Предыдущая статьяСоздание собственной версии UseCase в 2023 году: гибкий и функциональный подход
Следующая статьяСобытийно-ориентированная архитектура