D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

açıkça elle yapılan: [explicit], programcı tarafından açık olarak yapılan
mutlak değişmez: [invariant], nesnenin tutarlılığı açısından her zaman için doğru olan
otomatik: [implicit], derleyici tarafından otomatik olarak yapılan
sözleşmeli programlama: [contract programming], işlevlerin giriş çıkış koşullarını ve nesnelerin tutarlılığını denetleyen dil olanağı
... bütün sözlük



İngilizce Kaynaklar


Diğer




Yapı ve Sınıflarda Sözleşmeli Programlama

Sözleşmeli programlama, kod hatalarını azaltmaya yarayan çok etkili bir olanaktır. D'nin sözleşmeli programlama olanaklarından ikisini Sözleşmeli Programlama bölümünde görmüştük. in ve out blokları, işlevlerin giriş ve çıkış garantilerini denetlemek için kullanılıyordu.

Not: O bölümdeki "in bloğu mu enforce mu" başlığı altındaki ilkeleri gözetmeniz önemlidir. Bu bölümdeki örnekler nesnelerin ve parametrelerin tutarlılıkları ile ilgili sorunların programcı hatalarına bağlı olduğu durumlarla ilgilidir. Diğer durumlarda işlevin kodlarından enforce'u çağırmanız doğru olur.

Hatırlamak amacıyla, üçgen alanını Heron formülünü kullanarak kenar uzunluklarından hesaplayan bir işlev yazalım. Üçgenin alanının doğru olarak hesaplanabilmesi için üç kenar uzunluğunun da sıfır veya daha büyük olması gerekir. Ek olarak, bir üçgenin hiçbir kenarının diğer ikisinin toplamından uzun olmaması da gerekir.

O giriş koşulları sağlandığında, üçgenin alanı da sıfır veya daha büyük olacaktır. Bu koşulları ve bu garantiyi sağlayan bir işlev şöyle yazılabilir:

private import std.math;

double üçgenAlanı(in double a, in double b, in double c)
in {
    // Kenarlar sıfırdan küçük olamaz
    assert(a >= 0);
    assert(b >= 0);
    assert(c >= 0);

    // Hiçbir kenar diğer ikisinin toplamından uzun olamaz
    assert(a <= (b + c));
    assert(b <= (a + c));
    assert(c <= (a + b));

} out (sonuç) {
    assert(sonuç >= 0);

} body {
    immutable yarıÇevre = (a + b + c) / 2;

    return sqrt(yarıÇevre
                * (yarıÇevre - a)
                * (yarıÇevre - b)
                * (yarıÇevre - c));
}
Üye işlevlerin in ve out blokları

in ve out blokları üye işlevlerle de kullanılabilir ve aynı biçimde işlevin giriş koşullarını ve çıkış garantisini denetler.

Yukarıdaki alan hesabı işlevini bir üye işlev olarak yazalım:

import std.stdio;
import std.math;

struct Üçgen {
private:

    double a;
    double b;
    double c;

public:

    double alan() const @property
    out (sonuç) {
        assert(sonuç >= 0);

    } body {
        const double yarıÇevre = (a + b + c) / 2;
        return sqrt(yarıÇevre
                    * (yarıÇevre - a)
                    * (yarıÇevre - b)
                    * (yarıÇevre - c));
    }
}

void main() {
    auto üçDörtBeşÜçgeni = Üçgen(3, 4, 5);
    writeln(üçDörtBeşÜçgeni.alan);
}

Üçgenin kenarları zaten yapının üye değişkenleri oldukları için bu işlevin parametreleri yok. O yüzden bu işlevin in bloğunu yazmadım. Üye değişkenlerin tutarlılıkları için aşağıdaki bilgileri kullanmanız gerekir.

Nesnelerin geçerliliği için in ve out blokları

