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 ise işlevin kodları içinden enforce'u çağırmanız doğru olacaktır.

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 her kenarın uzunluğunun sıfırdan büyük olması gerekir. Ek olarak, bir üçgenin hiçbir kenarı da diğer ikisinin toplamından uzun veya eşit olamaz.

Ancak o giriş koşulları sağlandığında üçgenin alanının varlığından söz edilebilir. Bu koşulları ve bu garantiyi sağlayan bir işlev şöyle yazılabilir:

private import std.math;

double üçgenAlanı(double a, double b, double c)
in {
    // Kenarlar sıfırdan büyük olmalıdır
    assert(a > 0);
    assert(b > 0);
    assert(c > 0);

    // Her kenar diğer ikisinin toplamından kısa olmalıdır
    assert(a < (b + c));
    assert(b < (a + c));
    assert(c < (a + b));

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

} do {
    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
    out (sonuç) {
        assert(sonuç > 0);

    } do {
        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ından bu işlevin parametreleri bulunmuyor. 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(double a, double b, double c)
    in {
        // Kenarlar sıfırdan büyük olmalıdır
        assert(a > 0);
        assert(b > 0);
        assert(c > 0);

        // Her kenar diğer ikisinin toplamından kısa olmalıdır
        assert(a < (b + c));
        assert(b < (a + c));
        assert(c < (a + b));

    } do {
        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.

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]));

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

        /* 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.do
Arayüz.işlev.out
Sınıf.işlev.out
[42, 1, 2, 2, 3, 42]
Özet