Teknik Detaylar

Command Tasarım Deseni Nedir?

← Teknik Detaylar
2021-10-01 · 8 dk okuma
Command Tasarım Deseni Nedir?
Bu yazıyı yapay zekâ ile tartış
Sayfayı kopyala

Bu yazı Design Patterns/Tasarım Desenleri Nedir? başlıklı yazı dizisinin bir parçasıdır.

Bu içerik ağırlıklı olarak refactoring.guru sitesindeki içeriğin tercümesi ve derlenmesinden oluşturulmuştur.

Tüm tasarım desenleri ya da diğer adıyla tasarım kalıplarına yönelik ayrıntılı içeriklere yazının sonundaki bağlantılardan ulaşabilirsiniz.

💡 Özet (TL;DR):

  • Amaç: Bir isteği veya işlemi, o işlemle ilgili tüm parametreleri ve metot çağrılarını içeren bağımsız bir nesneye dönüştürür.
  • Kilit Yapılar: Gönderici (Invoker), Alıcı (Receiver), Komut Arayüzü (Command) ve Somut Komutlar (ConcreteCommand).
  • Avantajları: İşlemleri kuyruğa almayı, loglamayı, çalışma zamanını geciktirmeyi ve geri almayı (Undo/Redo) kolaylaştırır. İsteği tetikleyen ile gerçekleştireni birbirinden ayırır (decoupling).

Command Tasarım Deseninin Amacı

Command (Komut), bir isteği kendisi ile ilgili tüm bilgileri içeren bağımsız bir nesneye dönüştüren davranışsal (behavioral) bir tasarım desenidir. Bu dönüşüm, istekleri metot parametresi olarak göndermenize, işlenmelerini geciktirmenize ya da sıraya sokmanıza ve geri alınabilir işlemleri desteklemenize olanak verir.


Sorun

Bir metin editörü uygulaması yazdığınızı düşünün. İlk işiniz, çeşitli işlevleri çalıştıran düğmeler içeren bir araç çubuğu yapmak. Araç çubuğunda ve çeşitli diyalog pencerelerinde kullanılabilecek genel bir Button sınıfı oluşturdunuz.

Command tasarım deseni düğme sınıfı örneği

Bu düğmelerin her biri benzer görünse de farklı işlevleri var. Bu düğmelerin tıklanma olaylarına (click event) ilişkin kodları nereye yazacaksınız? Akla gelen en basit çözüm, düğmenin kullanılacağı her yer için bir sürü düğme alt sınıfı oluşturmak olacaktır. Bu alt sınıflar o düğme türüne tıklandığında yapılacak işlemin kodunu içerecektir.

Command tasarım deseni düğme alt sınıfları

Çok geçmeden bu yaklaşımın hatalı olduğunu fark edeceksiniz. İlk olarak, çok fazla sayıda alt sınıfla uğraşmanız gerekecek ve ana düğme sınıfında yapacağınız değişikliklerin bu alt sınıfların bazılarını bozma riskini de almış olacaksınız. Arayüz (GUI) için yazdığınız kodun, asıl işlev kodu ile çok fazla iç içe geçmesi de cabası.

Command tasarım deseni

Ve geldik en kötü tarafına. "Metni kopyala/yapıştır" gibi bazı fonksiyonların farklı yerlerden çağrılması gerekecektir. Örneğin kullanıcı araç çubuğundaki "Kopyala" butonuna tıklayabilir, sağ tıklayarak açılan menüden kopyalayı seçebilir veya CTRL+C klavye kısayolunu kullanabilir. Kopyalama işlevini gerçekleştiren kodu tüm bu tetikleyiciler için kopyala-yapıştır yaparak mı çoğaltacaksınız?


Çözüm

İyi bir yazılım tasarımı, uygulamanın katmanlara ayrılması ile sonuçlanan işlevlerin ayrılması ilkesine (separation of concerns) dayanır. En yaygın örnek: Grafik arayüz (GUI) için bir katman, uygulama iş mantığı (business logic) için ise başka bir katman olmasıdır.

Command tasarım deseni katmanları ayırma

Command deseni, GUI nesnelerinin bu isteği doğrudan göndermemesini tavsiye eder. Bunun yerine, çağrılan nesne, metot adı ve gönderilen parametreleri bir "Komut" (Command) sınıfı içine aktarmanızı ve isteği bu nesne içindeki tek bir çalıştırıcı metotla tetiklemenizi önerir.

Command nesneleri, GUI nesneleri ile iş mantığı nesneleri arasında bağlantı sağlayan bir köprü görevi görür. GUI nesnesi isteği hangi iş mantığı nesnesinin alacağını veya nasıl işleyeceğini bilmek zorunda kalmaz. GUI nesnesi sadece ilgili Command nesnesini tetikler ve gerisini o nesne halleder.

Command tasarım deseni örnek

Sonraki adım, tüm command nesnelerinin aynı arayüzü (interface) paylaşmasıdır. Bu arayüz genellikle parametre almayan tek bir execute() metodundan oluşur. Arayüzün aynı olması sayesinde, aynı tetikleyici (örneğin buton nesnesi) ile farklı komutları rahatça çalıştırabiliriz. Çalışma zamanında tetikleyicinin bağlı olduğu komut nesnesini değiştirerek davranışını dinamik olarak değiştirebiliriz.

