Mẫu Command
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 91 trên 107.
Mẫu Command đóng gói một yêu cầu thành một đối tượng, giúp bạn tham số hóa các thao tác, xếp chúng vào hàng đợi hoặc hỗ trợ chức năng hoàn tác. Strategy đóng gói các thuật toán, còn Command đóng gói toàn bộ hành động cùng với các tham số của chúng.
Trong Go, chúng ta định nghĩa một giao diện Command với phương thức Execute, sau đó tạo các lệnh cụ thể chứa mọi thông tin cần thiết để thực hiện một hành động:
type Command interface {
Execute() string
}
type Light struct {
IsOn bool
}
type TurnOnCommand struct {
light *Light
}
func (c *TurnOnCommand) Execute() string {
c.light.IsOn = true
return "Light turned on"
}
type TurnOffCommand struct {
light *Light
}
func (c *TurnOffCommand) Execute() string {
c.light.IsOn = false
return "Light turned off"
}Bộ gọi lưu trữ và thực thi các lệnh mà không biết chúng làm gì:
type RemoteControl struct {
command Command
}
func (r *RemoteControl) SetCommand(c Command) {
r.command = c
}
func (r *RemoteControl) PressButton() string {
return r.command.Execute()
}Bộ gọi lệnh hoàn toàn tách rời khỏi đối tượng nhận lệnh (đèn):
light := &Light{}
remote := &RemoteControl{}
remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton()) // Đèn đã bật
remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton()) // Đèn đã tắtMẫu Command rất phù hợp để xây dựng hệ thống hoàn tác/làm lại, hàng đợi tác vụ hoặc chức năng ghi macro khi bạn cần lưu trữ, trì hoãn hoặc phát lại các thao tác.
Thử thách
DễHãy xây dựng một trình soạn thảo văn bản có chức năng hoàn tác bằng mẫu Command! Bạn sẽ tạo các lệnh đóng gói những thao tác trên văn bản, cho phép trình soạn thảo thực hiện rồi đảo ngược hành động: đây là một trường hợp sử dụng kinh điển cho thấy vì sao các lệnh lại mạnh mẽ đến vậy.
Bạn sẽ sắp xếp code trong ba tệp:
command.go: Định nghĩa giao diện lệnh và các lệnh chỉnh sửa văn bản cụ thể.Tạo giao diện
Commandvới hai phương thức:Execute() stringđể thực hiện hành động vàUndo() stringđể đảo ngược hành động đó.Triển khai hai kiểu lệnh thao tác trên
*TextEditor:InsertCommand: lưu con trỏ đến trình soạn thảo và văn bản cần chèn. Execute nối văn bản vào nội dung của trình soạn thảo và trả vềInserted: [text]. Undo xóa văn bản đó khỏi cuối nội dung và trả vềUndone insert: [text]DeleteCommand: lưu con trỏ đến trình soạn thảo và số ký tự cần xóa ở cuối nội dung. Execute xóa các ký tự đó (lưu chúng để hoàn tác) và trả vềDeleted: [removed text]. Undo khôi phục chúng và trả vềUndone delete: [restored text]
editor.go: Tạo trình soạn thảo văn bản (bên nhận) và đối tượng gọi quản lý lịch sử lệnh.Tạo cấu trúc
TextEditorvới trườngContent(chuỗi) và các phương thứcAppend(text string)vàDeleteLast(count int) string(trả về văn bản đã xóa).Tạo cấu trúc
EditorInvokerlưu một lát cắt các lệnh đã thực thi làm lịch sử. Thêm các phương thức sau:ExecuteCommand(cmd Command) string: thực thi lệnh, thêm lệnh vào lịch sử và trả về kết quảUndoLast() string: xóa lệnh cuối cùng khỏi lịch sử, gọi Undo của lệnh đó và trả về kết quả. Nếu lịch sử trống, trả vềNothing to undo
main.go: Minh họa hệ thống lệnh bằng một loạt thao tác.Đọc số lượng thao tác. Với mỗi thao tác, đọc loại thao tác (
insert,deletehoặcundo). Với thao tác insert, đọc thêm văn bản cần chèn. Với thao tác delete, đọc số ký tự cần xóa. Thực hiện từng thao tác thông qua đối tượng gọi và in ra kết quả. Sau tất cả thao tác, in nội dung cuối cùng của trình soạn thảo theo định dạngFinal: [content].
Đầu vào sẽ có định dạng như sau:
- Dòng 1: Số lượng thao tác
- Với mỗi thao tác: loại thao tác, sau đó là dữ liệu bổ sung nếu cần (văn bản cần chèn với insert, số lượng ký tự cần xóa với delete)
Ví dụ, với đầu vào:
5
insert
Hello
insert
World
delete
3
undo
undoĐầu ra của bạn sẽ là:
Inserted: Hello
Inserted: World
Deleted: rld
Undone delete: rld
Undone insert: World
Final: HelloVới đầu vào:
4
insert
Go
insert
Lang
undo
insert
!Đầu ra của bạn sẽ là:
Inserted: Go
Inserted: Lang
Undone insert: Lang
Inserted: !
Final: Go!Với đầu vào:
2
undo
insert
TestĐầu ra của bạn sẽ là:
Nothing to undo
Inserted: Test
Final: TestLưu ý cách mỗi lệnh đóng gói mọi thứ cần thiết để vừa thực hiện vừa đảo ngược hành động của nó. Đối tượng gọi không biết các lệnh làm gì. Nó chỉ thực thi chúng và duy trì lịch sử để hỗ trợ hoàn tác!
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc số thao tác
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// Tạo editor và invoker
editor := &TextEditor{}
invoker := &EditorInvoker{}
for i := 0; i < numOps; i++ {
// Đọc loại thao tác
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
var result string
switch opType {
case "insert":
// Đọc văn bản cần chèn
textLine, _ := reader.ReadString('\n')
text := strings.TrimSuffix(textLine, "\n")
// TODO: Tạo InsertCommand và thực thi lệnh thông qua invoker
_ = text
_ = editor
case "delete":
// Đọc số ký tự cần xóa
countLine, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countLine))
// TODO: Tạo DeleteCommand và thực thi lệnh thông qua invoker
_ = count
case "undo":
// TODO: Gọi UndoLast trên invoker
}
fmt.Println(result)
}
// In ra nội dung cuối cùng
fmt.Printf("Final: %s\n", editor.Content)
}
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ệp11Thư viện chuẩn và OOP
io.Reader và io.Writersort.Interfaceinterface fmt.Stringerencoding/json với structinterface http.HandlerÔn tập: mô hình REST API14Mẫu thiết kế phần 2
Mẫu CommandMẫu AdapterMẫu DecoratorMẫu Template MethodMẫu StateMẫu CompositeMiddleware dưới dạng Decorator3Con 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