Yukarıdaki üye işlev parametre almadığı için in bloğunu yazmadık. İşlevdeki hesabı da nesnenin üyelerini kullanarak yaptık. Yani bir anlamda üyelerin geçerli değerlere sahip olduklarını varsaydık. Bu varsayımın doğru olmasını sağlamanın bir yolu, sınıfın kurucu işlevine in bloğu eklemektir. Böylece kurucunun aldığı parametrelerin geçerli olduklarını en baştan nesne kurulurken denetleyebiliriz:

struct Üçgen {
// ...

    this(in double a, in double b, in double c)
    in {
        // Kenarlar sıfırdan küçük olamaz
        assert(a >= 0);
        assert(b >= 0);
        assert(c >= 0);

        // Hiçbir kenar diğer ikisinin toplamından uzun olamaz
        assert(a <= (b + c));
        assert(b <= (a + c));
        assert(c <= (a + b));

    } body {
        this.a = a;
        this.b = b;
        this.c = c;
    }

// ...
}

Üçgen nesnelerinin geçersiz değerlerle oluşturulmaları en başından engellenmiş olur. Artık programın geçersiz değerlerle kurulmuş olan bir üçgen nesnesi kullanması olanaksızdır:

    auto eksiKenarUzunluklu = Üçgen(-1, 1, 1);
    auto birKenarıFazlaUzun = Üçgen(1, 1, 10);

Kurucu işlevin in bloğu, yukarıdaki geçersiz nesnelerin oluşturulmalarına izin vermez:

core.exception.AssertError@deneme.d: Assertion failure

Bu sefer de out bloğunu yazmadığıma dikkat edin. Eğer gerekirse, daha karmaşık türlerde kurucu işlevin out bloğu da yazılabilir. O da nesnenin üyeleri kurulduktan sonra gerekebilecek denetimler için kullanılabilir.

Nesnelerin tutarlılığı için invariant blokları

Kurucuya eklenen in ve out blokları nesnenin yaşamının geçerli değerlerle başlayacağını, üyelere eklenen in ve out blokları da işlevlerin doğru işlediklerini garanti eder.

Ancak, bu denetimler nesnenin üyelerinin her zaman için geçerli veya tutarlı olacaklarını garanti etmeye elverişli değillerdir. Nesnenin üyeleri, üye işlevler içinde programcı hataları sonucunda tutarsız değerler edinebilirler.

Nesnenin tutarlılığını tarif eden koşullara "mutlak değişmez" denir. Örneğin, bir müşteri takip sınıfında her siparişe karşılık bir fatura bulunacağını varsayarsak, fatura adedinin sipariş adedinden fazla olamayacağı bu sınıfın bir mutlak değişmezidir. Eğer bu koşulun geçerli olmadığı bir müşteri takip nesnesi varsa, o nesnenin tutarlı durumda olduğunu söyleyemeyiz.

Bunun bir örneği olarak Sarma ve Erişim Hakları bölümünde kullandığımız Okul sınıfını ele alalım:

class Okul {
private:

    Öğrenci[] öğrenciler;
    int kızToplamı;
    int erkekToplamı;

// ...
}

Bu sınıftan olan nesnelerin tutarlı olarak kabul edilmeleri için, üç üyesi arasındaki bir mutlak değişmezin sağlanması gerekir. Öğrenci dizisinin uzunluğu, her zaman için kız öğrencilerin toplamı ile erkek öğrencilerin toplamına eşit olmalıdır:

    assert(öğrenciler.length == (kızToplamı + erkekToplamı));

O koşulun bozulmuş olması, bu sınıf kodlarında yapılan bir hatanın göstergesidir.

Yapı ve sınıf nesnelerinin tutarlılıkları o türün invariant bloklarında denetlenir. Bir veya daha fazla olabilen bu bloklar yapı veya sınıf tanımı içine yazılırlar ve sınıf nesnelerinin tutarlılık koşullarını belirlerler. in ve out bloklarında olduğu gibi, burada da assert denetimleri kullanılır:

