“islem(…)” Metodunun Hikayesi

oğuzhan katlı
5 min readMay 12, 2020

--

Hiçbir kod ilk seferinde mükemmel olmaz; projeler de…

Kurumsal firmaların takımlar halinde geliştirdiği profesyonel projelerden, bir öğrencinin kendi kendine geliştirdiği projelere kadar bir ürünün geliştirme süreci kabaca aynı şekilde işlemektedir. Temel bir tasarım ile başlanan süreç, geliştirme, test etme ve sürekli iyileştirme olarak devam eder. Projenin ilerlemesi, gelişmesi yaptığımız tasarımların iyileştirilmesi ve yazdığımız kodların güncellenmesi ile sağlanmaktadır.

Başta çok basit olarak geliştirilen bir kod parçası, genişleyen kapsamlar ve yeni isterler doğrultusunda son haline geldiğinde, çok daha karmaşık ve profesyonel olabilir. Bu geliştirme süreci içerisinde ilgili kod parçasının ne kadar kolay anlaşılabilir ve değiştirilebilir olduğu, o işin kalitesini ve ömrünü belirlemektedir.

Bu yazıda, yukarıda bahsetmiş olduğum bilgiler doğrultusunda ilk başta çok basit ve masum gözüken “işlem” metodunun ilerleyen zamanlarında nasıl bir hale evrileceğini doğru ve yanlış yöntemler ile anlatıyor olacağım.

Gelelim “işlem” metodumuzun ilk haline; metodumuz a ve b iki tam sayı değer alarak basit bir matematik işlemi gerçekleştiren ve sonucunu dönerken yaptığı işlemi raporlayan basit bir metot olsun.

int işlem(int a, int b) 
{
auto sonuç = a + b * (a - b);
std::cout << "işlem sonucu: (int) " << sonuç << "\n";
return sonuç;
}

“Bir metodun en gelişmiş hali ne olabilir ki?” sorusunu soruyorsanız ve yazmış olduğumuz “işlem” metodunun ne kadar kompleks bir hale gelebileceğini düşünüyorsanız, umarım yazının devamında metodumuzun geleceği son hali sorunuza yeteri kadar cevap vermiş olacaktır.

Projemize devam edelim ve “işlem” metodumuzu başka tipteki değişkenler ile çağırmamız gereksin. Bunu yaparken de işlem sonucunu yazdığımız rapora hangi tipteki değişken ile çağrıldığını da belirtmemiz gereksin.
Bunu gerçekleştirmek için fonksiyon yüklemesi(function overloading) aklımıza ilk gelen seçenek olacaktır; istenilen tipler için bu yüklemeleri yapmamız bizim için yeterlidir.

int işlem(int a, int b) {
auto sonuç = a + b * (a - b);
std::cout << "işlem sonucu: (int) " << sonuç << "\n";
return sonuç;
}
float işlem(float a, float b) {
auto sonuç = a + b * (a - b);
std::cout << "işlem sonucu: (float) " << sonuç << "\n";
return sonuç;
}
double işlem(double a, double b) {
auto sonuç = a + b * (a - b);
std::cout << "işlem sonucu: (double) " << sonuç << "\n";
return sonuç;
}

Yukarıda int, float ve double tipleri için “işlem” metodumuzu yükleyerek ve işlem sonucunu düzgün bir şekilde raporlayarak bizden beklenen istekleri karşıladığımızı düşünelim. İlk başta yaptığımız geliştirme yeterli gibi gözükse de, bir çok tanımlanmamış durum ve olası hataya davet çıkaran bir yapıda bulunmaktadır.
Biraz düşündüğümüzde metodumuzun hangi durumlar için hatalı veya tanımsız olduğunu bulabiliriz:

  • Fonksiyon yüklemesi yapılmayan bir değişken tipi için hangi metodun çalışacağına nasıl karar vereceğiz?
  • Aynı şekilde “işlem” metodunu çağırırken verilen değişkenlerin tipleri birbirinden farklı ise, hangi tip ile işlem yapmalı ve sonucu hangi tipte döndürmeliyiz?
    örneğin: işlem(float, double) için dönüş değeri hangi tipte olmalıdır?
  • Farklı tipler için çağrılan işlem metodunu nasıl raporlayacağız?

Burada ilk sorduğumuz soruyu tekrarlamakta fayda var:
“Bir metodun en gelişmiş hali ne olabilir ki?”
Yazının başında belirttiğimiz gibi, projelerimiz gelişirken yaptığımız tasarımların sadeliğini koruması, yazdığımız kodların kolay anlaşılır ve düzenlenebilir olması, yaptığımız işin kalitesini ve ömrünü belirlemektedir.

İlk başta kolay gibi gözüken bu değişiklik için şimdi daha kapsamlı çözümler üretmemiz kaçınılmaz oldu; bunu yaparken yazdığımız kodların kolay geliştirilebilir olmasına gösterdiğimiz özeni korumamız gerektiğini de unutmamalıyız. Bize verilen yeni gereksinimleri karşılamak için yükleme yöntemi dışında başka bir yönteme sahip olmadığımız düşünülünce, yukarıda listelediğimiz problemlerin çözümü için bütün tipler ve tipler arasındaki kombinasyonlar arasında işlem metodumuzu yazmamız gerekecektir.

Sadece 4 farklı tip için bile bu işlemi yapmak istediğimizde elimizde 16 farklı “işlem” metodumuzun olması gerekecektir. Bu N tip için metot anlamına gelmektedir ki, geliştirdiğimiz yazılım dilinin bize sunduğu temel tipler dışında kendi tiplerimizin de olacağını varsayacak olursak, bu şekilde devam etmemiz imkansız olacaktır.

Template Tekniği

Aynı işi yapan, ancak farklı tipler için özelleştirilmesi gereken bir yapıyı el ile tek tek yapıyor olmak, hem zaman anlamında çok verimsiz, hem de yönetilebilirlik anlamında bizi çözümsüzlüğe yönlendirecektir.

Bunun yerine “işlem” metodumuzu jenerik(generic) olarak tanımlayabilir, kullanılan tipler için gereken özelleştirmeleri derleyicinin sorumluluğuna bırakabiliriz.

int işlem(int a, int b) 
{
auto sonuç = a + b * (a - b);
std::cout << "işlem sonucu: (int) " << sonuç << "\n";
return sonuç;
}

Yukarıda ilk hali ile duran “işlem” metodumuzu jenerik olarak tekrar yazalım ve metodumuzu farklı tipler ile kullanıyor olalım. Kullandığımız her tip için bizim yazdığımız jenerik “işlem” metodu derleyici tarafından gerçeklenerek kullanılacaktır ve o tipe özgün bir fonksiyon yüklemesi otomatik olarak yapılmış olacaktır.

https://godbolt.org/z/3uPMsL
https://godbolt.org/z/3uPMsL

Jenerik olarak yazdığımız “işlem” metodumuz ile farklı tipler için aynı işlemi yapan metodumuz hazır sayılır; sadece bizden istenen raporlama kısmını halletmemiz gerekiyor.

template <typename type1, typename type2>
auto işlem(type1 a, type2 b)
{
auto sonuç = a + b * (a - b);
std::cout << "işlem sonucu: (?) " << sonuç << "\n";
return sonuç;
}

Ancak, tipi belli olmayan bir jenerik fonksiyon içerisinde “bu tip için işlemin sonucu bu’dur” ifadesini nasıl raporlayacağız? Bunun için yine bir template tekniği olan Trait sınıflarını kullanarak, jenerik fonksiyonumuz içerisinde gerçekleştirmemiz gereken tipe özgün raporlama işlemini tamamlayabiliriz.

template <typename type>
struct sonuc_traits {
static const char *name;
};
// varsayılan isim olarak bir değer verebilir
// veya varsayılan bir tanım yaratmayarak derleme zamanında hata
// verilmesini sağlayabiliriz
template <typename T>
const char* sonuc_traits<T>::name = "bilinmiyor";
// beklediğimiz tiplere ait isimlerini template özelleştirmesini
// kendimiz yaparak belirtebiliriz
template <>
const char* sonuc_traits<int>::name = "int";
template <>
const char* sonuc_traits<double>::name = "double";

Yeni hali ile raporlama işlemi de doğru olarak çalışacaktır:

template <typename type1, typename type2>
auto işlem(type1 a, type2 b)
{
auto sonuç = a + b * (a - b);
cout << "işlem sonucu ("
<< sonuc_traits<decltype(sonuç)>::name << "): "
<< sonuç << "\n";
return sonuç;
}

“işlem” metodunun geldiği son halinin bizden istenen gereksinimleri başarıyla karşıladığını söyleyebiliriz. Kullandığımız jenerik metotlar ve template teknikleri sayesinde kodumuz, kolay geliştirilebilir ve düzenlenebilir olmaya devam etmektedir.

Yazının başında sormuş olduğumuz “Bir metodun en gelişmiş hali ne olabilir ki?” sorusunun cevabını vermeye çalışırken, proje içerisinde kullandığımız yazılım dilinin bütün enstrümanlarından faydalanmamız gerektiğini de fark etmiş olduk. Bir projede kullanılan yazılım dillerinin sahip olduğu bütün özellikleri kullanıyor olmak, o projenin ömrünü belirleyen kod kalitesinin en üst seviyede kalması için yapılması gereken en önemli gerekliliklerinden birisidir.

Bir proje, ne kadar basit başlarsa başlasın, yaşam döngüsü boyunca geliştirilmeye maruz kalacaktır. Projenin ömrünü ise, bu süreçte geliştirilen kodların kalitesi belirleyecektir.

Yukarıda örnek olarak gösterilen kodların derlenmesi için C++ 14 ve üzeri standardı gerekmektedir.
“işlem” metodunun C++98 standardında derlenmesi için decltype, auto ifadelerinin kaldırıldığı, dönüş tipinin derleyici zamanında belirtildiği versiyonunu aşağıda bulabilirsiniz.

--

--