Command tasarım deseni örneği

Metin editörü örneğimize geri dönersek; Command desenini uyguladıktan sonra farklı tıklama davranışları sergileyen alt düğme sınıfları oluşturmamıza gerek kalmaz. Düğmeye sadece bir command nesnesi referansı verip, kendisine tıklandığında bu command nesnesinin execute() metodunu çalıştırmasını söyleyebiliriz. Aynı command nesnesini sağ tık menüsüne ve klavye kısayollarına da bağlayarak kod tekrarını tamamen engelleriz.


Gerçek Hayat Senaryosu: Geri Al (Undo) Destekli Dosya Sistemi Komutları

Backend mimarilerinde dosya işlemleri gibi geri alınması gereken işlemler için komut deseni şu şekilde kurgulanır:

// 1. Alıcı Sınıf (Receiver) - Gerçek işi yapan sınıf
class FileSystem {
    public function createFile(string $path): void {
        echo "Dosya oluşturuldu: $path\n";
    }

    public function deleteFile(string $path): void {
        echo "Dosya silindi: $path\n";
    }
}

// 2. Komut Arayüzü (Command Interface)
interface Command {
    public function execute(): void;
    public function undo(): void;
}

// 3. Somut Komutlar (Concrete Commands)
class CreateFileCommand implements Command {
    private FileSystem $fileSystem;
    private string $path;

    public function __construct(FileSystem $fs, string $path) {
        $this->fileSystem = $fs;
        $this->path = $path;
    }

    public function execute(): void {
        $this->fileSystem->createFile($this->path);
    }

    public function undo(): void {
        // Oluşturma işleminin tersi silmektir
        $this->fileSystem->deleteFile($this->path);
    }
}

Command vs Strategy vs Chain of Responsibility

KriterCommandStrategyChain of Responsibility
Amaçİsteği nesneye dönüştürüp delege etmek, zamanlamak veya geri almak.Aynı işi yapmanın farklı algoritmalarını çalışma zamanında sunmak.İsteği bir alıcı zinciri boyunca iletip uygun alıcıyı aramak.
İlişkiGönderici ile alıcı arasında tek yönlü bağ kurar.Context'e dışarıdan enjekte edilen algoritmaları değiştirir.Alıcılar arasında zincirleme bir geçiş ilişkisi kurar.
Geri Al (Undo)Doğa gereği undo() desteği sunmaya çok uygundur.Genellikle geri alma desteği içermez.Geri alma özelliği genellikle kurgulanmaz.

Uygulanabilirlik

  • Nesneleri Parametrelendirme: İşlevler içeren nesneleri metotlara parametre olarak göndermek, saklamak veya çalışma zamanında değiştirmek istediğinizde kullanın.
  • Zamanlama ve Kuyruk (Queue) Sistemleri: İşlemleri sıraya almak, loglamak, geciktirerek çalıştırmak veya ağ üzerinden uzak makinelere göndermek istediğinizde kullanın.
  • Geri Alınabilir İşlemler (Undo/Redo): Kullanıcının yaptığı işlemleri geri alabilmesini sağlamak için command geçmişi (command history stack) tutarak kullanın.

Diğer Tasarım Desenleri ile İlişkisi

  • Chain of Responsibility işleyicileri (handlers) Command deseni ile uygulanabilir.
  • Command ve Memento deseni geri al (undo) işlemlerinde beraber kullanılır. Command işlemi yaparken Memento nesnenin durumunu yedekler.
  • Prototype, geçmiş kaydında (history) tutulacak komut nesnelerinin kopyalanmasına yardımcı olabilir.

Command Tasarım Deseni Kod Örnekleri

Örnek PHP Kodu

<?php

namespace RefactoringGuru\Command\Conceptual;

interface Command
{
    public function execute(): void;
}

class SimpleCommand implements Command
{
    private string $payload;

    public function __construct(string $payload)
    {
        $this->payload = $payload;
    }

    public function execute(): void
    {
        echo "SimpleCommand: Basit işleri yapıyorum, ekrana yazdırıyorum (" . $this->payload . ")\n";
    }
}

class ComplexCommand implements Command
{
    private Receiver $receiver;
    private string $a;
    private string $b;

    public function __construct(Receiver $receiver, string $a, string $b)
    {
        $this->receiver = $receiver;
        $this->a = $a;
        $this->b = $b;
    }

    public function execute(): void
    {
        echo "ComplexCommand: Karmaşık işler alıcı (receiver) nesnesine devrediliyor.\n";
        $this->receiver->doSomething($this->a);
        $this->receiver->doSomethingElse($this->b);
    }
}

class Receiver
{
    public function doSomething(string $a): void
    {
        echo "Receiver: Alıcı şunun üzerinde çalışıyor (" . $a . ".)\n";
    }

    public function doSomethingElse(string $b): void
    {
        echo "Receiver: Alıcı ayrıca şunun üzerinde çalışıyor (" . $b . ".)\n";
    }
}

