Menu
CoddyTech

Đóng gói ở cấp package

Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 48 trên 107.

Mặc dù bài học trước đã đề cập đến khả năng hiển thị ở cấp trường, tính đóng gói trong Go thực chất hoạt động ở cấp gói. Mọi thứ trong cùng một gói, bất kể nằm trong tệp nào, đều có thể truy cập các trường và hàm chưa được xuất.

Hãy xem xét một package có nhiều tệp:

// account/account.go
package account

type Account struct {
    balance int  // không công khai
}

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

func ResetAccount(a *Account) {
    a.balance = 0  // Hoạt động vì cùng package
}

Cả hai tệp đều thuộc package account, nên helper.go có thể truy cập trực tiếp vào trường chưa xuất khẩu balance. Điều này là có chủ đích. Cách này cho phép bạn chia logic của package thành nhiều tệp mà vẫn giữ quyền truy cập nội bộ.

Tuy nhiên, code bên ngoài package không thể truy cập các thành viên chưa được export:

// main.go
package main

import "account"

func main() {
    acc := &account.Account{}
    acc.Deposit(100)        // hoạt động: phương thức (method) được xuất
    // acc.balance = 50     // lỗi: trường không được xuất
    // account.ResetAccount // hoạt động nếu ResetAccount được xuất
}

Ranh giới ở cấp package này là cơ chế đóng gói chính của Go. Bạn thiết kế các package thành những đơn vị thống nhất, trong đó code nội bộ phối hợp tự do, còn code bên ngoài chỉ tương tác với API được xuất ra. Hãy xem mỗi package như một module độc lập, có giao diện công khai rõ ràng.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống quản lý hàng tồn kho để minh họa cách nhiều tệp trong cùng một package có thể phối hợp với nhau bằng cách truy cập trực tiếp vào các trường không được xuất, đồng thời giữ kín dữ liệu đó với code bên ngoài.

Bạn sẽ tạo ba tệp phối hợp với nhau trong package main:

  • product.go: Định nghĩa một cấu trúc Product có trường được xuất Name và trường không được xuất quantity (int). Thêm một phương thức được xuất Info() string trả về định dạng: [Name]: [quantity] in stock
  • inventory.go: Tạo các hàm trợ giúp thao tác với sản phẩm bằng cách truy cập trực tiếp vào trường không được xuất quantity (có thể làm vậy vì chúng nằm trong cùng một package). Hãy triển khai:
    • Restock(p *Product, amount int): cộng amount vào quantity của sản phẩm
    • Sell(p *Product, amount int) bool: trừ amount khỏi quantity nếu còn đủ hàng, trả về true nếu thành công hoặc false nếu không đủ hàng
    • NewProduct(name string, initialQty int) *Product: hàm khởi tạo tạo một sản phẩm với tên và số lượng ban đầu đã cho
  • main.go: Đọc thông tin sản phẩm từ đầu vào, tạo sản phẩm bằng hàm khởi tạo, thực hiện các thao tác với hàng tồn kho và hiển thị kết quả. Sau mỗi thao tác, in thông tin sản phẩm để thể hiện trạng thái đã cập nhật.

Đầu vào sẽ có các dòng sau:

  • Dòng 1: Tên sản phẩm
  • Dòng 2: Số lượng ban đầu (số nguyên)
  • Dòng 3: Số lượng nhập thêm (số nguyên)
  • Dòng 4: Số lượng bán (số nguyên)

Sau khi tạo sản phẩm, hãy in thông tin ban đầu của sản phẩm. Sau đó nhập thêm hàng và in lại thông tin. Cuối cùng, thử bán hàng và in thông tin đã cập nhật nếu bán thành công, hoặc Sale failed: insufficient stock nếu không đủ số lượng.

Ví dụ, với đầu vào Laptop, 10, 5 và 8, đầu ra sẽ là:

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

Với đầu vào Phone, 3, 2 và 10, đầu ra sẽ là:

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

Điểm mấu chốt ở đây là inventory.go có thể đọc và sửa đổi trực tiếp trường không được xuất quantity vì nó nằm trong cùng package với product.go. Đây là cách đóng gói ở cấp package: các thành phần bên trong có thể tự do phối hợp với nhau, trong khi dữ liệu vẫn được bảo vệ khỏi các package bên ngoài.

Tự làm thử

package main

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

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

	// Đọc tên sản phẩm
	name, _ := reader.ReadString('\n')
	name = strings.TrimSpace(name)

	// Đọc số lượng ban đầu
	initialQtyStr, _ := reader.ReadString('\n')
	initialQty, _ := strconv.Atoi(strings.TrimSpace(initialQtyStr))

	// Đọc số lượng nhập thêm
	restockStr, _ := reader.ReadString('\n')
	restockAmount, _ := strconv.Atoi(strings.TrimSpace(restockStr))

	// Đọc số lượng cần bán
	sellStr, _ := reader.ReadString('\n')
	sellAmount, _ := strconv.Atoi(strings.TrimSpace(sellStr))

	// TODO: Tạo sản phẩm mới bằng hàm khởi tạo NewProduct

	// TODO: In ra thông tin ban đầu của sản phẩm

	// TODO: Nhập thêm hàng và in ra thông tin sản phẩm

	// TODO: Thử bán sản phẩm và in ra kết quả
	// Nếu bán thành công, in ra thông tin đã cập nhật
	// Nếu bán không thành công, in ra "Sale failed: insufficient stock"

	fmt.Println() // Thay bằng đầu ra thực tế
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Go online