Basitliğin güzelliği

Yakın zamanda, daha önceden ekip olarak yazdığımız ve uzun süredir çalışmakta olan bir projede tekrar kod yazmam gerekti. Bir LNG terminalinin günlük işlemlerini özetleyen bir raporda, cihazların ve işletim mekanizmasının değişmesi nedeni ile kodun yeniden yazılması gerekti.

Kısaca bilgi vermek gerekirse rapor, LNG giriş ve çıkış miktarlarını, işlemler için harcanan elektrik miktarını, soğutma ve ısıtma işlemleri için kullanılan kaynakları, stokları ve gaz niteliklerini saatlik, günlük, aylık ve genel toplam bazında listelemekteydi.

Koda bugüne kadar o kadar çok kişi dokunmuş ve kendince kod yazmıştı ki kodu okumak ve değiştirmek aşırı zorlaşmıştı. Asıl sorun ise raporu işleten hemen hemen bütün kodun tek bir blok içerisine yazılması idi. Her ne kadar işlem yapan bloklar boşluklar ve yorum satırları ile ayrılmış olsa da yaklaşık 900 satırlık tek bir metod içerisinde geçtim hatayı bulmak, aradığım kodu bulmak bile çok zordu.

İlk iş olarak kodu tek bir fonksiyon içerisinde çıkartıp ilgili iş parçacıkları oluşturacak şekilde farklı fonksiyonlara bölmem gerekti. Böylece, hem karmaşıklıktan kurtulup kodu yapacağı işe göre yalıtacak, hem de kodu yeniden yazarken daha iyi odaklanabilecektim. Bu aşamada kodu 10 farklı fonksiyona ayırdım ve sırası ile her birini eski yerinden kopyalayarak, yeni yazdığım ilgili fonksiyonun içine taşımaya başladım.

Asıl eğlenceli kısım eski kodları yeni fonksiyondaki yerlerine taşırken başladı. O kadar çok kişi dikkatsizce kod yazmıştı ki kodları taşırken gitgide artan bir heyecanla “Acaba daha neler göreceğim?” diyordum.

“Delilik: Aynı şeyleri tekrar tekrar yapıp farklı sonuçlar beklemektir.” — Albert EINSTEIN

List<IskeleOlcumDailyPay> iodpy = new List<IskeleOlcumDailyPay>();
if (iodpy.Count() > 0)
{
    iodpy = StokHesap.UygunBilesenleriGetir(tarih, "Tank1");
    Yogunluk_Tank1 = LiquidHesap.Liquid_DLNG_Hesapla(iodpy, Tanklar.Tank1, tarih);
    iodpy = StokHesap.UygunBilesenleriGetir(tarih, "Tank2");
    Yogunluk_Tank2 = LiquidHesap.Liquid_DLNG_Hesapla(iodpy, Tanklar.Tank2, tarih);
}
else
{
    mok_Tank1 = StokHesap.UygunBilesenleriGetir(tarih, "Tank1");
    Yogunluk_Tank1 = LiquidHesap.Liquid_DLNG_Hesapla(mok_Tank1, Tanklar.Tank1, tarih);
    mok_Tank2 = StokHesap.UygunBilesenleriGetir(tarih, "Tank2");
    Yogunluk_Tank2 = LiquidHesap.Liquid_DLNG_Hesapla(mok_Tank2, Tanklar.Tank2, tarih);
}

Yukarıda örneği verilen kodun benzerini pek çok yerde gördüm. Şimdi, neresinden başlasam bilmiyorum. Yeni yaratılmış bir koleksiyonun eleman sayısını gereksiz yere kontrol etmekten mi yoksa, if ve else koşullarının ikisinde de aynı işlemin yapılmasını mı? Kontrol hatalı, koşul gereksiz. Bilemiyorum Altan…

Tümevarım vs. Tümdengelim

Rapordaki verilerin Saatlik, Günlük, Aylık ve Genel toplam olarak listelendiğinden bahsetmiştik. Farkettiğim bir diğer hata da, raporda gösterilecek her bir veri için veritabanından tekrar tekrar veri çekilmesi idi.

ToplamSevkiyat = Daily.Getir(tarih, sonrakiGun, VeriTipi.TOTALS, Gaz.COR_NET_VOLUME);
GunlukSevkiyat = Daily.Getir(tarih, tarih, VeriTipi.TOTALS, Gaz.COR_NET_VOLUME);
AylikSevkiyat = Daily.Getir(bastar, tarih, VeriTipi.TOTALS, Gaz.COR_NET_VOLUME);

