Nhúng và kết tập
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 38 trên 107.
Bạn đã học rằng nhúng giúp đưa các trường và phương thức ra struct bên ngoài. Go cũng hỗ trợ kết hợp, tức là thêm một struct khác làm trường có tên. Hiểu khi nào nên dùng mỗi cách là yếu tố then chốt để thiết kế tốt.
Với nhúng, bạn bỏ tên trường, và các thành viên của kiểu bên trong có thể được truy cập trực tiếp:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // Nhúng trực tiếp, không có tên trường
}
d := Document{}
d.Write() // Truy cập trực tiếp
Với phép kết hợp (aggregation), bạn đặt tên tường minh cho trường, và cần dùng tên đó để truy cập kiểu bên trong:
type Document struct {
writer Writer // kết tập: trường có tên
}
d := Document{writer: Writer{}}
d.writer.Write() // phải dùng tên trường
Điểm khác biệt chính nằm ở mối quan hệ mà bạn muốn thể hiện. Kết hợp nhúng ngụ ý mối quan hệ “là một”, trong đó kiểu bên ngoài mang danh tính của kiểu bên trong. Kết hợp chứa ngụ ý mối quan hệ “có một”, trong đó kiểu bên ngoài chỉ chứa kiểu bên trong.
Dùng nhúng (embedding) khi bạn muốn kiểu bên ngoài đáp ứng các giao diện mà kiểu bên trong triển khai, hoặc khi truy cập trực tiếp vào phương thức giúp API gọn hơn. Dùng kết tập (aggregation) khi kiểu bên trong là chi tiết triển khai không nên để lộ, hoặc khi bạn muốn kiểm soát rõ cách truy cập kiểu bên trong.
Thử thách
DễHãy xây dựng một hệ thống tạo báo cáo để minh họa khi nào nên dùng nhúng và khi nào nên dùng kết tập. Bạn sẽ tạo một hệ thống trong đó một số thành phần có thể được truy cập trực tiếp (nhúng), còn những thành phần khác vẫn là chi tiết triển khai nội bộ (kết tập).
Bạn sẽ sắp xếp code trong ba tệp:
formatter.go: Tạo một structFormattercó trườngStyle(chuỗi) và phương thứcFormat(text string) stringtrả về văn bản được bao quanh bởi kiểu định dạng, như[Style] text [/Style]. Đây sẽ là một thành phần hỗ trợ nội bộ, không nên để lộ trực tiếp.report.go: Tạo hai struct xử lý việc tạo báo cáo theo những cách khác nhau:- Một struct
Metadatacó các trườngAuthorvàDate. Struct này đại diện cho thông tin công khai mà bạn có thể truy cập trực tiếp. - Một struct
Reportcó trườngTitle, nhúngMetadatađể truy cập trực tiếp tác giả và ngày tháng, nhưng dùng kết tập vớiFormatternhư một chi tiết triển khai nội bộ qua trường có tênformatter.
Generate() stringvàoReport. Phương thức này dùng formatter nội bộ để định dạng tiêu đề và thêm thông tin metadata.- Một struct
main.go: Đọc thông tin báo cáo từ đầu vào, tạo mộtReportcó metadata được nhúng và formatter được kết tập. Minh họa sự khác biệt bằng cách truy cập trực tiếp tác giả từ report (thông qua nhúng), rồi tạo báo cáo đã định dạng (sử dụng formatter ẩn bên trong).
Đầu vào sẽ gồm:
- Dòng 1: Tiêu đề báo cáo
- Dòng 2: Tên tác giả
- Dòng 3: Ngày tháng
- Dòng 4: Kiểu định dạng của formatter
Phương thức Format của bạn cần trả về:
[Style] text [/Style]Phương thức Generate của bạn cần trả về:
[formatted title]
Author: [Author], Date: [Date]Trong tệp chính, hãy in ra ba dòng:
- Tác giả được truy cập trực tiếp từ Report (minh họa cách nhúng giúp truy cập trực tiếp trường)
- Ngày tháng được truy cập thông qua tên kiểu dữ liệu được nhúng (
report.Metadata.Date) - Kết quả gọi
Generate()
Ví dụ, với Quarterly Results, Jane Doe, 2024-01-15 và BOLD, đầu ra của bạn sẽ là:
Jane Doe
2024-01-15
[BOLD] Quarterly Results [/BOLD]
Author: Jane Doe, Date: 2024-01-15Lưu ý rằng bạn có thể truy cập trực tiếp Author và Date từ Report thông qua nhúng, trong khi formatter vẫn được ẩn. Bạn không thể gọi trực tiếp report.Format() vì formatter được kết tập qua một trường có tên. Đây là ví dụ thực tế về mối quan hệ “là một” và “có một”.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
title := scanner.Text()
scanner.Scan()
author := scanner.Text()
scanner.Scan()
date := scanner.Text()
scanner.Scan()
style := scanner.Text()
// TODO: Tạo struct Metadata chứa tác giả và ngày tháng
// TODO: Tạo Formatter với kiểu định dạng đã cho
// TODO: Tạo Report nhúng Metadata và chứa Formatter
// TODO: In ra tác giả bằng cách truy cập trực tiếp qua Report (việc nhúng đưa trường này lên Report)
// TODO: In ra ngày tháng bằng cách truy cập qua tên kiểu được nhúng (report.Metadata.Date)
// TODO: In ra kết quả khi gọi Generate()
// Ẩn cảnh báo về các biến chưa dùng (xóa các dòng này khi bạn sử dụng các biến đó)
_ = title
_ = author
_ = date
_ = style
}
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