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.
Command Tasarım Deseninin Amacı
Command ( Komut ) tasarım deseni, bir isteği kendisi ile ilgili tüm bilgileri içeren bağımsız bir nesneye dönüştüren davranışsal 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ınamaz işlemleri desteklemenize olanak verir.
Sorun
Bir metin-editörü uygulaması yazdığınızı düşünün. İlk işiniz çeşitli işlevler çalışmasını sağlayan düğmeler içeren bir araç çubuğu yapmak. Araç çubuğunda ve çeşitli dialog pencerelerinde kullanılabilecek çok güzel bir Button
sınıfı oluşturdunuz.
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.
Ç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. GUI için yazdığınız kodun, asıl işlev kodu ile çok fazla iç içe geçmesi de cabası.
Ve geldik en kötü tarafına. “Metni kopyala/yapıştır” gibi bazı fonksiyonların farklı yerlerden çağırılması gerekecek. Örneğin kullanıcı araç çubuğundaki “Kopyala” butonuna tıklayabilir, sağ tıklayarak açılan menüden kopyalayı seçebilir, ya da CTRL+C klavye kısa yolunu kullanabilir. “Kopyala” işlevi için yazdığınız kodu tüm bu yöntemler için kopyala/yapıştır mı yapacaksınız? 🙂
Çözüm
İyi bir yazılım tasarımı, genelde uygulamanın katmanlara ayrılması ile sonuçlanan işlevlerin ayırılması ilkesine ( seperation of concern principle ) dayanır. En alışıldık örnek: Grafik arayüz için bir katman, uygulama işlevleri mantığı için bir katman olmasıdır. Bu kodda genelde şöyle çalışır: Bir GUI nesnesi bir işlev nesnesinin bir metodunu çeşitli parametrelerle çalıştırır. Yani bir nesne diğer bir nesneye istek gönderir.
Command deseni GUI nesnelerinin bu isteği doğrudan göndermemesini tavsiye eder. Tersine, çağırılan nesne, metot adı ve gönderilen parametreleri bir command sınıfı içine aktarmanızı ve isteği bu nesne içindeki tek bir metotla tetiklemenizi tavsiye eder.
Command nesneleri çeşitli GUI ve işlev nesneleri arasında bağlantı sağlarlar. Bundan sonra GUI nesnesi isteği hangi işlev nesnesinin alacağını veya nasıl işleyeceğini bilmek zorunda değildir. GUI nesnesi sadece ilgili ‘command’ nesnesini tetikler ve gerisini o nesne halleder.
Sonraki adım tüm command’lerinizin aynı arayüzü (interface) paylaşmasıdır. Bu arayüz genelde parametre almayan tek bir çalıştırma metodundan oluşur. Bu arayüz aynı istek göndericisi ile çeşitli command’leri kullanabilmenizi sağlar. Bonus olarak gönderici ile ilişkili command nesnesini değiştirerek, çalışma anında göndericinin davranışını değiştirebilirsiniz.
Yapbozun bir parçasının eksik olduğunu fark etmiş olabilirsiniz. Eksik parça istek parametreleri. GUI nesnesi, işlev nesnesine bazı parametreler göndermiş olabilir. Command çalıştırma metodu parametre almadığına göre isteği alıcıya doğru bir şekilde nasıl aktaracak? Bunun için bu veri command’e önceden tanımlanmış olmalı veya command bu veriyi nasıl alacağını biliyor olmalıdır.
Metin editörü örneğimize geri dönelim. Command desenini uyguladıktan sonra farklı tıklama davranışları sergileyen alt düğme sınıfları entegre etmemize gerek kalmadı. Düğmeye sadece bir command nesnesi referans verip kendisine tıklandığında bu command nesnesinin çalıştırma metodunu tetiklemesini sağlayabiliriz.
Her işlev için farklı command sınıfları oluşturmanız ve bunları ilgili düğmelerle bağlamanız gerekecektir.
Sağ tıklama menüsü, kısa yollar veya diyalog pencerelerini de aynı şekilde tasarlayarak bu commandlere bağlayabilirsiniz. Böylece aynı işlevi gerçekleştirdiğiniz farklı elemanları tek bir command nesnesine bağlayarak işlevin içeriğini tek bir yerden düzenleyebilirsiniz.
Sonuç olarak command deseni GUI ve işlev katmanı arasındaki bağımlılığı azaltan kullanışlı bir ara katman görevi görecektir.
Uygulanabilirlik
İşlevler içeren nesneleri parametre olarak kullanmak istediğinizde Command desenini kullanabilirsiniz.
Command deseni spesifik bir metod çağrısını bağımsız bir nesneye dönüştürebilir. Bu değişiklik bir çok ilginç kullanımın kapısını açar. Command’leri metodlara parametre olarak gönderebilir, onları başka nesnelerin içerisinde saklayabilir ve bağlı command’leri çalışma esnasında değiştirebilirsiniz.
Örneğin bir düğmelerden oluşan bir GUI bileşeni oluşturduğunuzu ve kullanıcının bu düğmelerin işlevlerini değiştirebilmesini istediğinizi düşünün. Bu Command deseni için harika bir kullanım alanıdır.
İşlemleri sıralaya almak, zamanlamak veya uzaktan çalıştırabilmek için Command desenini kullanabilirsiniz.
Başka nesnelerde de olduğu gibi command nesnesi seriye dönüştürülebilir (serialize), bu da metine dönüştürülerek bir dosyaya ya da veri tabanına yazılmasına olanak verir. Böylece çalışmasına geciktirebilir veya zamanlayabilirsiniz. Bu yöntemle command’leri sıraya alabilir, loglayabilir veya ağ üzerinden komutlar gönderebilirsiniz.
Geri döndürülebilir işlemler oluşturmak istediğinizde command desenini kullanabilirsiniz.
Geri Al / Tekrar Uygula (Undo/Redo) için çeşitli yöntemler olsa da Command deseni bunlar arasında en popüler olanıdır.
İşlemleri geri almak için yapılan işlemlerin bir geçmişini tutmanız gerekir. Command geçmişi çalıştırılan tüm command nesneleri ve uygulamanın anlık durumunun yedeğini alan bir yoğındır.
Tabi bu metodun dezavantajları da var. İlk olarak, uygulamanın bazı bölümleri özel (private) olacağı için durumunu saklamak her zaman kolay olmaz. Bu sorun Memento deseni ile çözülebilir. İkinci sorun ise durum yedeklerinin çok fazla hafıza (RAM) işgal ediyor olmasıdır. Bu nedenle bazen alternatif bir çözüme gidebilir, önceki duruma dönmek yerine, mevcut işlemin tersini yapabilirsiniz. Tersine işlem yapmanın da bir bedeli var, o da uygulamanın zor, hatta bazen imkansız olması.
Diğer tasarım desenleri/kalıpları ile ilişkisi
- Chain of Responsiblity, Command, Mediator ve Observer alıcı ve göndericileri birbirine bağlamak için çeşitli yöntemler önerir
- Chain of Responsibility bir isteği potansiyel alıcılardan en az biri işleyene kadar dinamik bir potansiyel alıcı zinciri boyunca sırayla iletir.
- Command göndericiler ve alıcılar arasında tek yönlü bağlantılar kurar.
- Mediator göndericiler ve alıcılar arasındaki doğrudan bağlantıları ortadan kaldırarak onları bir aracı nesne aracılığıyla dolaylı olarak iletişim kurmaya zorlar.
- Observer alıcıların isteklere dinamik olarak abone olmalarını ve abonelikten çıkmalarını sağlar.
- Chain of Responsibility ‘deki işleyiciler (handler) Command olarak uygulanabilir. Bu durumda farklı bir çok işlem, istekle ilgili bilgiler içeren aynı bağlam (context) nesnesi üzerinde çalıştırılır.
Öte yandan isteğin kendisinin bir Command nesnesi olduğu bir yaklaşımda mevcuttur. Bu durumda aynı işlemi bir zincirdeki farklı bağlam ( context ) nesnelerinin tamamında çalıştırabilirsiniz. - Geri Al ( Undo ) işlevi için Command ve Memento‘yu birlikte uygulayabilirsiniz. Bu durumda Command’ler hedef nesneye bazı işlemler yapmaktan sorumlu iken, Memento’lar Command çalıştırılmadan önce nesnenin durumunu kaydetmek için kullanılır.
- Her ikisi de bir nesneyi parametre olarak kullanmayı sağladığı için Command ve Strategy benzer desenler olarak görülebilir. Fakat aslında çok farklı amaçları vardır;
- Command herhangi bir işlemin bir nesneye dönüştürülmesi için kullanılır. Bu işlemin parametreleri nesnenin alanları haline gelir. Bu dönüşüm işlemin çalışmasını ertelemenizi, sıraya almanızı, geçmişini saklamanızı ve uzak servislere gönderebilmenizi sağlar.
- Öte yandan Strategy aynı şeyi yapmak için farklı yöntemler sunar ve tek bir bağlam içerisinde algoritmaları değiştirebilmenize olanak tanır.
- Command herhangi bir işlemin bir nesneye dönüştürülmesi için kullanılır. Bu işlemin parametreleri nesnenin alanları haline gelir. Bu dönüşüm işlemin çalışmasını ertelemenizi, sıraya almanızı, geçmişini saklamanızı ve uzak servislere gönderebilmenizi sağlar.
- Prototype ,Command kopyalarını geçmiş kaydında tutmanıza yardımcı olur.
- Visitor‘ı Command‘in çok daha güçlü hali olarak görebilirsiniz. Visitor nesneleri, farklı sınıflardan nesneler üzerinde işlem çalıştırmaya olanak sağlar.
Command Tasarım Deseni Kod Örnekleri
Örnek PHP Kodu
<?php
namespace RefactoringGuru\Command\Conceptual;
/**
* The Command interface declares a method for executing a command.
*/
interface Command
{
public function execute(): void;
}
/**
* Some commands can implement simple operations on their own.
*/
class SimpleCommand implements Command
{
private $payload;
public function __construct(string $payload)
{
$this->payload = $payload;
}
public function execute(): void
{
echo "SimpleCommand: See, I can do simple things like printing (" . $this->payload . ")\n";
}
}
/**
* However, some commands can delegate more complex operations to other objects,
* called "receivers."
*/
class ComplexCommand implements Command
{
/**
* @var Receiver
*/
private $receiver;
/**
* Context data, required for launching the receiver's methods.
*/
private $a;
private $b;
/**
* Complex commands can accept one or several receiver objects along with
* any context data via the constructor.
*/
public function __construct(Receiver $receiver, string $a, string $b)
{
$this->receiver = $receiver;
$this->a = $a;
$this->b = $b;
}
/**
* Commands can delegate to any methods of a receiver.
*/
public function execute(): void
{
echo "ComplexCommand: Complex stuff should be done by a receiver object.\n";
$this->receiver->doSomething($this->a);
$this->receiver->doSomethingElse($this->b);
}
}
/**
* The Receiver classes contain some important business logic. They know how to
* perform all kinds of operations, associated with carrying out a request. In
* fact, any class may serve as a Receiver.
*/
class Receiver
{
public function doSomething(string $a): void
{
echo "Receiver: Working on (" . $a . ".)\n";
}
public function doSomethingElse(string $b): void
{
echo "Receiver: Also working on (" . $b . ".)\n";
}
}
/**
* The Invoker is associated with one or several commands. It sends a request to
* the command.
*/
class Invoker
{
/**
* @var Command
*/
private $onStart;
/**
* @var Command
*/
private $onFinish;
/**
* Initialize commands.
*/
public function setOnStart(Command $command): void
{
$this->onStart = $command;
}
public function setOnFinish(Command $command): void
{
$this->onFinish = $command;
}
/**
* The Invoker does not depend on concrete command or receiver classes. The
* Invoker passes a request to a receiver indirectly, by executing a
* command.
*/
public function doSomethingImportant(): void
{
echo "Invoker: Does anybody want something done before I begin?\n";
if ($this->onStart instanceof Command) {
$this->onStart->execute();
}
echo "Invoker: ...doing something really important...\n";
echo "Invoker: Does anybody want something done after I finish?\n";
if ($this->onFinish instanceof Command) {
$this->onFinish->execute();
}
}
}
/**
* The client code can parameterize an invoker with any commands.
*/
$invoker = new Invoker();
$invoker->setOnStart(new SimpleCommand("Say Hi!"));
$receiver = new Receiver();
$invoker->setOnFinish(new ComplexCommand($receiver, "Send email", "Save report"));
$invoker->doSomethingImportant();
Örnek Python Kodu
from __future__ import annotations
from abc import ABC, abstractmethod
class Command(ABC):
"""
The Command interface declares a method for executing a command.
"""
@abstractmethod
def execute(self) -> None:
pass
class SimpleCommand(Command):
"""
Some commands can implement simple operations on their own.
"""
def __init__(self, payload: str) -> None:
self._payload = payload
def execute(self) -> None:
print(f"SimpleCommand: See, I can do simple things like printing"
f"({self._payload})")
class ComplexCommand(Command):
"""
However, some commands can delegate more complex operations to other
objects, called "receivers."
"""
def __init__(self, receiver: Receiver, a: str, b: str) -> None:
"""
Complex commands can accept one or several receiver objects along with
any context data via the constructor.
"""
self._receiver = receiver
self._a = a
self._b = b
def execute(self) -> None:
"""
Commands can delegate to any methods of a receiver.
"""
print("ComplexCommand: Complex stuff should be done by a receiver object", end="")
self._receiver.do_something(self._a)
self._receiver.do_something_else(self._b)
class Receiver:
"""
The Receiver classes contain some important business logic. They know how to
perform all kinds of operations, associated with carrying out a request. In
fact, any class may serve as a Receiver.
"""
def do_something(self, a: str) -> None:
print(f"\nReceiver: Working on ({a}.)", end="")
def do_something_else(self, b: str) -> None:
print(f"\nReceiver: Also working on ({b}.)", end="")
class Invoker:
"""
The Invoker is associated with one or several commands. It sends a request
to the command.
"""
_on_start = None
_on_finish = None
"""
Initialize commands.
"""
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:
"""
The Invoker does not depend on concrete command or receiver classes. The
Invoker passes a request to a receiver indirectly, by executing a
command.
"""
print("Invoker: Does anybody want something done before I begin?")
if isinstance(self._on_start, Command):
self._on_start.execute()
print("Invoker: ...doing something really important...")
print("Invoker: Does anybody want something done after I finish?")
if isinstance(self._on_finish, Command):
self._on_finish.execute()
if __name__ == "__main__":
"""
The client code can parameterize an invoker with any commands.
"""
invoker = Invoker()
invoker.set_on_start(SimpleCommand("Say Hi!"))
receiver = Receiver()
invoker.set_on_finish(ComplexCommand(
receiver, "Send email", "Save report"))
invoker.do_something_important()