Oysa ki, başta çekilecek bir genel veri zaten aylık ve günlük verileri içerecekti. Bu durumda aşağıdaki kodu kullanmak bizi veritabanına yapılacak ziyaretlerden kurtaracaktı.

ToplamSevkiyat = Daily.Getir(tarih, sonrakiGun, VeriTipi.TOTALS, Gaz.COR_NET_VOLUME);
GunlukSevkiyat = ToplamSevkiyat.Where(x => x.Tarih >= tarih.Date && x.Tarih < tarih.Date.AddDays(1)).ToList();
AylikSevkiyat = ToplamSevkiyat.Where(x => x.Tarih >= bastar && x.Tarih <= tarih).ToList();
Kim korkar hain değişkenden?

Değişkenlerden iki türlü korkulur. Ya korkar, yanına bile yaklaşmazsınız ya da korkunuzdan defalarca aynı şeyi tanımlar durursunuz. Maalesef bu kodda her ikisi de mevcuttu. Rapora genel olarak bakıldığında ihtiyaç duyulan tarihsel parametrelerin ilk başlangıç tarihi, rapor günü ve raporun çekildiği ayın ilk günü olduğu aşikar olmasına rağmen hemen hemen her bir hesap bloğunu başında bu parametreler tekrar tekrar yeni değişkenlere atanmış ve devamında kullanılmıştı. Ve aslında ihtiyaç duyulan tüm parametreler 3 tarih bilgisinden türetilebilecekken (hatta ay başlangıcını da rapor tarihinden elde edebileceğimiz düşünüldüğünde 2!) kod içerisinde 30’a yakın tarih değişkeni mevcuttu. Bu bilgileri derleyip fonksiyonların dışında global bir parametre olarak tanımladığımızda hem kod basitleşti ve kısaldı hem de veri çağrımlarında hata yapma olasılığı azaldı.

Kontrollü olmak iyidir ama daha kontrollü olmak daha iyi değildir!

Bir şeyi kontrol ettiyseniz bir dah neden kontrol edesiniz? Ya da ettiğinizde ne bekliyorsunuz? Örneğin aşağıdaki kodda

var genelMiktar = progGenel.Where(g => g.Tarih == item);
if (genelMiktar.Count() > 0)
    ProgramGenel = GazProgramGenel / (progGenel.Where(g => g.Tarih == item).Sum(g => g.Miktar));   

genelMiktar değişkeni zaten tarihe göre süzülmüş veriyi içeriyorken, kontrol bloğunun içerisinde (Bkz. Satır 4) tekrardan bir süzme yapmanın gereksizliği ortada. Doğrudan yapılacak olan hesaplama da zaten bize aynı sonucu verecektir.

Açılın, ben boşluğum!

Visual Studio bence, Microsoft ürünleri içerisinde, Excel ile birlikte en iyilerinden biri. Hatta kullandığım pek çok IDE içerisinde en iyisi diyebilirim. Kod yazmada, düzenlemede, incelemede ve değerlendirmede getirdiği özellikler bir yazılımcının işini çok kolaylaştırıyor. Geçen zamanda farkettim ki başka bir IDE’de geliştirme yaptığımda, her eylemde Visual Studio’nun sağladığı kolaylıkları arıyorum. Bunun yanısıra HAVELSAN’da Coding Style üzerine staj yaptığım günlerden kalma bir alışkanlık olsa gerek düzenli, temiz ve okunur kod yazmak bende neredeyse bir hastalık haline gelmiş. Yazılı olmasa da bazı kurallarım var ve onlara göre kod yazmayı alışkanlık haline getirmek, sonra kendi koduma baktığımda çok faydalı oluyor.

Özellikle boşluk, adlandırma ve yakışıklı parantez ({ }) kullanımındaki alışkanlıklarım sayesinde kodu çok daha hızlı okuyabiliyorum. Tabii ki bir başkasının yazdığı kodda benim alışkanlıklarıma göre yazmasını beklemek anlamsız ancak en azından yazılan kodun kendi içerisinde bir düzeni, tutarlılığı olması güzel bir şeydir. Bir yerde parantez kullanırken diğerinde kullanmıyorsan ya da anlamsız yerde, anlamsız sayıda satır boşluğu bırakıyorsan senden sonra kodu okuyan kişinin eforunun bir kısmını da bunları ayırt edip ayıklamaya harcamasına neden oluyorsun demektir.

Gereksiz kod ve Ötesi

