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:
- Kurucu işlev sonunda: Böylece nesnenin yaşamına tutarlı olarak başladığı garanti edilir.
- Sonlandırıcı işlev çağrılmadan önce: Böylece sonlandırma işlemlerinin tutarlı üyeler üzerinde yapılacakları garanti edilir.
public
bir işlev işletilmeden önce ve sonra: Böylece üye işlevlerdeki kodların nesneyi bozmadıkları garanti edilir.Not: Burada
public
işlevler için söylenen,export
işlevler için de geçerlidir. (export
işlevleri kısaca "dinamik kütüphalerin sundukları işlevler" olarak tanımlayabiliriz.)
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
in
veout
bloklarını üye işlevlerle de kullanabilirsiniz; kurucu işleve ekleyerek nesnelerin geçersiz parametrelerle kurulmalarını önleyebilirsiniz.- Nesnelerin yaşamları boyunca her zaman için tutarlı olmalarını garantilemek için
invariant
bloklarını kullanabilirsiniz. - Alt türlerin üye işlevlerinin de
in
blokları olabilir. Alt sınıfların giriş koşulları üst sınıftakilerden daha ağır olmamalıdır. (in
bloğunun olmaması "hiç giriş koşulu gerektirmemek" anlamına gelir.) - Alt türlerin üye işlevlerinin de
out
blokları olabilir. Alt sınıf işlevleri kendi garantilerinden başka üst sınıfların garantilerini de sağlamak zorundadırlar.