Komuta Tasarım Deseni — (Command Design Pattern)

Buse Nur Şahin
6 min readDec 8, 2023

--

Photo by Kiki Siepel on Unsplash

Senaryo

Bir arayüz uygulamasında kullanıcının arayüz üzerinden yapabileceği işlemlerin yer aldığı bir palet oluşturulacak. Palet üzerinden yapılabilen her işlemin geri alma ve geri alınan işlemi tekrar ileri alma özelliklerinin olması isteniyor. Bu paleti genişletilebilir, esnek ve kullanıcıdan alınan komutların rahatça saklanıp yönetilebildiği bir yapı ile geliştirilmesi isteniyor. Nasıl yapardınız?

Komuta Tasarım Deseni

Bu senaryoyu gerçekleştirmek için uygulayacağımız Komuta Tasarım Desenini kısaca tanıyarak başlayabiliriz. Komuta tasarım deseni işlemleri(fonksiyonları) soyutlayarak ve sarmalayarak bir noktadan yönetilebilir olmasını sağlar.

Mesela televizyonun uzaktan kumandasını elimize aldığımızda ve bir tuşa bastığımızda bir komut veriyoruz. Kumandada verebileceğimiz tüm komutlar kayıtlı. Bu soyutlanmış yapıyı uyguladığımızda komuta tasarım desenini uyguluyoruz gibi düşünebiliriz.

Bu tasarım deseni için genelde verilen bir örnek vardır, bir ışığı açıp kapatmak için önümüzde bir anahtar var. Anahtara bastığımızda ışık açılıyor ve tekrar bastığımızda kapanıyor.

Bu yapıyı komuta tasarım deseni ile nasıl uygulayabilirdik?

Komuta: komut, anahtar üzerinden yapılabilecek işlemlerin soyut kavramı

public interface ICommand
{
void Execute();
}

Komuta alıcı: lamba, komutları uygulayacak olan lamba

public class Light
{
public void TurnOn()
{
Console.WriteLine("Işık açıldı");
}

public void TurnOff()
{
Console.WriteLine("Işık kapatıldı");
}
}

Somut komutlar: uygulanabilir komut, kullanıcının kumandasında yer alabilecek işlemler (anahtar açma ve kapama)

public class TurnOnCommand : ICommand
{
private Light _light { get; set; }
public TurnOnCommand(Light light)
{
_light = light;
}

public void Execute()
{
_light.TurnOn();
}
}

public class TurnOffCommand : ICommand
{
private Light _light { get; set; }
public TurnOffCommand(Light light)
{
_light = light;
}
public void Execute()
{
_light.TurnOff();
}
}

Çağırıcı: anahtar, işlemleri çağırdığımız arayüz

public class RemoteControl
{
private Dictionary<string, ICommand> _commands = new Dictionary<string, ICommand>();

public void Register(string key, ICommand command)
{
_commands.Add(key, command);
//_commands[key] = command;
}

public void ExecuteCommand(string key)
{
if(_commands.TryGetValue(key, out ICommand command))
{
command.Execute();
}
else
{
Console.WriteLine("geçersiz komut");
}
}

}

Lambayı kapatıp açmayı test edelim


Light light = new Light();

ICommand turnOnCommand = new TurnOnCommand(light);
ICommand turnOffCommand = new TurnOffCommand(light);

RemoteControl remoteControl = new RemoteControl();
remoteControl.Register("on", turnOnCommand);
remoteControl.Register("off", turnOffCommand);

// Lambayı açma işlemi
remoteControl.ExecuteCommand("on");
// Lambayı kapatma işlemi
remoteControl.ExecuteCommand("off");

Kod çıktısı:

Işık açıldı
Işık kapatıldı

Soru: Tamam da neden?

Light light = new Light();
light.TurnOn();
light.TurnOff();

Bu şekilde 3 satırda ve birçok yeni sınıf oluşturmadan yapabileceğimiz bir işi neden uzatarak yaptık? Aslında cevap olarak, “kompleks uygulamalarda daha temiz, yönetilebilir, takip edilebilir, kod yazmak için” diyebiliriz. Bu yüzden evet, ışık açıp kapama kodunda uygulamak hiç mantıklı değil ama belki yukarıdaki senaryoda kullanmak biraz daha anlamlı gelebilir, hadi senaryoyu uygulamaya başlayalım.

Kullanıcı İşlemlerinin Saklanması

Palet üzerinde metin ekleme, şekil çizme, arka plan silme, arka plan rengi değiştirme işlemlerinin olduğunu düşünelim. Komutların alıcısı olan üç veri türü var, metin ve çizim ve arka plan.

public class Text
{
public void AppendText()
{
Console.WriteLine("yeni metin eklendi");
}
}

public class Draw
{
public void DrawLine()
{
Console.WriteLine("çizgi çizildi");
}

public void DrawCircle()
{
Console.WriteLine("daire çizildi");
}
}

public class Background
{
public void ClearBackground()
{
Console.WriteLine("arka plan kaldırıldı");
}

public void ChangeBackgroundColor()
{
Console.WriteLine("arka plan rengi değiştirildi");
}
}

Bu sınıflar komuta tasarım deseninde hangi kavrama denk gelir? Komuta alıcı sınıflara (reciever).

Birbirinden farklı türdeki işlemleri bir yığında tutabilmem için bu işlemleri sarmalamam gerektiğini düşünüyorum ve tüm sarmalanan farklı türdeki işlemler eğer aynı arayüzden türetilmişse, nesne yönelimli programlamanın poliformizm prensibine uygun şekilde, tüm işlemleri aynı veri yapısında tutabilirim.

public interface ICommand
{
void Execute();
void Undo();
}

Her alıcıdaki her işlem için gerekli somut sınıfların yazılması gerekiyor.

İşlemlerin türediği ortak yapıda, ortak olan özellikleri saklayabilirim. Bu işlemler çalıştırma ve geri alma işlemleridir.

  • Bir işlem uygulanması için butona basıldığında “çalıştır” komutu gitmiş olur ki bu “komut” sınıfında yer alıyor (ICommand/Execute).
  • İşlem geri alındığında “geri al” komutunu çalıştırırım ki bu da “komut” sınıfında yer alıyor (ICommand/Undo).
  • Geri alınan işlemi tekrar ileri almam için de tekrar “çalıştır” komutunu çağırmam yeterli.
public class AppendText : ICommand
{
private Text _text { get; set; }
public AppendText(Text text) => _text = text;

public void Execute() => _text.AppendText();

public void Undo() => _text.UndoText();
}

public class DrawLine : ICommand
{
private Draw _draw { get; set; }

public DrawLine(Draw draw) => _draw = draw;

public void Execute() => _draw.DrawLine();

public void Undo() => _draw.UndoDraw();
}

public class DrawCircle : ICommand
{
private Draw _draw { get; set; }

public DrawCircle(Draw draw) => _draw = draw;
public void Execute() => _draw.DrawCircle();

public void Undo() => _draw.UndoDraw();
}

public class ChangeBackgroundColor : ICommand
{
private Background _background { get; set; }

public ChangeBackgroundColor(Background background) => _background = background;

public void Execute() => _background.ChangeBackgroundColor();

public void Undo() => _background.UndoBackground();
}

public class CleareBackground : ICommand
{
private Background _background { get; set; }

public CleareBackground(Background background) => _background = background;

public void Execute() => _background.ClearBackground();

public void Undo() => _background.UndoBackground();
}

Artık alıcılarımız ve işlemlerimiz hazır. Paletin bu işlemlerden tamamen soyutlanmış olması için bir kontrol yapısı adeta bir kumanda oluşturacağız.

public class RemoteControl
{

}

Kullanıcı palet üzerindeki işlemlere tıkladığında yapılan son işlemleri ileri-geri alabilmek için bir yerde saklamamız gerekiyor. Son yapılan işlemin geri alınabilmesi için işlemlerin saklanabileceği en uygun yapı son giren-ilk çıkar (lifo) prensibine uygun veri yapısı olan stack (yığın-yığıt) gibi duruyor.

