Menu
CoddyTech

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ắt

Mẫ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.

challenge icon

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 Command vớ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 TextEditor với trường Content (chuỗi) và các phương thức Append(text string) và DeleteLast(count int) string (trả về văn bản đã xóa).

    Tạo cấu trúc EditorInvoker lư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, delete hoặc undo). 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ạng Final: [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: Hello

Vớ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: Test

Lư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)
}
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