class Okul {
private:

    Öğrenci[] öğrenciler;
    int kızToplamı;
    int erkekToplamı;

    invariant() {
        assert(öğrenciler.length == (kızToplamı + erkekToplamı));
    }

// ...
}

invariant bloklarındaki kodlar aşağıdaki zamanlarda otomatik olarak işletilir, ve bu sayede programın yanlış verilerle devam etmesi önlenmiş olur:

invariant bloklarındaki denetimlerin başarısız olmaları da in ve out bloklarında olduğu gibi AssertError atılmasına neden olur. Bu sayede programın tutarsız nesnelerle devam etmesi önlenmiş olur.

in ve out bloklarında olduğu gibi, invariant blokları da -release seçeneği ile iptal edilebilir:

dmd deneme.d -w -release
Sözleşmeli programlama ve türeme

Arayüz ve sınıf üye işlevlerinin in ve out blokları olabilir. Böylece hem alt sınıflarının güvenebilecekleri giriş koşulları hem de kullanıcılarının güvenebilecekleri çıkış garantileri tanımlamış olurlar. Üye işlevlerin alt sınıflardaki tanımları da in ve out blokları içerebilirler. Alt sınıflardaki in blokları giriş koşullarını hafifletebilirler ve out blokları da ek çıkış garantileri verebilirler.

Normalde bir arayüzle etkileşecek biçimde soyutlanmış olarak yazıldığından kullanıcı kodunun çoğu durumda alt sınıflardan haberi yoktur. Kullanıcı kodu bir arayüzün sözleşmesine uygun olarak yazıldığından, bir alt sınıfın bu sözleşmenin giriş koşullarını ağırlaştırması da doğru olmaz. O yüzden alt sınıflar giriş koşullarını ancak hafifletebilirler.

