Menu
Coddy logo textTech

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

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

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

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

// 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. Это сделано намеренно. Такой подход позволяет разделить логику пакета между файлами, сохраняя внутренний доступ.

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

// main.go
package main

import "account"

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

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

challenge icon

Задание

Легко

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

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

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

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

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

После создания продукта выведите его начальный info. Затем пополните запас и снова выведите info. Наконец, попытайтесь выполнить продажу и выведите либо обновлённый info (при успешном выполнении), либо 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. Это инкапсуляция на уровне пакета в действии: внутреннее взаимодействие происходит свободно, а данные остаются защищёнными от внешних пакетов.

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

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: Попытаться продать и вывести результат
	// Если продажа успешна, вывести обновлённую информацию
	// If sale fails, print "Sale failed: insufficient stock"

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

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор Go