Refactoring’i Detaylı Bir Örnek İle İnceleme Part 1

Selamlar,

Bu makale serisinde Refactoring nedir? Uygularsak ne kazanırız ? Kodu olduğu gibi bırakırsak başımıza neler gelebilir? gibi sorulara güncel hayattan örnekler ile cevap arayacağız. Martin Fowler’ın Refactoring (2019) kitabındaki, Javascript örneğinden yola çıkılarak aşağıdaki örnek kodlanmıştır.

Proje Oluşturma:

Aşağıdaki komut yazılarak .Net Core Console Application oluşturulur.

Aşağıda görüldüğü gibi, dışardan data alınan 2 json dosya vardır.

1-) course.json :

2-) invoices.json :

Proje içinde yeni bir Model klasörü açılır. Projede kullanılacak modeller, yukarıdaki json datalara bağlı olarak, aşağıdaki gibi oluşturulur.

Course.cs:

Invoice.cs:

Register.cs:

Types.cs:

Program.cs: İlk etapta gelen dataya göre modeller, manuel olarak aşağıdaki gibi doldurulur.

Şimdi gelin bussines’ı işletip, fiyat hesaplayan kodu yazalım.

Part 1: Topam bütçe ve kazanılan para puan değişkenleri tanımlanır. Parasal değerleri formatlama amaçlı, CultureInfo tanımlanır.

Part 2: Alınılan kurslar teker teker gezilir. Kurs tipine ve öğrenci sayılarına göre ödenecek ücret (thisAmount) hesaplanır.

Part 3: Her bir gezilen kursa ve eğitim alınılan öğrenci sayısına göre, kazanılan bonus para hesaplanır. Her bir dersin toplam maliyeti, mesaj olarak ekrana basılır ve genel toplama eklenir.

Part 4: Toplam borç ve toplam kazanç ekrana yazdırılır.

Tüm Kod: Yapılan tüm işlemler, aşağıda toplu bir hale getirilmiştir.  Güncel hayatda da bolca karşılaşabileceğimiz ve genelde çoğumuza normal gelen bu kodları gelin adım adım, çok daha rahat okunabilen, kolaylıkla değiştirilebilen, anlaşılır ve sade bir hale getirelim. Kısaca Refactor edelim.

Result:

Extract Function : Refactoring işlemine geçmeden önce, testlerin hazır olması gerekmektedir. Çünkü yapılan her bir yenileme ya da düzenlemenin, öncelikle mutlaka test edilmesi gerekmektedir. Test, Refactoring işleminin olmazsa olmazıdır.

Gelin ilk etapta, Switch ile hesaplanan fiyat kısmını ayrı bir method olarak dışarıya(Extract Function) alalım:

getAmounth():

İlgili kodlarda, aşağıda görüldüğü gibi Kırmızı kısım çıkarılır ve Yeşil kısım eklenir :

