Sözleşmeli Programlama
Sözleşmeli programlama, işlevlerin hizmet sunan birimler olarak kabul edilmeleri düşüncesi üzerine kurulu bir programlama yöntemidir. Bu düşünceye göre, işlevler ve onları çağıran kodlar arasında yazısız bazı anlaşmalar vardır. Sözleşmeli programlama, bu anlaşmaları dil düzeyinde belirlemeye yarayan olanaktır.
Sözleşmeli programlama, ticari bir dil olan Eiffel tarafından "design by contract (DBC)" adıyla yayılmıştır. Bu yöntem D dilinde "contract programming" olarak geçer. Birim testlerinde olduğu gibi, assert ifadesine dayanır ve D'nin kod sağlamlığı sağlayan bir başka olanağıdır.
D'de sözleşmeli programlama üç temelden oluşur:
- İşlevlerin
inblokları - İşlevlerin
outblokları - Sınıfların
invariantblokları (Bunu daha sonra sınıfları anlatırken göstereceğim)
Giriş koşulları için in blokları
İşlevlerin doğru çalışabilmeleri, aldıkları parametrelere bağlı olabilir. Örneğin karekök alan bir işlev kendisine verilen parametrenin sıfırdan küçük olmamasını şart koşar; veya parametre olarak tarih bilgisi alan bir işlev ayın 1 ile 12 arasında olmasını şart koşar.
Bu tür koşulları daha önce assert dersinde görmüştük. İşlevlerin parametreleriyle ilgili denetimleri, assert ifadeleri kullanarak işlevin tanımlandığı blok içinde yapıyorduk:
dstring zamanDizgisi(in int saat, in int dakika) { assert((saat >= 0) && (saat <= 23)); assert((dakika >= 0) && (dakika <= 59)); return to!dstring(saat) ~ ":" ~ to!dstring(dakika); }
D'nin sözleşmeli programlama anlayışında işlevlerin giriş koşulları "giriş" anlamına gelen in bloklarında denetlenir. Sözleşmeli programlama blokları kullanıldığı zaman, işlevin asıl bloğu da "gövde" anlamına gelen body ile belirlenir:
import std.cstream; import std.conv; dstring zamanDizgisi(in int saat, in int dakika) in { assert((saat >= 0) && (saat <= 23)); assert((dakika >= 0) && (dakika <= 59)); } body { return to!dstring(saat) ~ ":" ~ to!dstring(dakika); } void main() { dout.writefln(zamanDizgisi(12, 34)); }
İşlevin in bloğunun yararı, işlevin başlatılmasıyla ilgili olan denetimlerin bir arada ve ayrı bir blok içinde yapılmasıdır. Böylece assert ifadeleri işlevin asıl işlemlerinin arasına karışmamış olurlar. İşlevin içinde yine de gerektikçe assert ifadeleri kullanılabilir, ama giriş koşulları sözleşmeli programlama anlayışına uygun olarak in bloğuna yazılırlar.
in bloklarındaki kodlar programın çalışması sırasında işlevin her çağrılışında otomatik olarak işletilirler. İşlevin asıl işleyişi, ancak bu koşullar sağlandığında devam eder. Böylece işlevin geçersiz başlangıç koşulları ile çalışması ve programın yanlış sonuçlarla devam etmesi önlenmiş olur.
in bloğundaki bir assert ifadesinin başarısız olması, sözleşmeyi işlevi çağıran tarafın bozduğunu gösterir; işlev, sözleşmenin gerektirdiği şekilde çağrılmamış demektir.
Çıkış garantileri için out blokları
İşlevin yaptığı kabul edilen sözleşmenin karşı tarafı da işlevin sağladığı garantilerdir. Örneğin belirli bir senedeki Şubat ayının kaç gün çektiği bilgisini döndüren bir işlevin çıkış garantisi, döndürdüğü değerin 28 veya 29 olmasıdır.
Çıkış garantileri, işlevlerin "çıkış" anlamına gelen out bloklarında denetlenirler.
İşlevin dönüş değerinin özel bir ismi yoktur; bu değer return ile isimsiz olarak döndürülür. Bu durum, dönüş değeriyle ilgili garantileri yazarken bir sorun doğurur: ismi olmayınca, dönüş değeriyle ilgili assert ifadeleri de yazılamaz.
Bu sorun out anahtar sözcüğünden sonra verilen isimle halledilmiştir. Bu isim dönüş değerini temsil eder ve denetlenecek olan garantilerde bu isim kullanılır:
int şubattaKaçGün(in int yıl) out (sonuç) { // Not: dmd 2.046'da devam etmekte olan bir hata yüzünden bu // koşulu denetleyemiyoruz: // assert((sonuç == 28) || (sonuç == 29)); } body { return artıkYıl_mı(yıl) ? 29 : 28; }
Ben out bloğunun parametresinin ismi olarak sonuç yazmayı uygun buldum; siz dönüşDeğeri gibi başka bir isim de verebilirsiniz. Hangi ismi kullanırsanız kullanın, o isim işlevin dönüş değerini temsil eder.
Bazen işlevin dönüş değeri yoktur, veya dönüş değerinin denetlenmesi gerekmiyordur. O zaman out bloğu parametresiz olarak yazılır:
out { // ... }
İşleve girerken in bloklarının otomatik olarak işletilmeleri gibi, out blokları da işlevden çıkarken otomatik olarak işletilirler.
out bloğundaki bir assert ifadesinin başarısız olması, sözleşmenin işlev tarafından bozulduğunu gösterir; işlev, sözleşmenin gerektirdiği değeri veya yan etkiyi üretememiş demektir.
Daha önceki derslerde hiç kullanmamış olduğumuzdan da anlaşılabileceği gibi, in ve out bloklarının kullanımı seçime bağlıdır. Bunlara yine seçime bağlı olan unittest bloklarını da eklersek, D'de işlevler dört blok halinde yazılabilirler:
- Giriş koşulları için
inbloğu: seçime bağlıdır ve giriş koşullarını denetler - Çıkış garantileri için
outbloğu: seçime bağlıdır ve çıkış garantilerini denetler - İşlevin asıl işlemlerini içeren
bodybloğu: bu bloğun yazılması şarttır, ama eğerinveoutblokları kullanılmamışsabodyanahtar sözcüğü yazılmayabilir - İşlevin birim testlerini içeren
unittestbloğu: bu aslında işlevin parçası değildir ve kendi başına işlev gibi yazılır; ama denetlediği işlevin hemen altına yazılması, aralarındaki bağı gösterme bakımından uygun olur
Bütün bu blokları içeren bir işlev tanımı şöyle yazılabilir:
import std.cstream; /* * Toplamı iki parça olarak bölüştürür. * * Toplamdan öncelikle birinciye verir, ama birinciye hiçbir * zaman 7'den fazla vermez. Gerisini ikinciye verir. */ void bölüştür(in int toplam, out int birinci, out int ikinci) in { assert(toplam >= 0); } out { assert(toplam == (birinci + ikinci)); } body { birinci = (toplam >= 7) ? 7 : toplam; ikinci = toplam - birinci; } unittest { int birinci; int ikinci; // Toplam 0 ise ikisi de 0 olmalı bölüştür(0, birinci, ikinci); assert(birinci == 0); assert(ikinci == 0); // Toplam 7'den az ise birincisi toplam'a, ikincisi 0'a // eşit olmalı bölüştür(3, birinci, ikinci); assert(birinci == 3); assert(ikinci == 0); // Sınır koşulunu deneyelim bölüştür(7, birinci, ikinci); assert(birinci == 7); assert(ikinci == 0); // 7'den fazla olduğunda birinci 7 olmalı, gerisi ikinciye // gitmeli bölüştür(8, birinci, ikinci); assert(birinci == 7); assert(ikinci == 1); // Bir tane de büyük bir değerle deneyelim bölüştür(1_000_007, birinci, ikinci); assert(birinci == 7); assert(ikinci == 1_000_000); } void main() { int birinci; int ikinci; bölüştür(123, birinci, ikinci); dout.writefln("birinci: ", birinci, " ikinci: ", ikinci); }
$ dmd deneme.d -ofdeneme -w -unittest
$ ./deneme
birinci: 7 ikinci: 116
Bu işlevin asıl işi yalnızca 2 satırdan oluşuyor; denetleyen kodlar ise tam 19 satır! Bu kadar küçük bir işlev için bu kadar emeğin gereksiz olduğu düşünülebilir. Ama dikkat ederseniz, programcı hiçbir zaman bilerek hatalı kod yazmaz. Programcının yazdığı kod her zaman için doğru çalışacak şekilde yazılmıştır. Buna rağmen, hatalar da hep böyle doğru çalışacağı düşünülen kodlar arasından çıkar.
İşlevlerden beklenenlerin; birim testleri ve sözleşmeli programlama ile açıkça ortaya koyulmaları, doğru olarak yazdığımız işlevlerin her zaman için doğru kalmalarına yardım eder. Program hatalarını azaltan hiçbir olanağı küçümsememenizi öneririm. Birim testleri ve sözleşmeli programlama olanakları bizi zorlu hatalardan koruyan çok etkili araçlardır. Böylece zamanımızı hata ayıklamak yerine, ondan çok daha zevkli ve verimli olan kod yazmaya ayırabiliriz.
Sözleşmeli programlamayı etkisizleştirmek
Birim testlerinin tersine, sözleşmeli programlama normalde etkilidir; etkisizleştirmek için özel bir derleyici veya geliştirme ortamı ayarı gerekir. Bunun için dmd derleyicisinde -release seçeneği kullanılır:
dmd deneme.d -ofdeneme -w -release
Program o seçenekle derlendiğinde in, out, ve invariant blokları programa dahil edilmezler.
Problem
- İki futbol takımının puanlarını bir maçın sonucuna göre arttıran bir işlev yazın.
Bu işlevin ilk iki parametresi, birinci ve ikinci takımın attıkları goller olsun. Son iki parametresi de bu takımların maçtan önceki puanları olsun. Bu işlev, golleri dikkate alarak birinci ve ikinci takımın puanlarını düzenlesin: fazla gol atan taraf üç puan kazansız, goller eşitse iki takım da birer puan kazansınlar.
Ek olarak, işlevin dönüş değeri de kazanan tarafı belirtsin: birinci kazanmışsa 1, ikinci kazanmışsa 2, berabere kalmışlarsa 0.
Aşağıdaki programla başlayın ve işlevin dört bloğunu uygun şekilde doldurun. Benim main içine yazdığım assert ifadelerini silmeyin; onlar, benim bu işlevin çalışması konusundaki beklentilerimi belgeliyorlar.
int puanEkle(in int goller1, in int goller2, ref int puan1, ref int puan2) in { // ... } out (sonuç) { // ... } body { // ... return -1; } unittest { // ... } void main() { int birincininPuanı = 10; int ikincininPuanı = 7; int kazananTaraf; kazananTaraf = puanEkle(3, 1, birincininPuanı, ikincininPuanı); assert(birincininPuanı == 13); assert(ikincininPuanı == 7); assert(kazananTaraf == 1); kazananTaraf = puanEkle(2, 2, birincininPuanı, ikincininPuanı); assert(birincininPuanı == 14); assert(ikincininPuanı == 8); assert(kazananTaraf == 0); }
Not: Aslında burada üç değeri olan bir enum türü döndürmek çok daha doğru olur:
enum MaçSonucu { birinciKazandı, ikinciKazandı, berabere } MaçSonucu puanEkle(in int goller1, in int goller2, ref int puan1, ref int puan2) // ...
Ben out bloğunda int türünden bir dönüş değerini denetleyebilelim diye işlevde int seçtim.
D.ershane
Forum
Wiki
Projeler
Tanıtım
İletişim
Hakları