Menu
Coddy logo textTech

Инкапсуляция на уровне пакетов

Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 48 из 107.

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

Рассмотрим пакет с несколькими файлами:

// account/account.go
package account

type Account struct {
    balance int  // неэкспортируемое
}

func (a *Account) Deposit(amount int) {
    a.balance += amount
}
// account/helper.go
package account

func ResetAccount(a *Account) {
    a.balance = 0  // работает - тот же пакет
}

Оба файла относятся к пакету account, поэтому helper.go может напрямую обращаться к неэкспортируемому полю balance. Это сделано намеренно — это позволяет разделять логику пакета между файлами, сохраняя при этом внутренний доступ.

Однако код вне пакета не может получить доступ к неэкспортируемым членам:

// main.go
package main

import "account"

func main() {
    acc := &account.Account{}
    acc.Deposit(100)        // работает — экспортируемый метод
    // acc.balance = 50     // ошибка — неэкспортируемое поле
    // account.ResetAccount // работало бы, если бы ResetAccount был экспортируемым
}

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

challenge icon

Задание

Легко

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

Вы создадите три файла, которые работают вместе в пакете main:

  • product.go: Определите структуру Product с экспортируемым полем Name и неэкспортируемым полем quantity (int). Добавьте экспортируемый метод Info() string, который возвращает строку в формате: [Name]: [quantity] in stock
  • inventory.go: Создайте вспомогательные функции, которые работают с продуктами, напрямую обращаясь к неэкспортируемому полю quantity (это возможно, так как они находятся в одном пакете). Реализуйте:
    • Restock(p *Product, amount int) — добавляет указанное количество к количеству товара
    • Sell(p *Product, amount int) bool — вычитает количество из запаса, если товара достаточно, возвращает true в случае успеха или false, если товара недостаточно
    • NewProduct(name string, initialQty int) *Product — конструктор, который создает продукт с заданным именем и начальным количеством
  • main.go: Считайте данные о продукте из входных данных, создайте продукт с помощью конструктора, выполните операции с запасами и выведите результаты. После каждой операции выводите информацию о продукте, чтобы показать обновленное состояние.

Будут предоставлены следующие входные данные:

  • Строка 1: Название продукта
  • Строка 2: Начальное количество (целое число)
  • Строка 3: Количество для пополнения (целое число)
  • Строка 4: Количество для продажи (целое число)

После создания продукта выведите его начальную информацию. Затем пополните запас и снова выведите информацию. Наконец, попытайтесь продать товар и выведите либо обновленную информацию (в случае успеха), либо Sale failed: insufficient stock (если количества недостаточно).

Например, для входных данных Laptop, 10, 5 и 8 ваш вывод должен быть следующим:

Laptop: 10 in stock
Laptop: 15 in stock
Laptop: 7 in stock

А для Phone, 3, 2 и 10 ваш вывод должен быть таким:

Phone: 3 in stock
Phone: 5 in stock
Sale failed: insufficient stock

Ключевая идея здесь заключается в том, что inventory.go может напрямую читать и изменять неэкспортируемое поле quantity, потому что оно находится в том же пакете, что и product.go. Это инкапсуляция на уровне пакета в действии — внутреннее взаимодействие происходит свободно, в то время как данные остаются защищенными от внешних пакетов.

Шпаргалка

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

Доступ на уровне пакета

Неэкспортируемые члены доступны во всех файлах одного и того же пакета:

// account/account.go
package account

type Account struct {
    balance int  // неэкспортируемое
}

func (a *Account) Deposit(amount int) {
    a.balance += amount
}
// account/helper.go
package account

func ResetAccount(a *Account) {
    a.balance = 0  // работает — тот же пакет
}

Ограничения внешних пакетов

Код за пределами пакета не может получить доступ к неэкспортируемым членам:

// main.go
package main

import "account"

func main() {
    acc := &account.Account{}
    acc.Deposit(100)        // работает — экспортируемый метод
    // acc.balance = 50     // ошибка — неэкспортируемое поле
}

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

Попробуйте сами

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// Прочитать название продукта
	name, _ := reader.ReadString('\n')
	name = strings.TrimSpace(name)

	// Прочитать начальное количество
	initialQtyStr, _ := reader.ReadString('\n')
	initialQty, _ := strconv.Atoi(strings.TrimSpace(initialQtyStr))

	// Прочитать количество для пополнения
	restockStr, _ := reader.ReadString('\n')
	restockAmount, _ := strconv.Atoi(strings.TrimSpace(restockStr))

	// Прочитать количество для продажи
	sellStr, _ := reader.ReadString('\n')
	sellAmount, _ := strconv.Atoi(strings.TrimSpace(sellStr))

	// TODO: Создать новый продукт, используя конструктор NewProduct

	// TODO: Вывести начальную информацию о продукте

	// TODO: Пополнить запас продукта и вывести информацию

	// TODO: Попытаться продать и вывести результат
	// Если продажа прошла успешно, вывести обновленную информацию
	// Если продажа не удалась, вывести "Sale failed: insufficient stock"

	fmt.Println() // Заменить на фактический вывод
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование