Menu
Coddy logo textTech

Patron Commande

Fait partie de la section Programmation Orientée Objet du Journey GO de Coddy — leçon 91 sur 107.

Le patron Commande encapsule une requête sous la forme d'un objet, vous permettant ainsi de paramétrer des opérations, de les mettre en file d'attente ou de prendre en charge la fonctionnalité d'annulation. Alors que Strategy encapsule des algorithmes, Commande encapsule des actions entières ainsi que leurs paramètres.

En Go, nous définissons une interface Command avec une méthode Execute, puis nous créons des commandes concrètes qui détiennent toutes les informations nécessaires pour effectuer une action :

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"
}

Un invoker stocke et exécute des commandes sans savoir ce qu'elles font :

type RemoteControl struct {
    command Command
}

func (r *RemoteControl) SetCommand(c Command) {
    r.command = c
}

func (r *RemoteControl) PressButton() string {
    return r.command.Execute()
}

L'appelant est complètement découplé du récepteur (la Lumière) :

light := &Light{}
remote := &RemoteControl{}

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // Lumière allumée

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // Lumière éteinte

Le pattern Command est idéal pour implémenter des systèmes d'annulation/rétablissement, des files d'attente de tâches ou l'enregistrement de macros lorsque vous devez stocker, différer ou rejouer des opérations.

challenge icon

Défi

Facile

Construisons un éditeur de texte avec une fonctionnalité d'annulation (undo) en utilisant le pattern Command ! Vous allez créer des commandes qui encapsulent les opérations de texte, permettant à l'éditeur d'exécuter des actions et de les inverser — un cas d'utilisation classique qui démontre pourquoi les commandes sont si puissantes.

Vous organiserez votre code sur trois fichiers :

  • command.go : Définissez votre interface de commande et les commandes concrètes d'édition de texte.

    Créez une interface Command avec deux méthodes : Execute() string pour effectuer l'action et Undo() string pour l'inverser.

    Implémentez deux types de commandes qui opèrent sur un *TextEditor :

    • InsertCommand — contient le pointeur de l'éditeur et le texte à insérer. Execute ajoute le texte au contenu de l'éditeur et retourne Inserted: [text]. Undo supprime ce texte de la fin et retourne Undone insert: [text]
    • DeleteCommand — contient le pointeur de l'éditeur et un nombre de caractères à supprimer de la fin. Execute supprime ces caractères (en les stockant pour l'annulation) et retourne Deleted: [removed text]. Undo les restaure et retourne Undone delete: [restored text]
  • editor.go : Créez votre éditeur de texte (le récepteur) et l'invocateur (invoker) qui gère l'historique des commandes.

    Construisez une structure TextEditor avec un champ Content (string) et des méthodes Append(text string) et DeleteLast(count int) string (retourne le texte supprimé).

    Construisez une structure EditorInvoker qui contient une tranche (slice) de commandes exécutées comme historique. Ajoutez ces méthodes :

    • ExecuteCommand(cmd Command) string — exécute la commande, l'ajoute à l'historique et retourne le résultat
    • UndoLast() string — supprime la dernière commande de l'historique, appelle son Undo et retourne le résultat. Si l'historique est vide, retournez Nothing to undo
  • main.go : Démontrez votre système de commande avec une série d'opérations.

    Lisez un nombre d'opérations. Pour chaque opération, lisez le type d'opération (insert, delete ou undo). Pour insert, lisez également le texte à insérer. Pour delete, lisez le nombre de caractères. Exécutez chaque opération via votre invocateur et affichez le résultat. Après toutes les opérations, affichez le contenu final de l'éditeur sous la forme Final: [content].

Les entrées suivantes seront fournies :

  • Ligne 1 : Nombre d'opérations
  • Pour chaque opération : type d'opération, puis données supplémentaires si nécessaire (texte pour insert, nombre pour delete)

Par exemple, étant donné :

5
insert
Hello
insert
 World
delete
3
undo
undo

Votre sortie devrait être :

Inserted: Hello
Inserted:  World
Deleted: rld
Undone delete: rld
Undone insert:  World
Final: Hello

Et étant donné :

4
insert
Go
insert
Lang
undo
insert
!

Votre sortie devrait être :

Inserted: Go
Inserted: Lang
Undone insert: Lang
Inserted: !
Final: Go!

Et étant donné :

2
undo
insert
Test

Votre sortie devrait être :

Nothing to undo
Inserted: Test
Final: Test

Remarquez comment chaque commande encapsule tout ce qui est nécessaire pour effectuer et inverser son action. L'invocateur ne sait pas ce que font les commandes — il se contente de les exécuter et de maintenir l'historique pour le support de l'annulation !

Aide-mémoire

Le pattern Command encapsule une requête sous forme d'objet, vous permettant de paramétrer des opérations, de les mettre en file d'attente ou de prendre en charge la fonctionnalité d'annulation (undo).

Définissez une interface Command avec une méthode Execute :

type Command interface {
    Execute() string
}

Créez des commandes concrètes qui contiennent toutes les informations nécessaires pour effectuer une action :

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"
}

Un invocateur (invoker) stocke et exécute des commandes sans savoir ce qu'elles font :

type RemoteControl struct {
    command Command
}

func (r *RemoteControl) SetCommand(c Command) {
    r.command = c
}

func (r *RemoteControl) PressButton() string {
    return r.command.Execute()
}

Exemple d'utilisation montrant le découplage entre l'invocateur et le récepteur :

light := &Light{}
remote := &RemoteControl{}

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // Light turned on

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // Light turned off

Pour la fonctionnalité d'annulation, ajoutez une méthode Undo à l'interface Command et maintenez un historique des commandes dans l'invocateur.

Essayez vous-même

package main

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

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// Lire le nombre d'opérations
	line, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(line))
	
	// Créer l'éditeur et l'invocateur
	editor := &TextEditor{}
	invoker := &EditorInvoker{}
	
	for i := 0; i < numOps; i++ {
		// Lire le type d'opération
		opLine, _ := reader.ReadString('\n')
		opType := strings.TrimSpace(opLine)
		
		var result string
		
		switch opType {
		case "insert":
			// Lire le texte à insérer
			textLine, _ := reader.ReadString('\n')
			text := strings.TrimSuffix(textLine, "\n")
			
			// TODO: Créer une InsertCommand et l'exécuter via l'invocateur
			_ = text
			_ = editor
			
		case "delete":
			// Lire le nombre de caractères à supprimer
			countLine, _ := reader.ReadString('\n')
			count, _ := strconv.Atoi(strings.TrimSpace(countLine))
			
			// TODO: Créer une DeleteCommand et l'exécuter via l'invocateur
			_ = count
			
		case "undo":
			// TODO: Appeler UndoLast sur l'invocateur
			
		}
		
		fmt.Println(result)
	}
	
	// Afficher le contenu final
	fmt.Printf("Final: %s\n", editor.Content)
}
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation Orientée Objet