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.
1 |
dotnet new console -o Refactoring |
Aşağıda görüldüğü gibi, dışardan data alınan 2 json dosya vardır.
1-) course.json :
1 2 3 4 5 |
{ "dpattern": {"name": "Design Pattern", "type": "Software"}, "hface": {"name": "Human Face", "type": "Art"}, "redis": {"name": "Redis", "type": "Software"} } |
2-) invoices.json :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[ { "customer": "Hasel Team", "performances": [ { "courseID": "dpattern", "student": 20 }, { "courseID": "hface", "student": 15 }, { "courseID": "redis", "student": 5 } ] } ] |
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:
1 2 3 4 5 |
public class Course { public string Name { get; set; } public Types Type { get; set; } } |
Invoice.cs:
1 2 3 4 5 |
public class Invoice{ public string customerName { get; set; } public Register[] registers { get; set; } } |
Register.cs:
1 2 3 4 |
public class Register{ public string courseID { get; set; } public int student { get; set; } } |
Types.cs:
1 2 3 4 5 |
public enum Types{ Software, Math, Art } |
Program.cs: İlk etapta gelen dataya göre modeller, manuel olarak aşağıdaki gibi doldurulur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static void Main(string[] args) { Console.WriteLine("Wellcome to Refactoring Example"); IDictionary<string,Course> courses = new Dictionary<string,Course>(); courses.Add("dpattern",new Course() { Name = "Design Pattern", Type = Types.Software }); courses.Add("hface",new Course() { Name = "Human Face", Type = Types.Art }); courses.Add("redis",new Course() { Name = "Redis", Type = Types.Software }); Invoice invoice = new Invoice(); invoice.customerName = "Hasel Team"; invoice.registers = new Register[]{ new Register(){courseID="dpattern",student=20}, new Register() { courseID = "hface", student = 5 }, new Register() { courseID = "redis", student = 5 }, }; Console.ReadLine(); } |
Ş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.
1 2 3 4 5 6 7 |
decimal totalAmount = 0; decimal volumeCredits = 0; var result = $"{invoice.customerName} için Fatura Detayı: \n"; CultureInfo trFormat = new CultureInfo("tr-TR", false); trFormat.NumberFormat.CurrencySymbol = "TL"; trFormat.NumberFormat.NumberDecimalDigits = 2; |
Part 2: Alınılan kurslar teker teker gezilir. Kurs tipine ve öğrenci sayılarına göre ödenecek ücret (thisAmount) hesaplanır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
foreach (Register reg in invoice.registers) { Course lesson = courses[reg.courseID]; var thisAmount = 0; 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; } } |
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.
1 2 3 4 5 6 7 8 9 |
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; |
Part 4: Toplam borç ve toplam kazanç ekrana yazdırılır.
1 2 3 4 |
result += $"Toplam borç { (totalAmount / 100).ToString("C", trFormat)}\n"; result += $"Kazancınız { volumeCredits.ToString("C", trFormat) } \n"; Console.WriteLine(result); Console.ReadLine(); |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; namespace Refactoring { class Program { static void Main(string[] args) { Console.WriteLine("Wellcome to Refactoring Example"); IDictionary<string, Course> courses = new Dictionary<string, Course>(); courses.Add("dpattern", new Course() { Name = "Design Pattern", Type = Types.Software }); courses.Add("hface", new Course() { Name = "Human Face", Type = Types.Art }); courses.Add("redis", new Course() { Name = "Redis", Type = Types.Software }); Invoice invoice = new Invoice(); invoice.customerName = "Hasel Team"; invoice.registers = new Register[]{ new Register(){courseID="dpattern",student=20}, new Register() { courseID = "hface", student = 15 }, new Register() { courseID = "redis", student = 5 }, }; decimal totalAmount = 0; decimal volumeCredits = 0; var result = $"{invoice.customerName} için Fatura Detayı: \n"; CultureInfo trFormat = new CultureInfo("tr-TR", false); trFormat.NumberFormat.CurrencySymbol = "TL"; trFormat.NumberFormat.NumberDecimalDigits = 2; foreach (Register reg in invoice.registers) { Course lesson = courses[reg.courseID]; var thisAmount = 0; 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 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; } result += $"Toplam borç { (totalAmount / 100).ToString("C", trFormat)}\n"; result += $"Kazancınız { volumeCredits.ToString("C",trFormat) } \n"; Console.WriteLine(result); Console.ReadLine(); } } } |
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():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public static int getAmounth(Register reg, Course lesson) { var thisAmount = 0; 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; } } return thisAmount; } |
İ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;}
1 2 3 4 |
public static Course findCourse(Register register) { return courses[register.courseID]; } |
Var olan kodun, findCourse() function’ı ile değiştirilmesi:
foreach (Register reg in invoice.registers){
}
foreach (Register reg in invoice.registers){//Course lesson = courses[reg.courseID];var thisAmount = getAmounth(reg, findCourse(reg));//kazanılan para puanvolumeCredits+=Math.Max(reg.student-15, 0);//extra bonus para puan her 5 yazılım öğrencisi içindecimal 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;}
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;}
var thisAmount = getAmounth(reg, findCourse(reg));
foreach (Register reg in invoice.registers){
}
1 2 3 4 5 6 7 8 9 10 11 |
public static decimal calculateVolumeCredit(Register register) { decimal volumeCredits = 0; //kazanılan para puan volumeCredits += Math.Max(register.student - 15, 0); // extra bonus para puan her 5 yazılım öğrencisi için decimal fiveStudentGroup = register.student / 5; if (Types.Software == findCourse(register).Type) volumeCredits += Math.Floor(fiveStudentGroup); return volumeCredits; } |
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;}
1 2 3 4 5 6 7 |
public static string format(decimal value) { CultureInfo trFormat = new CultureInfo("tr-TR", false); trFormat.NumberFormat.CurrencySymbol = "TL"; trFormat.NumberFormat.NumberDecimalDigits = 2; return value.ToString("C", trFormat); } |
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 puanvolumeCredits+=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”;
/*CultureInfo trFormat = new CultureInfo(“tr-TR”, false);
trFormat.NumberFormat.CurrencySymbol = “TL”;
trFormat.NumberFormat.NumberDecimalDigits = 2;*/
1 2 3 4 5 6 7 |
public static string tr(decimalvalue) { CultureInfotrFormat=newCultureInfo("tr-TR", false); trFormat.NumberFormat.CurrencySymbol="TL"; trFormat.NumberFormat.NumberDecimalDigits=2; returnvalue.ToString("C", trFormat); } |
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”;
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”;
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”;
1 2 3 4 5 6 7 8 9 |
public static decimal totalValumeCredits() { decimal volumeCredits = 0; foreach (Register reg in invoice.registers) { volumeCredits += calculateVolumeCredit(reg); } return volumeCredits; } |
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”;
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”;
- 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.
1 2 3 4 5 6 7 8 9 |
public static decimal getTotalAmount() { decimal totalAmount = 0; foreach (Register reg in invoice.registers) { totalAmount += getAmounth(reg); } return totalAmount; } |
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;}
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…
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.
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) ?
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..
Github üzerinden incelediğimde farkettim okurken kaçırmışım sanırım :) Sorunun cevabını aldım. Teşekkürler.
Gene bir müthiş yazı dizisi başlangıcı.
Teşekkürler Miraç :)
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
Selamlar,
Teşekkürler Adaş:)