Uygulanan komutlar ve ileri alma işlemi için geri alınan komutlar olmak üzere iki adet yığın oluşturmalıyız.

public class RemoteControl
{
Stack<ICommand> commands = new Stack<ICommand>();
Stack<ICommand> undoCommands = new Stack<ICommand>();
}

Bir işlemi uyguladığımızda bu komutayı çağırmalı ve komutlar yığınına eklemeliyiz.

public class RemoteControl
{
Stack<ICommand> commands = new Stack<ICommand>();
Stack<ICommand> undoCommands = new Stack<ICommand>();

public void Execute(ICommand command)
{
command.Execute();
commands.Push(command);
}
}

Uygulanan son komut geri alındığında komutlar yığınındaki son elemanı alıp işletmeli ve geri alınan komutlar yığınına eklemeliyiz.

public class RemoteControl
{
Stack<ICommand> commands = new Stack<ICommand>();
Stack<ICommand> undoCommands = new Stack<ICommand>();

public void Execute(ICommand command)
{
command.Execute();
commands.Push(command);
}

public void UndoLastCommand()
{
if (commands.Count > 0)
{
ICommand command = commands.Pop();
command.Undo();
undoCommands.Push(command);
}
else
{
Console.WriteLine("Geri alınacak bir işlem yok.");
}
}
}

Geri alınan son komut için ise geri alınan komutlar yığınındaki son elemanı çıkarmalı, çalıştırmalı ve çalıştırılan komutlara eklemeliyiz.

public class RemoteControl
{
Stack<ICommand> commands = new Stack<ICommand>();
Stack<ICommand> undoCommands = new Stack<ICommand>();

public void Execute(ICommand command)
{
command.Execute();
commands.Push(command);
}

public void UndoLastCommand()
{
if (commands.Count > 0)
{
ICommand command = commands.Pop();
command.Undo();
undoCommands.Push(command);
}
else
{
Console.WriteLine("Geri alınacak bir işlem yok.");
}
}

public void RedoLastCommand()
{
if (undoCommands.Count > 0)
{
ICommand command = undoCommands.Pop();
command.Execute();
commands.Push(command);
}
else
{
Console.WriteLine("İleri alınacak bir işlem yok.");
}
}
}

Test

Command Design Pattern ile uyguladığımız senaryonun testini yapalım.

Uygulamayı açtık ve paleti kullanarak

  • Arka planı kaldırdık
  • Bir metin ekledik
  • Arka planı değiştirdik
  • Arka planı beğenmeyerek geri aldık
  • Metinden de vazgeçerek geri aldık
  • Yalnızca bir daire ekleyerek uygulamamızı sonlandırdık.

Bu kullanıcı deneyimini uygulayalım


var text = new Text();
var draw = new Draw();
var background = new Background();

var remoteControl = new RemoteControl();
remoteControl.Execute(new CleareBackground(background));
remoteControl.Execute(new AppendText(text));
remoteControl.Execute(new ChangeBackgroundColor(background));
remoteControl.UndoLastCommand();
remoteControl.UndoLastCommand();
remoteControl.Execute(new DrawCircle(draw));

Kod çıktısı:

arka plan kaldırıldı
yeni metin eklendi
arka plan rengi değiştirildi
arka planda yapılan son değişiklik geri alındı
son eklenen yazı geri alındı
daire çizildi

Uygulama tamamlandığında kumanda hafızasında kalan son komutlara baktığımızda yalnızca arka plan temizleme ve daire çizme kaldığını görmekteyiz.

Command Design Pattern, metotları sınıf halinde yazarak yönetimi, takibi, loglamayı, listelemeyi kolaylaştırmakta ve soyutlayarak metodu uygulayacak kişinin metot hakkında bilgi sahibi olması gerekliliğini ortadan kaldırmaktadır. Böylece kolaylıkla tek bir ICommand üzerinden farklı nesnelerin farklı metotlarını tek noktadan yönetebildik ve bir yığın içerisinde saklayarak geri alma-ileri alma işlemlerini gerçekleştirebildik.

Okuduğunuz için teşekkür ederim

--

--

Responses (1)