in blokları üst sınıftan alt sınıfa doğru otomatik olarak işletilir. Herhangi bir in bloğunun başarılı olması (bütün assert'lerin doğru çıkması), giriş koşullarının sağlanmış olduğu anlamına gelir ve işlev çağrısı başarıyla devam eder. (Aşağıda anlatılacağı gibi, bunun bir etkisi, giriş koşullarının istenmeden etkisizleştirilebilmeleridir.)

Benzer biçimde, alt sınıflar out blokları da tanımlayabilirler. Çıkış garantileri bir işlevin verdiği garantileri tanımladığından alt sınıf üye işlevi üst sınıfın garantilerini de sağlamak zorundadır. Alt sınıf ek garantiler de getirebilir.

out blokları üst sınıftan alt sınıfa doğru otomatik olarak işletilir. Bir işlevin çıkış garantilerinin sağlanmış olması için bütün out bloklarının başarıyla işletilmeleri gerekir.

Bu kuralları gösteren aşağıdaki yapay program bir interface ve ondan türeyen bir class tanımlamaktadır. Buradaki alt sınıf hem daha az koşul gerektirmekte hem de daha fazla garanti vermektedir:

interface Arayüz {
    int[] işlev(int[] a, int[] b)
    in {
        writeln("Arayüz.işlev.in");

        /* Bu arayüz işlevi parametrelerinin aynı uzunlukta
         * olmalarını gerektirmektedir. */
        assert(a.length == b.length);

    } out (sonuç) {
        writeln("Arayüz.işlev.out");

        /* Bu arayüz işlevi dönüş değerinin çift sayıda
         * elemandan oluşacağını garanti etmektedir.
         * (Not: Boş dilimin çift sayıda elemanı olduğu kabul
         * edilir.) */
        assert((sonuç.length % 2) == 0);
    }
}

class Sınıf : Arayüz {
    int[] işlev(int[] a, int[] b)
    in {
        writeln("Sınıf.işlev.in");

        /* Bu sınıf işlevi üst türdeki giriş koşullarını
         * hafifletmektedir: Birisi boş olmak kaydıyla
         * parametrelerin uzunluklarının eşit olmaları
         * gerekmemektedir. */
        assert((a.length == b.length) ||
               (a.length == 0) ||
               (b.length == 0));

    } out (sonuç) {
        writeln("Sınıf.işlev.out");

        /* Bu sınıf ek garantiler vermektedir: Sonuç boş
         * olmayacaktır ve ilk ve sonuncu elemanların
         * değerleri eşit olacaktır. */
        assert((sonuç.length != 0) &&
               (sonuç[0] == sonuç[$ - 1]));

    } body {
        writeln("Sınıf.işlev.body");

        /* Bu yalnızca 'in' ve 'out' bloklarının işleyişini
         * gösteren yapay bir gerçekleştirme. */

        int[] sonuç;

        if (a.length == 0) {
            a = b;
        }

        if (b.length == 0) {
            b = a;
        }

        foreach (i; 0 .. a.length) {
            sonuç ~= a[i];
            sonuç ~= b[i];
        }

        sonuç[0] = sonuç[$ - 1] = 42;

        return sonuç;
    }
}

import std.stdio;

void main() {
    auto c = new Sınıf();

    /* Aşağıdaki çağrı Arayüz'ün gerektirdiği koşulu
     * sağlamadığı halde kabul edilir çünkü Sınıf'ın giriş
     * koşulunu sağlamaktadır. */
    writeln(c.işlev([1, 2, 3], []));
}

Sınıf.işlev'in in bloğu Arayüz.işlev'in giriş koşulu sağlanmadığı için işletilmiştir:

Arayüz.işlev.in
Sınıf.işlev.in    ← Arayüz.işlev.in başarılı olsa bu işletilmezdi
Sınıf.işlev.body
Arayüz.işlev.out
Sınıf.işlev.out
[42, 1, 2, 2, 3, 42]
Giriş koşullarının istenmeden etkisizleştirilmeleri

in bloğu olmayan bir işlev hiçbir giriş koşulu gerektirmiyor demektir. Bunun bir etkisi olarak, üst sınıfta giriş koşulu bulunan alt sınıf işlevleri kendileri giriş koşulu tanımlamazlarsa üst sınıftaki giriş koşullarını etkisiz hale getirmiş olurlar. (Yukarıda anlatıldığı gibi, üye işlevin tanımlarından birisinin in bloğunun başarılı olması giriş koşullarının sağlanmış olduğu anlamına gelir.)

Bu yüzden, genel bir kural olarak, üst sınıfta in bloğu bulunan bir üye işlevin alt sınıfta da in bloğu bulunmalıdır. Örneğin, alt sınıf işlevine her zaman için başarısız olan bir in bloğu eklenebilir.

Bunu görmek için üst sınıfının giriş koşulunu etkisiz hale getiren bir alt sınıfa bakalım:

class Protokol {
// ...

    void hesapla(double d)
    in {
        assert(d > 42);

    } body {
        // ...
    }
}

class ÖzelProtokol : Protokol {
    /* 'in' bloğu bulunmadığından, bu işlev
     * 'Protokol.hesapla'nın giriş koşulunu belki de
     * istemeden etkisizleştirir. */
    override void hesapla(double d) {
        // ...
    }
}

void main() {
    auto ö = new ÖzelProtokol();
    ö.hesapla(10);    /* HATA: Parametre değeri olan 10 üst
                       * sınıfın giriş koşulunu sağlamadığı
                       * halde bu çağrı başarılı olur. */
}

Bir çözüm, ÖzelProtokol.hesapla'ya her zaman için başarısız olan bir giriş koşulu eklemektir:

class ÖzelProtokol : Protokol {
    override void hesapla(double d)
    in {
        assert(false);

    } body {
        // ...
    }
}

Bu sefer üst sınıfın giriş koşulu etkili olacak ve yanlış parametre değeri yakalanacaktır:

core.exception.AssertError@deneme.d: Assertion failure
Özet