foreach (Registerregininvoice.registers)
{
        Course lesson=courses[reg.courseID];
        //var thisAmount = 0;
        var thisAmount=getAmounth(reg,lesson);
    /* switch (lesson.Type)
        {
              case Types.Art:
              {
                    thisAmount = 3000;
                    if (reg.student > 15)
                    {
                          thisAmount += 1000 * (reg.student – 10);
                    }
                    break;
              }
              case Types.Software:
              {
                  thisAmount = 30000;
                  if (reg.student > 10)
                  {
                       thisAmount += 10000 + 500 * (reg.student – 5);
                  }
                  thisAmount += 300 * reg.student;
                  break;
              }
        } */
//kazanılan para puan

Refactoring işleminde bu yönteme Extract Function denilmektedir. İlgili testler çalıştırıldığında, yine aynı sonuçlar alınmaktadır.

Şimdi gelin isterseniz getAmounth() function’ını biraz daha değiştirelim. Her function geriye bir result dönmelidir. Kısaca, bir functionda geri dönen değişkenin adı result olur ise, anlaşılması çok daha kolay olacaktır. Ayrıca anlaşılması zor olan “reg” değişkeni “register” olarak değiştirilmiştir.

public static int getAmounth(Register register, Course lesson)
{
        /*var thisAmount = 0;*/
        var result=0;
        switch (lesson.Type)
        {
            caseTypes.Art:
            {
                    result=3000;
                    if (register.student>15)
                    {
                             result+=1000* (register.student-10);
                    }
                    break;
            }
            case Types.Software:
            {
                    result=30000;
                    if (register.student>10)
                    {
                             result+=10000+500* (register.student-5);
                    }
                    result+=300*register.student;
                    break;
           }
        }
        return result;
}
Şimdi sırada ilgili kursun, findCourse() bir function ile bulunmasına :

Var olan kodun, findCourse() function’ı ile değiştirilmesi:

foreach (Register reg in invoice.registers){
                //Course lesson = courses[reg.courseID];
                Course lesson=findCourse(reg);
                var thisAmount=getAmounth(reg, lesson);
                //kazanılan para puan
                volumeCredits+=Math.Max(reg.student-15, 0);
                //extra bonus para puan her 5 yazılım öğrencisi için
                decimal fiveStudentGroup=reg.student/5;
                if (Types.Software==lesson.Type) volumeCredits+=Math.Floor(fiveStudentGroup);
                // her bir şiparişin fiyatı
                result+=$”{lesson.Name}: {(thisAmount/100).ToString(“C”, trFormat)} ({reg.student} kişi)\n”;
                totalAmount+=thisAmount;
}
Şimdi gelin “lesson” değişkenini kaldıralım :
foreach (Register reg in invoice.registers)
{
        //Course lesson = courses[reg.courseID];
        var thisAmount = getAmounth(reg, findCourse(reg));
        //kazanılan para puan
        volumeCredits+=Math.Max(reg.student-15, 0);
        //extra bonus para puan her 5 yazılım öğrencisi için
        decimal fiveStudentGroup=reg.student/5;
        if (Types.Software==findCourse(reg).Type) volumeCredits+=Math.Floor(fiveStudentGroup);
        //her bir şiparişin fiyatı
        result+=$”{findCourse(reg).Name}: {(thisAmount/100).ToString(“C”, trFormat)} ({reg.student} kişi)\n”;
        totalAmount+=thisAmount;
}
Şimdi gelin “lesson” değişkenini getAmounth() function’ı için de kaldıralım :
public static int getAmounth(Register register, /*Course lesson*/)
{
        var result=0;
        switch (findCourse(register).Type)
        {
              case Types.Art:
              {
                    result=3000;
                    if (register.student>15)
                    {
                           result+=1000* (register.student-10);
                    }
                    break;
              }
              case Types.Software:
              {
                   result=30000;
                   if (register.student>10)
                   {
                         result+=10000+500* (register.student-5);
                   }
                   result+=300*register.student;
                   break;
              }
        }
        return result;
}
Yukarıdaki değişikliğe bağlı olarak “getAmounth()” function’ın kullanıldığı yerlerde, lesson parametresi aşağıdaki gibi kaldırılır.
var thisAmount = getAmounth(reg, findCourse(reg));
Şimdi tekrardan derleyip testleri çalıştıralım. Aynı sonuçlar alınıyor ise, her şey yolunda demektir.
Son değişikliğe bakıldığı zaman, “foreach” loop’u içinde method çağrılmış olsa da, büyük projelerde local değişkenlerin kaldırılması Extraction yöntemine göre çok daha kolaydır.
Aşağıda, “getAmounth()” methodunun local variabledan arındırılmış hali ile kullanılışı gösterilmiştir.
foreach (Register reg in invoice.registers){
                Course lesson=findCourse(reg);
                var thisAmount=getAmounth(reg, lesson);
                //kazanılan para puan
                volumeCredits+=Math.Max(reg.student-15, 0);
                //extra bonus para puan her 5 yazılım öğrencisi için
                decimal fiveStudentGroup=reg.student/5;
                if (Types.Software==lesson.Type) volumeCredits+=Math.Floor(fiveStudentGroup);
                // her bir şiparişin fiyatı
                result+=$”{lesson.Name}: {(thisAmount/100).ToString(“C”, trFormat)} ({reg.student} kişi)\n”;
                totalAmount+=thisAmount;
}
Extract Volume Credits: Şimdi sıra geldi, kazanılan bonus paranın dışarıda bir function ile (Extract Function) hesaplanmasına:
calculateVolumeCredit():

Genel kazanç hesaplamada kullanılan kodlar, aşağıdaki gibi çıkarılıp direk aynı işi yapan “calculateVolumeCredit()” methodu çağrılmıştır.

foreach (Registerregininvoice.registers)
{
           var thisAmount=getAmounth(reg);
           //kazanılan para puan
           //volumeCredits += Math.Max(reg.student – 15, 0);
           //extra bonus para puan her 5 yazılım öğrencisi için
           //decimal fiveStudentGroup = reg.student / 5;
           //if (Types.Software == findCourse(reg).Type) volumeCredits += Math.Floor(fiveStudentGroup);
           volumeCredits+=calculateVolumeCredit(reg);
           // her bir şiparişin fiyatı
           result+=$”{findCourse(reg).Name}: {(thisAmount/100).ToString(“C”, trFormat)} ({reg.student} kişi)\n”;
           totalAmount+=thisAmount;
}
Extract trFormat: Son olarak “trFormat” değişkeninin kaldırılması : Sayısal değerlerin Formatlanma işlemi için, aşağıdaki method kullanılır.

format() methodunun kullanılış şekli, aşağıdaki gibidir: İlgili trFormat değişkeni yerine, format() methodu, biçimlendireceği sayısal değer ile birlikte çağrılmıştır.

foreach (Registerregininvoice.registers)
{
        var thisAmount=getAmounth(reg);
        //kazanılan para puan
        volumeCredits+=calculateVolumeCredit(reg);
        // her bir şiparişin fiyatı
        //result += $”{findCourse(reg).Name}: {(thisAmount / 100).ToString(“C”, trFormat)} ({reg.student} kişi)\n”;
        result+=$”{findCourse(reg).Name}: {format(thisAmount/100)} ({reg.student} kişi)\n”;
        totalAmount+=thisAmount;
}
/* result += $”Toplam borç { (totalAmount / 100).ToString(“C”, trFormat)}\n”;
result += $”Kazancınız { volumeCredits.ToString(“C”, trFormat) } \n”; */
result+=$”Toplam borç { format(totalAmount/100) }\n”;
result+=$”Kazancınız { format(volumeCredits) } \n”;
Tüm bu işlemlerden sonra format() methodunun ismi, çok da anlaşılır durmuyor. Farklı dillere göre format türleri de olabileceği için, bunun adının tr() olarak değiştirilmesi çok daha sağlıklıdır. Son hali aşağıdaki gibidir.
/*CultureInfo trFormat = new CultureInfo(“tr-TR”, false);
trFormat.NumberFormat.CurrencySymbol = “TL”;
trFormat.NumberFormat.NumberDecimalDigits = 2;*/

Kullanılış şekli de aşağıdaki gibi değiştirilmiştir:
foreach (Registerregininvoice.registers)
{
var thisAmount=getAmounth(reg);
//kazanılan para puan
volumeCredits+=calculateVolumeCredit(reg);
// her bir şiparişin fiyatı
result+=$”{findCourse(reg).Name}: {tr(thisAmount/100)} ({reg.student} kişi)\n”;
totalAmount+=thisAmount;
}
result+=$”Toplam borç { tr(totalAmount/100) }\n”;
result+=$”Kazancınız { tr(volumeCredits) } \n”;

Bu kadar işlemden sonra test yapmak şart. Test sonucu aşağıdaki gibi ise, herşey yolunda demektir.
Sırada kaldırmak istenen değişkenler volumeCredits ve thisAmount var: Bunun için aşağıda, Split Loop ile iki farklı döngü oluşturulmuştur:

decimal totalAmount = 0;
decimal volumeCredits = 0;
var result = $”{invoice.customerName} için Fatura Detayı: \n”;

foreach (Register reg in invoice.registers)
{
        var thisAmount = getAmounth(reg);
        result += $”{findCourse(reg).Name}: {tr(getAmounth(reg) / 100)} ({reg.student} kişi)\n”;
        totalAmount += getAmounth(reg);
}

foreach (Register reg in invoice.registers)
{
         volumeCredits += calculateVolumeCredit(reg);
}
result += $”Toplam borç { tr(totalAmount / 100) }\n”;
result += $”Kazancınız { tr(volumeCredits) } \n”;

Bu durumda Slide Statements kullanılarak, valumeCredits değişkeni foreach loop’ın yanına taşınır.

decimal totalAmount = 0;
/*decimal volumeCredits = 0;*/
var result = $”{invoice.customerName} için Fatura Detayı: \n”;

foreach (Register reg in invoice.registers)
{
        var thisAmount = getAmounth(reg);
        result += $”{findCourse(reg).Name}: {tr(getAmounth(reg) / 100)} ({reg.student} kişi)\n”;
        totalAmount += getAmounth(reg);
}

decimal volumeCredits = 0;

foreach (Register reg in invoice.registers)
{
         volumeCredits += calculateVolumeCredit(reg);
}
result += $”Toplam borç { tr(totalAmount / 100) }\n”;
result += $”Kazancınız { tr(volumeCredits) } \n”;

Daha sonra volumeCredits‘in hesaplandığı bu kısım, Extract Function (totalValumeCredits) olarak dışarı alınır :

Kullanım Şekli:

decimal totalAmount = 0;
var result = $”{invoice.customerName} için Fatura Detayı: \n”;

foreach (Register reg in invoice.registers)
{
        var thisAmount = getAmounth(reg);
        result += $”{findCourse(reg).Name}: {tr(getAmounth(reg) / 100)} ({reg.student} kişi)\n”;
        totalAmount += getAmounth(reg);
}

/*decimal volumeCredits = 0;

foreach (Register reg in invoice.registers)
{
         volumeCredits += calculateVolumeCredit(reg);
}*/

decimal volumeCredits=totalValumeCredits();

result += $”Toplam borç { tr(totalAmount / 100) }\n”;
result += $”Kazancınız { tr(volumeCredits) } \n”;

Son olarak volumeCredits değişkeni de extract edilir. Yani çıkarılır:

decimal totalAmount = 0;
var result = $”{invoice.customerName} için Fatura Detayı: \n”;

foreach (Register reg in invoice.registers)
{
        var thisAmount = getAmounth(reg);
        result += $”{findCourse(reg).Name}: {tr(getAmounth(reg) / 100)} ({reg.student} kişi)\n”;
        totalAmount += getAmounth(reg);
}

result += $”Toplam borç { tr(totalAmount / 100) }\n”;
result += $”Kazancınız { tr(totalValumeCredits();) } \n”;

İsterseniz buraya kadar olan kısmı bir konuşalım. Döngünün 2 kere tekrarlanması performans konusundan dolayı endişe verse de, bazen refactoring işlemi sırasında bu göz ardı edilebilir. Bazen refactoring işlemi sırasında, performansda büyük sorunlar yaşanabilir. O zaman önce refactoring işlemini tamamlayın. Sonra performans’a tekrardan bakın. Yukarıda refactoring yöntemlerinden en basit olan 4 tanesi incelenmiştir. Ama gerçek hayat bundan çok daha karmaşık olabilir.
  • Split Loop
  • Slide Statements
  • Extract Function
  • Inline Variable

Refactoring için bazen bu kadar kısa adımlar yeterli olmayabilir. Testlerde hatalar da çıkabilir. Bu durumda, en son yapılan değişiklik geri alınır ve daha küçük parçalara ayırıp, adım adım yapılan her değişiklik test edilerek tekrarlanır.

Şimdi sıra geldi, totalAmount değişkenini kaldırmaya: Bu işlem için önce totalAmout‘ı hesaplayan bir Extract Function  yazalım. Sonra değişken yerine bu methodu çağralım. Aşağıda görüldüğü gibi getTotalAmount() methodu tanımlanır.

Aşağıda görüldüğü gibi totalAmount değişkeni, önce Slide Statement ile yeri değiştirilip loop üstünde tanımlanmış, sonra extract edilerek koddan çıkarılmıştır. getTotalAmount() ile extract function yazılmış ayrıca Amount ve Volume için Split Loop yapılarak, üzerlerinde rahatlıkla değişiklik yapılabilmesine imkan sağlanmıştır.

var result = $”{invoice.customerName} için Fatura Detayı: \n”;

//decimal totalAmount = 0;
foreach (Register reg in invoice.registers)
{
        result += $”{findCourse(reg).Name}: {tr(getAmounth(reg) / 100)} ({reg.student} kişi)\n”;
        //totalAmount += getAmounth(reg);
}

decimal volumeCredits = totalValumeCredits();

/*result += $”Toplam borç { tr(totalAmount / 100) }\n”;
result += $”Kazancınız { tr(volumeCredits) } \n”;*/

result += $”Toplam borç { tr(getTotalAmount() / 100) }\n”;
result += $”Kazancınız { tr(totalValumeCredits()) } \n”;

Console.WriteLine(result);
Console.ReadLine();

Hatta istenir ise “getTotalAmount()” ve “totalValumeCredits()” methodları için geri döndürülen değişken ismleri, daha rahat anlaşılması adına “result” olarak aşağıdaki gibi değiştirilebilir.

public static decimal totalValumeCredits()
{
        //decimal volumeCredits = 0;
        decimal result=0;
        foreach (Register reg in invoice.registers)
        {
                //volumeCredits += calculateVolumeCredit(reg);
                result+=calculateVolumeCredit(reg);
        }
        //return volumeCredits;
        return result;
}
public static decimal getTotalAmount()
{
        // decimal totalAmount = 0;
        decimal result=0;
        foreach (Register reg in invoice.registers)
        {
                //totalAmount += getAmounth(reg);
                result+=getAmounth(reg);
         }
        //return totalAmount;
        return result;
}
Tekrar test edelim. Eğer sonuç ekranı aşağıdaki gibi ise, bunca değişikliğe rağmen her şey yolunda demektir.
Geldik bu makale serisinin, ilk bölümünün sonuna. Bir sonraki bölümde, refactor işlemlerine kaldığı yerden devam edeceğiz. Haftaya görüşmek üzere hepinize hoşçakalın.
Reference By (Source) : Martin Fowler Refactoring (2019) Second Edition

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

9 Cevaplar

  1. Ali dedi ki:

    Yine çok güzel bir makale olmuş hocam ama kafama bir kaç soru takıldı. Cevaplarsanız çok sevinirim.
    Extract function’ların static olmasının özel bir nedeni var mı?

    Ayrıca public olarak dışarıya açmak sorun yaratabilir mi? Dışarıdan farklı amaçlarla kullanılması vs.

    Methodların geriye dönen değerin adı result oluşu kod okunabilirliği açısından kötü değil mi result’ün içinde ne var! adını mı dönüyor toplam değeri mi ders sayısı mı geriye döndüğü değerin içinde ney var anlaşılması zor olmuyor mu?

    Örneğin getAmounth Extract function da sadece type ve student kullanıyor. Sadece kullandığı değerleri parametreden vermek daha doğru bir yaklaşım değil mi? Tüm modeli alması gereksiz gibi…

    • borsoft dedi ki:

      Selam Ali,
      Yaavv ne güzel sorular sormuşun :)
      1-)Extract Function’ın Static olmasının tek nedeni Console Application’da Main() Methodunun Static olmasıdır:)
      2-)Başka yerlerde de aynı method kullanılacak ise Public açılabilir. Ama bu örnekteki açılmasa daha iyi.
      3-)Method’un geriye dönüş değeri neden result. Çünkü hiç bilmiyen biri onun geriye dönen değer olduğunu rahat anlamalı. Ne işe yaradığını ve ne dönebileceğini methodun adından anlamalı demiş şair burada. function result dönmeli genel bir kabuldür. Direk benim düşüncem değildir. Bilginize.
      4-)getAmounth() functon’ı için refactor’e devam edeceğiz :) Güzel yakalamışın.

      Makalenin devamında görüşmek üzere.
      Hoşçakal.

  2. Serdar dedi ki:

    Bora Hocam merhaba,

    Hem twitterdan hem kişisel web sitenizden hem de youtubedan çalışmalarınızı ilgiyle takip ediyorum. Bir önceki soruda sorulanlardan benim de aklıma takılanlar olmuştu cevaplanması hoş olmuş :) Ancak makalede göremediğim kodu yazarken fark ettiğim bir durum oldu. Dictionary tipindeki “courses” değişkeni ve Invoice tipindeki “Invoice” değişkeni Extract olarak tanımladığımız fonksiyonların hemen hemen hepsinde kullanılıyor ve bu değişkenler Main metodu içerisinde tanımlanmış. Bunların sınıf içerisinde global olarak tanımlanması gerekmez miydi eğer fonksiyonlara parametre olarak geçilmeyeceklerse (ki yazıda parametre olarak kullanılmamışlar) ?

    • borsoft dedi ki:

      Selamlar Serdar,
      Öncelikle takibin için teşekkür ederim :)

      Makalenin diğer bölümlerini de okuduktan sonra, halen bu soru kafana takılırsa konuşalım.
      Bir de makalede göremediğin kod varsa GitHub’a bakabilirsin.

      Görüşmek üzere.
      Hoşçakal..

      • Serdar dedi ki:

        Github üzerinden incelediğimde farkettim okurken kaçırmışım sanırım :) Sorunun cevabını aldım. Teşekkürler.

  3. Miraç dedi ki:

    Gene bir müthiş yazı dizisi başlangıcı.

  4. Bora dedi ki:

    Refactoring in tanımlamaları ve detaylı örnek ile açıklanması gerçekten çok güzel ve açıklayıcı olmuş elinize sağlık.
    Aslında makarna kod yazmamak için her yazılımcının bu şekilde kod yazması gerektiğini ve yaptığını düşünüyorum. Yazının bana en büyük katkısı Refactoringe ait terimlerin ne anlama geldiklerini öğrenmiş oldum

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir