Ẩn thông tin trong Go
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 50 trên 107.
Ẩn thông tin không chỉ đơn thuần là làm cho các trường không được xuất. Đó là việc thiết kế các kiểu dữ liệu sao cho code bên ngoài chỉ biết những gì cần biết. Không hơn.
Mục tiêu là thể hiện hành vi thông qua các phương thức, đồng thời giữ kín hoàn toàn cấu trúc bên trong. Hãy xem cách làm này:
type Counter struct {
value int
maxLimit int
}
func NewCounter(limit int) *Counter {
return &Counter{maxLimit: limit}
}
func (c *Counter) Increment() bool {
if c.value >= c.maxLimit {
return false
}
c.value++
return true
}
func (c *Counter) Value() int {
return c.value
}
Code bên ngoài không biết Counter dùng int bên trong. Nó có thể là float64, một lát cắt hoặc một kiểu hoàn toàn khác. Bạn có thể thay đổi cách triển khai mà không ảnh hưởng đến code sử dụng Counter, miễn là các phương thức vẫn hoạt động như cũ.
Nguyên tắc này cũng áp dụng cho các hàm hỗ trợ. Giữ logic nội bộ không được xuất:
// Đã xuất, thuộc API công khai
func (c *Counter) Reset() {
c.value = 0
}
// Chưa xuất, dùng để hỗ trợ nội bộ
func (c *Counter) isAtLimit() bool {
return c.value >= c.maxLimit
}
Bằng cách ẩn isAtLimit(), bạn có thể tự do thay đổi hoặc xóa nó sau này. Tính đóng gói thông tin tạo ra ranh giới rõ ràng giữa những gì package của bạn cam kết với người dùng bên ngoài và cách package hoạt động bên trong.
Thử thách
DễHãy xây dựng một hệ thống ví an toàn để minh họa tính ẩn thông tin. Bạn sẽ tạo một ví kỹ thuật số, trong đó việc theo dõi số dư nội bộ và lịch sử giao dịch hoàn toàn được ẩn khỏi code bên ngoài: chỉ có hành vi của ví được thể hiện thông qua các phương thức.
Bạn sẽ sắp xếp code trong hai tệp:
wallet.go: Tạo một cấu trúcWalletẩn toàn bộ trạng thái nội bộ. Ví cần theo dõi số dư hiện tại và hạn mức chi tiêu ở bên trong, nhưng code bên ngoài không được biết chúng được lưu trữ hay biểu diễn như thế nào. Cung cấp các hành vi thông qua những phương thức sau:NewWallet(limit float64) *Wallet- hàm khởi tạo tạo một ví với hạn mức chi tiêu đã cho và số dư bằng 0Deposit(amount float64) bool- nạp tiền vào ví, trả vềtruenếu thành công (số tiền phải lớn hơn 0)Spend(amount float64) bool- trừ tiền nếu số dư đủ VÀ số tiền không vượt quá hạn mức chi tiêu, trả vềtruenếu thành côngBalance() float64- trả về số dư hiện tạiStatus() string- trả về thông báo trạng thái theo định dạng:Balance: $[balance] (Limit: $[limit])
main.go: Đọc cấu hình ví và các giao dịch từ đầu vào, tạo ví, thực hiện các thao tác và hiển thị kết quả sau mỗi thao tác.
Đầu vào sẽ gồm những thông tin sau:
- Dòng 1: Hạn mức chi tiêu
- Dòng 2: Số tiền nạp
- Dòng 3: Số tiền chi tiêu lần đầu
- Dòng 4: Số tiền chi tiêu lần thứ hai
Sau khi tạo ví, hãy in trạng thái ban đầu. Tiếp đó, thực hiện từng thao tác và in trạng thái đã cập nhật nếu thành công, hoặc Transaction failed nếu thao tác bị từ chối. Định dạng tất cả số tiền theo đô la với hai chữ số thập phân.
Ví dụ, với các giá trị 50, 100, 30 và 80, kết quả đầu ra sẽ là:
Balance: $0.00 (Limit: $50.00)
Balance: $100.00 (Limit: $50.00)
Balance: $70.00 (Limit: $50.00)
Transaction failedGiao dịch cuối cùng thất bại vì $80 vượt quá hạn mức chi tiêu $50, dù số dư vẫn đủ. Ví tự thực thi các quy tắc mà không tiết lộ cách đưa ra quyết định. Code bên ngoài chỉ cần gọi các phương thức và nhận kết quả, mà không biết các chi tiết triển khai nội bộ.
Tự làm thử
package main
import (
"fmt"
)
func main() {
// Đọc các giá trị đầu vào
var limit float64
var depositAmount float64
var spend1 float64
var spend2 float64
fmt.Scanln(&limit)
fmt.Scanln(&depositAmount)
fmt.Scanln(&spend1)
fmt.Scanln(&spend2)
// TODO: Tạo ví mới với hạn mức chi tiêu đã cho
// TODO: In ra trạng thái ban đầu
// TODO: Nạp tiền và in ra kết quả (trạng thái hoặc "Transaction failed")
// TODO: Chi tiêu lần đầu và in ra kết quả
// TODO: Chi tiêu lần thứ hai và in ra kết quả
}
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
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online