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.
Template Method Tasarım Deseninin Amacı
Template Method ( Şablon Yöntemi ), üst sınıfta algoritmanın bir iskeletini oluşturan, alt sınıfların yapıyı değiştirmeden bu algoritmanın belirli adımlarını değiştirmesine izin veren bir tasarım desenidir.
Sorun
Kurumsal belgeleri analiz eden bir veri madenciliği uygulaması oluşturduğunuzu hayal edin. Kullanıcılar, uygulama belgelerini çeşitli biçimlerde (PDF, DOC, CSV) besler ve bu belgelerden tek tip bir biçimde anlamlı veriler çıkarmaya çalışır.
Uygulamanın ilk sürümü yalnızca DOC dosyalarıyla çalışırken, bir sonraki sürümde, CSV dosyalarını desteklemeye karar verdiniz. Ve bir ay sonra uygulamanıza PDF dosyalarından veri çıkarmayı “öğrettiniz”.
Bir noktada, her üç sınıfın da birçok benzer kodu olduğunu fark ettiniz. Çeşitli veri formatlarıyla ilgili kod tüm sınıflarda farklıyken, veri işleme ve analiz kodu hemen hemen aynıydı. Algoritma yapısını bozmadan kod tekrarından kurtulmak harika olmaz mı?
Koleksiyonun yapısı ne olursa olsun, elemanlarının başka kodlar tarafından erişilir olması için yöntemler sağlamalıdır. Tekrar tekrar aynı elemanlarla karşılaşmadan bütün elemanların üzerinden geçmek için bir yöntem olmalıdır.
Bu sınıfları kullanan istemci koduyla ilgili başka bir sorun daha var. İşleme nesnesinin sınıfına bağlı olarak uygun bir eylem planı seçmek için birçok koşullu ifade içeriyor. Her üç işleme sınıfının ortak bir arayüz ya da temel bir sınıfı varsa, istemci kodundaki koşulları ortadan kaldırabilir ve işleme nesnesindeki yöntemleri çağırırken polimorfizm kullanabilirsiniz.
Çözüm
Template Method deseni bir algoritmayı bir dizi adıma ayırmanızı, bu adımları yöntemlere dönüştürmenizi ve bu yöntemlere olan çağırıları tek bir template method içerisine yerleştirmenizi ön görür. Adımlar soyut (abstract) olabilir veya varsayılan bir içeriğe sahip olabilirler İstemci algoritmayı kullanmak için kendi alt sınıfını belirtmeli, tüm soyut adımlara sahip olmalı ve gerekirse bazı isteğe bağlı olanları ezmelidir. ( Ama Template methodun kendisini değil, sadece belirli metotlarını )
Gelin bunun veri madenciliği uygulamamızda nasıl çalışacağını görelim. Üç ayrıştırma algoritmasının tümü için temel bir sınıf oluşturabiliriz. Bu sınıf, çeşitli belge işleme adımlarına yönelik bir dizi çağrıdan oluşan bir şablon yöntemi tanımlar.
İlk başta bütün adımları soyut (abstract) tanımlayıp alt sınıfları kendi uygulamalarını belirlemeye zorlayabiliriz. Örnek uygulamamızda alt sınıfların uygulamaları hazır, tek yapmamız gereken metodun çağırılığını üst sınıfla (superclass) uyumlu hale getirmek olacaktır.
Şimdi yinelenen koddan kurtulmak için neler yapabileceğimize bakalım. Dosyaları açma/kapatma ve verileri ayıklama/ayrıştırma kodu değişik veri formatları için farklı görünüyor, bu nedenle bu yöntemlere dokunmanın bir anlamı yok. Ancak, ham verileri analiz etmek ve raporları oluşturmak gibi diğer adımların uygulanması çok benzer, bu nedenle alt sınıfların bu kodu paylaşabileceği temel sınıfa çekilebilir.
Gördüğünüz üzere iki tip adımımız var:
- Soyut (abstract) adımlar her alt sınıf için uyarlanmalıdır.
- İsteğe bağlı adımların varsayılan bir uyarlaması vardır fakat gerekirse ezilebilir. (override)
Esasında kanca (hook) adı verilen bir adım daha var. Bir kanca içeriği boş olan isteğe bağlı bir adımdır. Template Method kanca ezilmese dahi çalışabilir. Kancalar genellikle önemli adımların öncesine ve sonrasına koyulur ve böylece alt sınıfların algoritmayı genişletmesi için ekstra noktalar oluşturulmuş olur.
Uygulanabilirlik
İstemcilerin tüm algoritmayı veya yapısını değil, yalnızca belirli adımlarını genişletmesine izin vermek istediğinizde Template Method tasarım desenini kullanabilirsiniz.
Template Method monolitik bir algoritmayı alt sınıflar tarafından kolaylıkla genişletilebilecek bağımsız adımlara dönüştürmenize olanak sağlar. Bunu yaparken üst sınıfının yapısının değişmemesini de garantilemiş olur.
Bazı küçük farklılıkları olsa bile neredeyse aynı algoritmayı içeren sınıflarınız olduğunda, algortima değiştiğinde sınıfların tümünü değiştirmeniz gerekebilir. Bu durumlarda Template Method desenini kullanabilirsiniz.
Böyle bir algoritmayı Template Method desenine dönüştürdüğünüzde, kod tekrarını ortadan kaldırarak benzer uygulamalara sahip adımları bir üst sınıfa çekebilirsiniz. Alt sınıflar arasında değişen kodlar alt sınıflarda kalabilir.
Diğer tasarım desenleri/kalıpları ile ilişkisi
- Factory Method Template Method’un özelleştirilmiş halidir. Aynı zamanda büyük Template Methodlar için bir adım görevi görür.
- Template Method deseni ‘inheritance’ temellidir, bir algoritmanın belirli bölümlerini alt sınıflara genişleterek değiştirmenizi sağlar. Strateji ise kompozisyon temellidir, nesnenin bazı davranışlarını ona o davranışlarla ilgili başka stratejiler vererek değiştirirsiniz. Template Method deseni sınıf seviyesinde çalışır, statiktir. Strategy deseni ise nesne seviyesinde çalışır ve nesnenin davranışlarını çalışma zamanında değiştirmenize olanak tanır.
Template Method Kod Örnekleri
Örnek PHP Kodu
<?php
namespace RefactoringGuru\TemplateMethod\Conceptual;
/**
* The Abstract Class defines a template method that contains a skeleton of some
* algorithm, composed of calls to (usually) abstract primitive operations.
*
* Concrete subclasses should implement these operations, but leave the template
* method itself intact.
*/
abstract class AbstractClass
{
/**
* The template method defines the skeleton of an algorithm.
*/
final public function templateMethod(): void
{
$this->baseOperation1();
$this->requiredOperations1();
$this->baseOperation2();
$this->hook1();
$this->requiredOperation2();
$this->baseOperation3();
$this->hook2();
}
/**
* These operations already have implementations.
*/
protected function baseOperation1(): void
{
echo "AbstractClass says: I am doing the bulk of the work\n";
}
protected function baseOperation2(): void
{
echo "AbstractClass says: But I let subclasses override some operations\n";
}
protected function baseOperation3(): void
{
echo "AbstractClass says: But I am doing the bulk of the work anyway\n";
}
/**
* These operations have to be implemented in subclasses.
*/
abstract protected function requiredOperations1(): void;
abstract protected function requiredOperation2(): void;
/**
* These are "hooks." Subclasses may override them, but it's not mandatory
* since the hooks already have default (but empty) implementation. Hooks
* provide additional extension points in some crucial places of the
* algorithm.
*/
protected function hook1(): void { }
protected function hook2(): void { }
}
/**
* Concrete classes have to implement all abstract operations of the base class.
* They can also override some operations with a default implementation.
*/
class ConcreteClass1 extends AbstractClass
{
protected function requiredOperations1(): void
{
echo "ConcreteClass1 says: Implemented Operation1\n";
}
protected function requiredOperation2(): void
{
echo "ConcreteClass1 says: Implemented Operation2\n";
}
}
/**
* Usually, concrete classes override only a fraction of base class' operations.
*/
class ConcreteClass2 extends AbstractClass
{
protected function requiredOperations1(): void
{
echo "ConcreteClass2 says: Implemented Operation1\n";
}
protected function requiredOperation2(): void
{
echo "ConcreteClass2 says: Implemented Operation2\n";
}
protected function hook1(): void
{
echo "ConcreteClass2 says: Overridden Hook1\n";
}
}
/**
* The client code calls the template method to execute the algorithm. Client
* code does not have to know the concrete class of an object it works with, as
* long as it works with objects through the interface of their base class.
*/
function clientCode(AbstractClass $class)
{
// ...
$class->templateMethod();
// ...
}
echo "Same client code can work with different subclasses:\n";
clientCode(new ConcreteClass1());
echo "\n";
echo "Same client code can work with different subclasses:\n";
clientCode(new ConcreteClass2());
Örnek Python Kodu
from abc import ABC, abstractmethod
class AbstractClass(ABC):
"""
The Abstract Class defines a template method that contains a skeleton of
some algorithm, composed of calls to (usually) abstract primitive
operations.
Concrete subclasses should implement these operations, but leave the
template method itself intact.
"""
def template_method(self) -> None:
"""
The template method defines the skeleton of an algorithm.
"""
self.base_operation1()
self.required_operations1()
self.base_operation2()
self.hook1()
self.required_operations2()
self.base_operation3()
self.hook2()
# These operations already have implementations.
def base_operation1(self) -> None:
print("AbstractClass says: I am doing the bulk of the work")
def base_operation2(self) -> None:
print("AbstractClass says: But I let subclasses override some operations")
def base_operation3(self) -> None:
print("AbstractClass says: But I am doing the bulk of the work anyway")
# These operations have to be implemented in subclasses.
@abstractmethod
def required_operations1(self) -> None:
pass
@abstractmethod
def required_operations2(self) -> None:
pass
# These are "hooks." Subclasses may override them, but it's not mandatory
# since the hooks already have default (but empty) implementation. Hooks
# provide additional extension points in some crucial places of the
# algorithm.
def hook1(self) -> None:
pass
def hook2(self) -> None:
pass
class ConcreteClass1(AbstractClass):
"""
Concrete classes have to implement all abstract operations of the base
class. They can also override some operations with a default implementation.
"""
def required_operations1(self) -> None:
print("ConcreteClass1 says: Implemented Operation1")
def required_operations2(self) -> None:
print("ConcreteClass1 says: Implemented Operation2")
class ConcreteClass2(AbstractClass):
"""
Usually, concrete classes override only a fraction of base class'
operations.
"""
def required_operations1(self) -> None:
print("ConcreteClass2 says: Implemented Operation1")
def required_operations2(self) -> None:
print("ConcreteClass2 says: Implemented Operation2")
def hook1(self) -> None:
print("ConcreteClass2 says: Overridden Hook1")
def client_code(abstract_class: AbstractClass) -> None:
"""
The client code calls the template method to execute the algorithm. Client
code does not have to know the concrete class of an object it works with, as
long as it works with objects through the interface of their base class.
"""
# ...
abstract_class.template_method()
# ...
if __name__ == "__main__":
print("Same client code can work with different subclasses:")
client_code(ConcreteClass1())
print("")
print("Same client code can work with different subclasses:")
client_code(ConcreteClass2())