Kodda yapılan en büyük değişikliklerden biri cihaz değişimi esnasında normalde cihazlardan otomatik olarak yüklenen verilerin manuel olarak girilmesi ve hesaplamalarda hesaba ait manuel veri varsa otomatik olarak okunan değerlerin ezilerek bu manuel değerlerin kullanılması idi. Buraya kadar tamam ama aşağıdaki kodu bir incelediğimizde

var manualSCV = SCVVSH.GetManualData(tarih, Type.SCV);
if (manualSCV == null)  // Manuel SCV kaydı bulunamadı, normal şekilde devam et
{
    :
    :
    scv_corrected = GetUpdatedCorrectedByManual(ilkTarihSaat.Date, tarih.Date, scv_corrected, Type.SCV);
}
else
{
    :
    :
    var scv_corrected = GetSCV_corrected(tarih, true);
    scv_corrected = GetUpdatedCorrectedByManual(bastar, tarih, scv_corrected, Type.SCV);
}

Manuel veriyi çekmek için bir fonksiyon yazmak ve bunu kullanmak güzel bir yaklaşım, ancak en başta manuel verinin olmadığı denetlenmiş bir blokta aynı kodu kullanmak doğru bir yaklaşım değil

Şimdi toparlayalım

Aslında kodu düzeltirken yaptığım hiçbir şey yoktu. Çoğunlukla Kopyala-Yapıştır ve yukarıda bahsettiğim gereksiz kodların ayıklanması ile kodu yeniden düzenlediğimde önceden 900 satır tutan kodu yaklaşık 350 satıra indirdim. Kod sadece azalmakla kalmamış, daha okunaklı ve sürdürülebilir bir hale gelmişti. Bunun yanında ufak bir performans artışı dahi elde ettiğimi söyleyebilirim.

En nihayetinde kod aşağıdaki hale gelmişti :

public class GunlukRaporBL
{
    public GunlukRapor GetGunlukRapor(DateTime tarih)
    {
        FonksiyonA();
        FonksiyonB();
        FonksiyonC();
              :
              :
        FonksiyonN();
    }
}

Kod bu hale geldiğinde, daha önceki yazımda mümkün olmayan bir fırsat ortaya çıktı. Bütün bu fonksiyonlar veri ve işletim bakımından birbirinden bağımsız olduğuna göre neden sıra ile çalışsın ki? Ben bunları asenkron bir şekilde işletirsem, zaman kazanmaz mıydım? Bu nedenle kodu aşağıdaki hale getirdim :

public class GunlukRaporBL
{
    public GunlukRapor GetGunlukRapor(DateTime tarih)
    {
        Parallel.Invoke(
        () => FonksiyonA(),
        () => FonksiyonB(),
        () => FonksiyonC(),
              :
              :
        () => FonksiyonN());
    }
}

Artık önceden yaklaşık 1 dakika 45 saniyede çalışan rapor 35 saniyede çalışıyordu. Sadece kodu biraz düzgün yazarak ve asenkron bir şekilde işleterek hem işletim zamanı hem de satır sayısı olarak üçte birine indirmiştim.

Ve perde…

Tüm bu işlerin sonucunda hem kod okunabilirliği, hem kod kalitesi hem de performansı gözle görünür şekilde artmıştı. İşin güzel yanı ise, bunları yapmak için ne uzun yılların tecrübe birikimine, ne yoğun bir teknik bilgiye, ne haftalar süren kurslara katılmaya ne de müthiş bir kod dehası olmaya gerek yoktu. Yazılımın en temel prensipleri olan KISS (Keep It Simple, Stupid — İşi basit tut, aptal), DRY (Don’t repeat yourself — Kendini tekrarlama) ve YAGNI (You Ain’t Gonna Need It — İhtiyacın olmayacak) yöntemleri ve biraz da özen ile bu sonuca ulaşmak da pekala mümkündü. İşin en güzel yanı da, eski kodu yazmak yeni hali ile yazmaktan daha zor ve daha uzun. Yani en baştan başlayarak bu temel prensipler izlenmediğinden zor yol seçilmiş ve devamında da bu yolda ilerlenmiş. Oysa ki kolay olan yol seçildiğinde, işin ne kadar kolaylaştığı ortada.

Bazen bir işi yapmanın en zor yolu en basit şekilde yapmak gibi görünse de biraz dikkat ve temel bilgi ile en basiti yakalamk çok da güç değil ve emin olun ki ileride işinizi çok daha kolaylaştıracak bir yaklaşım. Hep söylerim, yazılım öğrenmesi çok kolay, ilerlemesi zor bir yol. Ve işin en kötü yanı bazen bu zorluğu bizzat kendimizin yaratması.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir