Sarma ve Erişim Hakları
Şimdiye kadar tasarladığımız bütün yapı ve sınıf türlerinin bütün üyeleri dışarıdan erişime açıktı.
Hatırlamak için şöyle bir öğrenci yapısı düşünelim.
enum Cinsiyet { kız, erkek } struct Öğrenci { string isim; Cinsiyet cinsiyet; }
O yapının nesnelerinin üyelerine istediğimiz gibi erişebiliyorduk:
auto öğrenci = Öğrenci("Tolga", Cinsiyet.erkek); writefln("%s bir %s öğrencidir.", öğrenci.isim, öğrenci.cinsiyet);
Üyelere böyle serbestçe erişebilmek, o üyeleri programda istediğimiz gibi kullanma olanağı sağladığı için yararlıdır. O kod, öğrenci hakkındaki bilgiyi çıkışa şöyle yazdırır:
Tolga bir erkek öğrencidir.
Ancak, üye erişiminin bu kadar serbest olması sakıncalar da doğurabilir. Örneğin belki de yanlışlıkla, öğrencinin yalnızca ismini değiştirdiğimizi düşünelim:
öğrenci.isim = "Ayşe";
O atama sonucunda artık nesnenin geçerliliği bozulmuş olabilir:
Ayşe bir erkek öğrencidir.
Başka bir örnek olarak, bir grup öğrenciyi barındıran Okul
isminde bir sınıfa bakalım. Bu sınıf, okuldaki kız ve erkek öğrencilerin sayılarını ayrı olarak tutuyor olsun:
class Okul { Öğrenci[] öğrenciler; size_t kızToplamı; size_t erkekToplamı; void ekle(Öğrenci öğrenci) { öğrenciler ~= öğrenci; final switch (öğrenci.cinsiyet) { case Cinsiyet.kız: ++kızToplamı; break; case Cinsiyet.erkek: ++erkekToplamı; break; } } override string toString() const { return format( "%s kız, %s erkek; toplam %s öğrenci", kızToplamı, erkekToplamı, öğrenciler.length); } }
ekle
işlevini kullanarak o sınıfın nesnelerine yeni öğrenciler ekleyebiliriz:
auto okul = new Okul; okul.ekle(Öğrenci("Leyla", Cinsiyet.kız)); okul.ekle(Öğrenci("Metin", Cinsiyet.erkek)); writeln(okul);
ve tutarlı bir çıktı elde ederiz:
1 kız, 1 erkek; toplam 2 öğrenci
Oysa bu sınıfın üyelerine serbestçe erişebiliyor olmak, onun nesnelerini de tutarsız hale getirebilir. Örneğin öğrenciler
üyesine doğrudan yeni bir öğrenci eklediğimizi düşünelim:
okul.öğrenciler ~= Öğrenci("Nimet", Cinsiyet.kız);
Yeni öğrenci, toplamları sayan ekle
işlevi çağrılmadan eklendiği için bu Okul
nesnesi artık tutarsızdır:
1 kız, 1 erkek; toplam 3 öğrenci
Sarma
Sarma, bu tür durumları önlemek için üyelere erişimi kısıtlayan bir olanaktır.
Başka bir yararı, kullanıcıların sınıfın iç yapısını bilmek zorunda kalmamalarıdır. Sınıf, sarma yoluyla bir anlamda bir kara kutu haline gelir ve ancak arayüzünü belirleyen işlevler aracılığıyla kullanılabilir.
Kullanıcıların sınıfın üyelerine doğrudan erişememeleri, ayrıca sınıfın iç tasarımının ileride rahatça değiştirilebilmesini de sağlar. Sınıfın arayüzündeki işlevlerin tanımına dokunulmadığı sürece, içinin yapısı istendiği gibi değiştirilebilir.
Sarma, kredi kartı numarası veya şifre gibi değerli veya gizli verilere erişimi kısıtlamak için değildir ve bu amaçla kullanılamaz. Sarma, program geliştirme konusunda yararlı bir olanaktır: Programdaki tanımların kolay ve doğru kullanılmalarını ve kolayca değiştirilebilmelerini sağlar.
Erişim hakları
D'de erişim hakları iki bağlamda belirtilebilir:
- Yapı veya sınıf düzeyinde: Her yapı veya sınıf üyesinin erişim hakkı ayrıca belirtilebilir.
- Modül düzeyinde: Modül içinde tanımlanmış olan her tür olanağın erişim hakkı ayrıca belirtilebilir: sınıf, yapı, işlev, enum, vs.
Bir üyenin veya modül tanımının erişim hakkı aşağıdaki özelliklerden birisi olarak belirtilebilir. Varsayılan erişim public
'tir.
-
public
, genel: Programın her tarafından erişilebilmeyi ifade eder; hiçbir erişim kısıtlaması yoktur.Bunun bir örneği olarak
stdout
standart akımını düşünebilirsiniz. Onu bildirenstd.stdio
modülününimport
ile eklenmesi,stdout
'un serbestçe kullanılabilmesi için yeterlidir. -
private
, özel: özel erişimi ifade ederBu şekilde tanımlanan üyelere içinde tanımlı oldukları sınıfın kodları tarafından, veya o sınıfı barındıran modüldeki kodlar tarafından erişilebilir.
Ek olarak,
private
olarak işaretlenmiş olan üye işlevler alt sınıflar tarafından tekrar tanımlanamazlar. -
package
, pakede açık: paketteki modüller tarafından erişilebilmeyi ifade ederBu şekilde işaretlenmiş olan bir tanım, onu barındıran paketteki bütün kodlara açıktır. Bu erişim hakkı yalnızca modülü içeren en içteki pakede verilir.
Örneğin
hayvan.omurgalilar.kedi
isimli bir modül içindepackage
olarak işaretlenmiş olan bir tanım;kedi
modülünün kendisinden başka,omurgalilar
pakedindeki bütün modüllere de açıktır.private
belirtecinde olduğu gibi,package
olarak işaretlenmiş olan üye işlevler alt sınıflar tarafından tekrar tanımlanamazlar. -
protected
, korumalı: türetilen sınıf tarafından da erişilebilmeyi ifade ederprivate
erişimi genişleten bir erişimdir: Bu şekilde işaretlenmiş olan üyeye sınıf tarafından erişilmek yanında, o sınıftan türetilen sınıflardan da erişilebilir.
Ek olarak, export
belirteci program içinde tanımlanmış olanaklara programın dışından da erişilebilmesini sağlar.
Belirtilmesi
Erişim hakları üç şekilde belirtilebilir.
Tek bir tanımın önüne yazıldığında yalnızca o tanımın erişim haklarını belirler. Bu, Java ve bazı başka dillerdeki gibidir:
private int birSayı; private void birİşlev() { // ... }
İki nokta üst üste karakteriyle yazıldığında, aynı şekilde yeni bir erişim hakkı yazılana kadarki bütün tanımları etkiler. Bu, C++'daki gibidir:
private: // ... // ... buradaki bütün tanımlar özel ... // ... protected: // ... // ... buradakiler korumalı ... // ...
Blok söz dizimiyle yazıldığında bütün bloğun içini etkiler:
private { // ... // ... buradakilerin hepsi özel ... // ... }
Bu üç yöntemin etkisi aynıdır. Hangisini kullanacağınıza tasarımınıza uygun olduğunu düşündüğünüz şekilde serbestçe karar verebilirsiniz.
import
'lar normalde modüle özeldir
import
ile eklenen modüller, o modülü dolaylı olarak ekleyen başka modüller tarafından görülemezler. Örneğin okul
modülü std.stdio
modülünü eklese, okul
modülünü ekleyen başka modüller std.stdio
'dan otomatik olarak yararlanamazlar.
Örneğin okul
modülü şöyle başlıyor olsun:
module okul.okul; import std.stdio; // kendi işi için eklenmiş... // ...
Onu kullanan şu program derlenemez:
import okul.okul; void main() { writeln("merhaba"); // ← derleme HATASI }
O yüzden, std.stdio
'yu asıl modülün ayrıca eklemesi gerekir:
import okul.okul; import std.stdio; void main() { writeln("merhaba"); // şimdi derlenir }
Bazen, eklenen bir modülün başka modülleri de otomatik ve dolaylı olarak sunması istenebilir. Örneğin okul
isimli bir modülün eklenmesinin, ogrenci
modülünü de otomatik olarak eklemesi istenebilir. Bu, import
işlemi public
olarak işaretlenerek sağlanır:
module okul.okul; public import okul.ogrenci; // ...
Artık okul
modülünü ekleyen modüller ogrenci
modülünü açıkça eklemek zorunda kalmadan, onun içindeki Öğrenci
yapısını kullanabilirler:
import okul.okul; void main() { auto öğrenci = Öğrenci("Tolga", Cinsiyet.erkek); // ... }
O program yalnızca okul
modülünü eklediği halde Öğrenci
yapısını da kullanabilmektedir.
Sarmayı ne zaman kullanmalı
Sarma, giriş bölümünde gösterdiğim sorunları önlemek ve sınıf tasarımlarını serbest bırakmak için çok etkili bir olanaktır.
Üyelerin ve başka değişkenlerin ilgisiz kodlar tarafından serbestçe değiştirilmeleri önlenmiş olur. Böylece, yukarıdaki ekle
işlevinde olduğu gibi, nesnelerin tutarlılıkları denetim altına alınmış olur.
Ayrıca, başka kodlar yapı ve sınıf gerçekleştirmelerine bağımlı kalmamış olurlar. Örneğin Okul.öğrenciler
üyesine erişilebiliyor olması, sizin o diziyi daha sonradan örneğin bir eşleme tablosu olarak değiştirmenizi güçleştirir. Çünkü bu değişiklik kullanıcı kodlarını da etkileyecektir.
Sarma, nesne yönelimli programlamanın en yararlı olanakları arasındadır.
Örnek
Yukarıdaki Öğrenci
yapısını ve Okul
sınıfını sarmaya uygun olacak şekilde tanımlayalım ve küçük bir deneme programında kullanalım.
Bu örnekte toplam üç dosya tanımlayacağız. Önceki bölümden de hatırlayacağınız gibi; "okul" isminde bir klasör içinde tanımlandıkları için ilk ikisi okul
pakedinin parçaları olacaklar:
- "okul/ogrenci.d":
Öğrenci
yapısını içerenogrenci
modülü - "okul/okul.d":
Okul
sınıfını tanımlayanokul
modülü - "deneme.d": küçük bir deneme programı
Bu programın oluşturulabilmesi için bütün dosyaların belirtilmesi gerektiğini unutmayın:
$ dmd deneme.d okul/ogrenci.d okul/okul.d -w
İlk önce "okul/ogrenci.d" dosyasını görelim:
module okul.ogrenci; import std.string; import std.conv; enum Cinsiyet { kız, erkek } struct Öğrenci { package string isim; package Cinsiyet cinsiyet; string toString() const { return format("%s bir %s öğrencidir.", isim, to!string(cinsiyet)); } }
Bu yapının üyelerini yalnızca kendi pakedindeki kodlara açmak için package
olarak belirledim; çünkü biraz sonra göreceğimiz Okul
sınıfının bu yapının üyelerine doğrudan erişmesini istedim.
Aynı pakedin parçası olsa bile başka bir modül tarafından yapının üyelerine erişilmesi, aslında temelde sarmaya karşıdır. Yine de; Öğrenci
ve Okul
'un birbirlerinin üyelerine doğrudan erişebilecek kadar yakın tanımlar oldukları düşünülebilir.
Bu sefer de, o modülden yararlanan "okul/okul.d" dosyasına bakalım:
module okul.okul; public import okul.ogrenci; // 1 import std.string; class Okul { private: // 2 Öğrenci[] öğrenciler; size_t kızToplamı; size_t erkekToplamı; public: // 3 void ekle(Öğrenci öğrenci) { öğrenciler ~= öğrenci; final switch (öğrenci.cinsiyet) { // 4a case Cinsiyet.kız: ++kızToplamı; break; case Cinsiyet.erkek: ++erkekToplamı; break; } } override string toString() const { string sonuç = format( "%s kız, %s erkek; toplam %s öğrenci", kızToplamı, erkekToplamı, öğrenciler.length); foreach (i, öğrenci; öğrenciler) { sonuç ~= (i == 0) ? ": " : ", "; sonuç ~= öğrenci.isim; // 4b } return sonuç; } }
- Bu modülü ekleyen programlar
ogrenci
modülünü de ayrıca eklemek zorunda kalmasınlar diye;public
olarak ekleniyor. Bir anlamda, bu "ekleme", genele açılıyor. Okul
sınıfının bütün üyeleri özel olarak işaretleniyor. Bu sayede sınıf nesnelerinin tutarlılığı güvence altına alınmış oluyor.- Bu sınıfın herhangi bir şekilde kullanışlı olabilmesi için üye işlevler sunması gerekir;
ekle
vetoString
işlevleri, kullanılabilmeleri içinpublic
olarak işaretleniyorlar. - Önceki dosyada
package
olarak işaretlendikleri için,Öğrenci
yapısının her iki üyesine de bu modüldeki kodlar tarafından erişilebiliyor.
Son olarak bu iki modülü kullanan program dosyasına da bakalım:
import std.stdio; import okul.okul; void main() { auto öğrenci = Öğrenci("Tolga", Cinsiyet.erkek); writeln(öğrenci); auto okul = new Okul; okul.ekle(Öğrenci("Leyla", Cinsiyet.kız)); okul.ekle(Öğrenci("Metin", Cinsiyet.erkek)); okul.ekle(Öğrenci("Nimet", Cinsiyet.kız)); writeln(okul); }
Bu program, Öğrenci
ve Okul
'u ancak genel erişime açık olan arayüzleri aracılığıyla kullanabilir. Ne Öğrenci
'nin, ne de Okul
'un üyelerine erişemez ve bu yüzden nesneler her zaman için tutarlıdır:
Tolga bir erkek öğrencidir. 2 kız, 1 erkek; toplam 3 öğrenci: Leyla, Metin, Nimet
Dikkat ederseniz, o program bu tanımları yalnızca Okul.ekle
ve Öğrenci.toString
işlevleri aracılığıyla kullanıyor. O işlevlerin kullanımları değiştirilmediği sürece, Öğrenci
'nin ve Okul
'un tanımlarında yapılan hiçbir değişiklik bu programı etkilemez.