class Invoker
{
    private ?Command $onStart = null;
    private ?Command $onFinish = null;

    public function setOnStart(Command $command): void
    {
        $this->onStart = $command;
    }

    public function setOnFinish(Command $command): void
    {
        $this->onFinish = $command;
    }

    public function doSomethingImportant(): void
    {
        echo "Invoker: İşleme başlamadan önce yapılacak bir şey var mı?\n";
        if ($this->onStart instanceof Command) {
            $this->onStart->execute();
        }

        echo "Invoker: ...çok önemli işler yapılıyor...\n";

        echo "Invoker: İşlem bittikten sonra yapılacak bir şey var mı?\n";
        if ($this->onFinish instanceof Command) {
            $this->onFinish->execute();
        }
    }
}

$invoker = new Invoker();
$invoker->setOnStart(new SimpleCommand("Merhaba!"));
$receiver = new Receiver();
$invoker->setOnFinish(new ComplexCommand($receiver, "E-posta Gönder", "Raporu Kaydet"));

$invoker->doSomethingImportant();

Örnek Python Kodu

from __future__ import annotations
from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass


class SimpleCommand(Command):
    def __init__(self, payload: str) -> None:
        self._payload = payload

    def execute(self) -> None:
        print(f"SimpleCommand: Basit yazdırma işlemi yapılıyor: ({self._payload})")


class ComplexCommand(Command):
    def __init__(self, receiver: Receiver, a: str, b: str) -> None:
        self._receiver = receiver
        self._a = a
        self._b = b

    def execute(self) -> None:
        print("ComplexCommand: Karmaşık iş mantığı alıcıya devrediliyor:")
        self._receiver.do_something(self._a)
        self._receiver.do_something_else(self._b)


class Receiver:
    def do_something(self, a: str) -> None:
        print(f"Receiver: Çalışıyor ({a}.)")

    def do_something_else(self, b: str) -> None:
        print(f"Receiver: Ayrıca çalışıyor ({b}.)")


class Invoker:
    def __init__(self) -> None:
        self._on_start = None
        self._on_finish = None

    def set_on_start(self, command: Command):
        self._on_start = command

    def set_on_finish(self, command: Command):
        self._on_finish = command

    def do_something_important(self) -> None:
        print("Invoker: Başlangıçta çalışacak komut var mı?")
        if isinstance(self._on_start, Command):
            self._on_start.execute()

        print("Invoker: ...önemli işlemler gerçekleştiriliyor...")

        print("Invoker: Bitişte çalışacak komut var mı?")
        if isinstance(self._on_finish, Command):
            self._on_finish.execute()


if __name__ == "__main__":
    invoker = Invoker()
    invoker.set_on_start(SimpleCommand("Merhaba!"))
    receiver = Receiver()
    invoker.set_on_finish(ComplexCommand(receiver, "E-posta gönder", "Rapor kaydet"))

    invoker.do_something_important()

Sıkça Sorulan Sorular (FAQ)

Geri Al (Undo/Redo) işlemlerinde RAM tüketimini azaltmak için ne yapılabilir?

Eğer komutların çalıştırıldığı andaki tüm nesne durumlarını (state) hafızaya kopyalarsanız (Memento yardımıyla), bu büyük projelerde ciddi RAM tüketimine yol açar. Alternatif olarak Ters İşlem (Reverse Operation) yaklaşımı uygulanabilir. Örneğin bir veritabanı kaydında Insert komutunun tersi Delete çalıştıran bir undo() metodudur. Bu sayede nesne yedeği almak yerine sadece ters kod çalıştırılarak RAM tüketimi önlenir.

Command deseni Job/Queue kuyruk mimarilerinde nasıl çalışır?

İstemci tarafından tetiklenen işlem doğrudan işlenmek yerine bir Command nesnesine dönüştürülür. Bu nesne JSON formatına serialize edilerek Redis, RabbitMQ veya SQL veritabanı gibi bir kuyruk sistemine atılır. Arka planda çalışan worker'lar (işleyiciler) bu komut verisini deserialize ederek alıcı (Receiver) sınıfa iletir ve execute() metodunu çalıştırır.

Command ile Memento hangi durumlarda birlikte kullanılmalıdır?

Eğer komut çalıştırıldıktan sonra sistemde meydana gelen değişikliklerin tersini yazmak çok karmaşıksa (örneğin karmaşık bir görsel filtreleme işlemi veya 3D modelleme), o zaman ters işlem yazmak imkansızlaşır. Bu gibi durumlarda, komut çalıştırılmadan hemen önce nesnenin durumu bir Memento nesnesiyle yedeklenir. Geri al (undo) çağrıldığında doğrudan bu Memento durumuna geri dönülür.


Diğer Tasarım Kalıpları/Design Patterns

Oluşumsal Kalıplar (Creational Patterns)

Factory Method, Abstract Factory, Builder, Prototype, Singleton

Yapısal Kalıplar (Structural Patterns)

Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Davranışsal Kalıplar (Behavioral Patterns)

Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor