16 Nesne Yönelimli Programlama

16 Nesne Yönelimli Programlama

Lua'da bir tablo birden fazla anlamda bir nesnedir. Nesneler gibi tablolar duruma sahiptir. Nesneler gibi tablolar kendi değerlerinden bağımsız bir kimliğe (self) sahiptir; özellikle, aynı değere sahip iki nesne (tablo) farklı nesnelerdir ki bir nesne farklı zamanlarda farklı değerlere sahip olabilir. Nesneler gibi tablolar onları kimin yarattığından veya nerede yaratıldıklarından bağımsız bir yaşam döngüsüne sahiptir.

Nesnelerin kendi işlemleri vardır. Tabloların da işlemleri olabilir:

Hesap = {bakiye = 0}
function Hesap.cekilen(miktar)
    Hesap.bakiye = Hesap.bakiye - miktar
end

Bu tanım yeni bir fonksiyon oluşturur ve Hesap nesnesinin cekilen alanında onu depolar. Velhasıl onu şöyle çağırabiliriz

Hesap.cekilen(100.00)

Bu tür bir fonksiyon hemen hemen metot dediğimiz şeydir. Bununla birlikte fonksiyonun içinde Hesap adlı globalin kullanılması kötü bir programlama uygulamasıdır. İlk olarak, bu fonksiyon yalnızca bu özel nesne için çalışacaktır. İkincisi, bu özel nesne için bile fonksiyon yalnızca nesne bu özel global değişkende depolandığı sürece çalışacaktır. Nesnenin adını değiştirirsek cekilen artık çalışmaz:

a = Hesap; Hesap = nil
a.cekilen(100.00) -- HATA!

Bu tür davranış biraz önce söylediğimiz nesnelerin bağımsız yaşam döngülerine sahip olma ilkesini ihlal eder.

Daha esnek bir yaklaşım, işlemin alıcısı üzerinden işlem yapmaktır. Bunun için metodumuz alıcısının değeri olan ekstra bir parametreye gereksinir. Bu parametre tipik olarak self veya this adına sahiptir:

function Hesap.cekilen(self, miktar)
    self.bakiye = self.bakiye - miktar
end

Şimdi metodu çağırdığımızda üzerinde işlem yapılması gereken nesneyi belirtmeliyiz:

a1 = Hesap; Hesap = nil
...
a1.cekilen(a1, 100.00) -- OK

self parametresi kullanımı ile birden fazla nesne için aynı metodu kullanabilirsiniz:

a2 = {bakiye=0, cekilen = Hesap.cekilen}
...
a2.cekilen(a2, 260.00)

self parametresinin bu kullanımı herhangi bir nesne yönelimli dilde merkezi bir noktadır. Çoğu nesne yönelimli dil programcıdan kısmen gizlenmiş olan bu mekanizmaya sahiptir böylece bu parametreyi deklare etmek zorunda değildir (yine de bir metot içinde this veya self adlarını kullanabilir). Lua da : operatörü kullanılarak bu parametreyi gizleyebilir. Önceki metot tanımlamasını aşağıdaki gibi yeniden yazabiliriz

function Hesap:cekilen(miktar)
    self.bakiye = self.bakiye - miktar
end

ve metot çağrısı

a:cekilen(100.00)

İki nokta üst üstenin etkisi, bir metot tanımlamasında ekstra gizli bir argüman ekleme ve bir metot çağrısında ekstra bir argüman eklemedir. ':' sadece söz dizimsel bir imkandır ama pratikliktir; Burada gerçekten yeni bir şey yok. Ekstra parametreyi doğru olarak kullandığımız sürece  nokta ile bir fonksiyonu tanımlayabilir  ve ':' ile onu çağırabiliriz, veya tersi:

Hesap ={ bakiye = 0,
           cekilen =function(self, miktar)
                        self.bakiye = self.bakiye - miktar
                     end
         }

function Hesap:yatan(miktar)
    self.bakiye = self.bakiye + miktar
end

Hesap.yatan(Hesap, 200.00)
Hesap:cekilen(100.00)






Nesnelerimiz kimliğe, duruma ve bu durum üzerinde işlemlere sahiptir. Hala sınıf sistemi, miras ve gizlilikten yoksundurlar. İlk sorunu çözelim: benzer davranışlara sahip birkaç nesne nasıl oluşturabiliriz? Örneğimiz özelinde, birkaç hesap(Hesap) nasıl oluşturabiliriz?

16.1 Sınıflar


Sınıf, nesnelerin oluşturulması için bir kalıp olarak çalışır. Çeşitli nesne yönelimli diller sınıf kavramını sunar. Bu tür dillerde her nesne belirli bir sınıfın bir örneğidir. Lua sınıf kavramına sahip değildir; her nesne kendi davranışını tanımlar ve kendi şekline sahiptir. Bununla birlikte, Self ve NewtonScript gibi prototip tabanlı dillerin rehberliğini takip edip Lua 'da sınıfları taklit etmek zor değildir. Bu dillerde, nesnelerin sınıfları yoktur. Bunun yerine, her nesne bir prototipe sahip olabilir, ki ilk nesnenin bilmediği herhangi bir işlemi aradığı normal bir nesnedir. Bu tür dillerde bir sınıfı temsil etmek için, diğer nesneler (örnekleri) için bir prototip olarak sadece kullanılacak bir nesne oluştururuz basitçe. Hem sınıflar hem de prototipler, birkaç nesne tarafından paylaşılacak davranışın konulduğu bir mekan olarak çalışır.

Lua'da, bölüm 13.4'te gördüğümüz miras fikrini kullanarak prototipleri uygulamak kolay. Daha spesifik olarak, a ve b olmak üzere iki nesnemiz varsa a için bir b prototipi yapmak için tek yapmamız gereken şu:

setmetatable(a, {__index = b})

Bundan sonra, a, sahip olmadığı herhangi bir işlem için b'ye bakar. b'i a nesnesinin sınıfı olarak görmek terminolojide bir değişiklikten daha fazlası değil.

banka hesabı örneğimize geri dönelim. Hesap'a benzer davranışlara sahip diğer hesapları oluşturmak için bu yeni nesnelerin işlemlerini Hesap'dan miras almasını sağlarız, __index metametodunu kullanarak. Küçük bir optimizasyon, hesap nesnelerinin metatablosu olacak ekstra bir tablo oluşturmamıza gerek kalmaması; Yerine, bu amaç için Hesap tablosunun kendisini kullanıyoruz:

function Hesap:yeni(o)
    o = o or {} -- kullanıcı bir tane sağlamazsa tablo oluştur
    setmetatable(o, self)
    self.__index = self
    return o
end

(Hesap:yeni çağırdığımızda, self Hesap'a eş değerdir; bu nedenle self yerine doğrudan Hesap kullanabilirdik. Bununla birlikte, bir sonraki bölümde sınıf mirasını  ele aldığımızda, self'in kullanımı güzel oturacak.) Bu koddan sonra yeni bir hesap oluşturduğumuzda ve üzerinden bir metot çağırdığımızda ne olur?

a = Hesap:yeni{bakiye = 0}
a:yatan(100.00)

Yeni hesap oluşturduğumuzda a, metatablosu olarak Hesap'a (Hesap:yeni çağrısında self) sahip olacak. Sonra, a:yatan(100.00) çağırdığımızda aslında a.yatan(a,100.00) 'ı çağırıyoruz; iki nokta üst üste sadece sözdizimsel hoşluk. Tabi, Lua a tablosunda "yatan" girişi bulamaz; bu nedenle, metatablonun __index girişine bakar. Durum şimdi az çok şöyle:

getmetatable(a).__index.yatan(a, 100.00)

a'nın metatablosu Hesap ve Hesap.__index 'de Hesap (çünkü yeni metotu self.__index=self yaptı). Bu nedenle önceki ifade şuna kısalır:

Hesap.yatan(a, 100.00)

Yani, Lua orjinal yatan fonksiyonunu çağırır ancak  self parametresi olarak a geçilir. Böylece, yeni hesap a, Hesap'dan yatan fonksiyonunu devraldı. Aynı mekanizma ile Hesap'dan tüm alanları miras alabilir.

Miras sadece metotlar için değil aynı zamanda yeni hesapta bulunmayan diğer alanlar için de çalışır. Bu nedenle bir sınıf yalnızca metotları değil aynı zamanda örneklenen alanları için de varsayılan değerler sağlayabilir. Hatırlayalım, ilk tanımımızda Hesap, 0 değerli bakiye alanını sağladık. Yani, ilk bakiye olmadan yeni bir hesap oluşturursak bu varsayılan değeri miras alacak:

b = Hesap:yeni()
print(b.bakiye) --> 0

b'de yatan metodunu çağırdığımızda devamdaki şu eşdeğeri çalışır:

b.bakiye = b.bakiye + miktar

(self, b olduğu için). b.bakiye ifadesi sıfır olarak ilklenir ve ilk yatan b.bakiye'e atanır. b.bakiye'e sonraki mütakip erişimler  index metametodunu çağırmaz çünkü artık b kendi bakiye alanına sahiptir.

16.2 Miras

Sınıflar nesneler olduğundan, diğer sınıflardan da metotlar alabilirler. Bu davranış, mirası (her zamanki nesne yönelimli anlamında) Lua'da uygulamayı oldukça kolay hale getirir.

Hesap gibi temel-baz bir sınıfımız olduğunu varsayalım:

Hesap = {bakiye = 0}

function Hesap:yeni(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

function Hesap:yatan(miktar)
    self.bakiye = self.bakiye + miktar
end

function Hesap:cekilen(miktar)
    if v > self.bakiye then error "yetersiz bakiye" end
    self.bakiye = self.bakiye - miktar
end

Bu sınıftan, müşterinin bakiyesinden daha fazlasını çekmesine izin veren bir alt sınıf(subclass) AcikHesap elde etmek istiyoruz. Tüm işlemlerini temel sınıfından miras alan boş bir sınıfla başlıyoruz:

AcikHesap = Hesap:yeni()

Şuan AcikHesap sadece bir Hesap örneği. Güzel şey şimdi oluyor:

s = AcikHesap:yeni{limit=1000.00}

AcikHesap başka herhangi bir metot gibi Hesap 'dan yeni'i miras alır. Bununla birlikte bu kez, yeni çalıştırıldığında self parametresi AcikHesap'a referans olacak. Bu nedenle, s'nin metatablosu AcikHesap olacak , __index alanındaki değeri  de AcikHesap. Yani s, Hesap'dan  miras alan AcikHesap'dan miras alır. Devamdakini değerlendirelim :

s:yatan(100.00)

Lua, s'de yatan alanını bulamayacak bu yüzden AcikHesap'a bakar; orada da yatan alanını bulamayacak bu yüzden Hesap 'a bakar ve orada yatan için orijinal uygulamayı bulur.

AcikHesap'ı özel kılan, üst sınıfından(superclass) miras alınan herhangi bir metodunu yeniden tanımlayabiliyor olmamızdır. Tek yapmamız gereken yeni metodu yazmaktır:

function AcikHesap:cekilen(miktar)
    if v - self.bakiye >= self:getLimit() then
        error "yetersiz bakiye"
    end
    self.bakiye = self.bakiye - miktar
end

function AcikHesap:getLimit()
    return self.limit or 0
end

artık, s:cekilen(200.00)'ı çağırdığımızda Lua Hesap'a gitmez çünkü önce AcikHesap'daki yeni cekilen metodunu bulur. s.limit 1000.00 olduğu için (s'i oluşturduğumuzda bu alanı set etmeyi unutmayalım), program s'i eksi bakiyeyle  bırakarak, çekim yapar.

Lua 'da nesnelerin ilginç bir yönü, yeni bir davranış belirtmek için yeni bir sınıf oluşturmanız gerekmez. Yalnızca tek bir nesnenin özgül bir davranışa ihtiyacı varsa bu davranışı doğrudan nesnede uygulayabilirsiniz. Örneğin s hesabının, limitinin daima bakiyesinin %10 olduğu özel bir müşteriyi temsil etmesi isteniyorsa yalnızca bu tek hesabı değiştirebilirsiniz:

function s:getLimit()
    return self.bakiye * 0.10
end

Bu deklarasyondan sonra s:cekilen(200.00) çağrısı AcikHesap'dan  cekilen metodunu çalıştırır ancak cekilen self:getLimit'i çağırdığında bu son tanım çağırılır.

16.3 Çoklu Miras

Nesneler Lua'da temel bir tip olmadığından Lua'da nesne yönelimli programlama için farklı yollar vardır. Gördüğümüz yaklaşım, index metametodu kullanımı, muhtemelen basitlik, performans ve esnekliğin en iyi kombinasyonudur. Bununla birlikte bazı özel durumlar için uygun olabilecek başka uyarlamalar da olabilir. Burada Lua'da çoklu mirası sağlayan alternatif bir uygulama göreceğiz.

Bu uygulamanın anahtarı, __index metaalanı için bir fonksiyonun kullanılmasıdır. Hatırlayalım, bir tablonun metatablosu __index alanında bir fonksiyona sahip olduğunda Lua, orjinal tabloda bir anahtarı bulamadığında bu fonksiyonu çağırır. Sonra __index, ebeveynlerin kaçında eksik anahtar olduğuna bakabilir.

Çoklu miras, bir sınıfın birden fazla üst sınıfa sahip olabileceği anlamına gelir. Bu nedenle, alt sınıflar oluşturmak için bir sınıf metodu kullanamayız. Bunun yerine bu amaç için özgül bir fonksiyon tanımlayacağız , sinifOlustur , yeni sınıfın üstsınıflarını argümanları olarak alan (bkz.Liste 16.1). Bu fonksiyon yeni sınıfı temsil etmek için bir tablo oluşturur ve çoklu miras yapan __index metametodu ile metatablosunu set eder. Çoklu mirasa rağmen, her bir nesne örneği tüm metotlarını arayan tek bir sınıf hüviyetinde. Bu nedenle, sınıflar ve süper sınıflar arasındaki ilişki sınıflar ve örnekler arasındaki ilişkiden farklıdır. Özellikle bir sınıf, örnekleri için ve alt sınıfları için aynı zamanda metatablo olamaz. 16.1 listesinde, örnekleri için metatablo olarak sınıfı tutmak ve sınıf 'ın metatablosu olması için başka bir tablo oluşturuyoz.

sinifOlustur kullanımını küçük bir örnekle gösterelim. Önceki Hesap sınıfına ve sadece iki metotlu ( ismikoy ve ismigetir) başka bir sınıfa(Isimli) sahip olduğumuzu varsayalım:

Isimli = {}
function Isimli:ismigetir()
    return self.isim
end

function Isimli:ismikoy(n)
    self.isim = n
end

Liste 16.1. çoklu kalıtım uygulama:

-- plist tablolar listesinde k'a bak
local function ara(k, plist)
    for i = 1, #plist do
        local v = plist[i][k] -- ’i’-inci superclass'ı dene
        if v then return v end
    end
end

function sinifOlustur(...)
    local c = {} -- yeni sınıf
    local ebeveynler = {...}

    -- sınıf , parents listesinde herbir metodu arayacak
    setmetatable(c,{__index = function(t, k)
        return ara(k, ebeveynler)
    end})

    -- mirasların metatablosu olack ’c’'i hazırla
    c.__index = c

    -- bu sınıf için  yeni oluştucusu(yapıcısı) tanımla
    function c:yeni(o)
        o = o or {}
        setmetatable(o, c)
        return o
    end

    return c -- yeni sınıfı döndür
end

Hesap ve Isimli'nin altsınıfı olan yeni bir IsimliHesap sınıfını oluşturmak için basitçe sinifOlustur'u çağırırız:

IsimliHesap = sinifOlustur(Hesap, Isimli)

Örnekleri oluşturup kullanmak için her zamanki gibi yaparız:

hesap = IsimliHesap:yeni{isim = "Mehmet"}
print(hesap:ismigetir()) --> Mehmet

Şimdi bu son deyimin nasıl çalıştığını izleyelim. Lua, hesap'da “ismigetir” alanını bulamaz; bu nedenle, hesap'ın metatablosunun __index alanını arar, IsimliHesap. Tabi yine IsimliHesap “ismigetir” alanı sağlamaz bu nedenle Lua, IsimliHesap'ın metatablosunun __index alanını arar. Bu alan bir fonksiyon içerdiğinden Lua onu çağırır. Bu fonksiyon sonra ilkin Hesap'da "ismigetir" 'i arar, başarısız olduğunda daha sonra Isimli'de arar; burada aramanın nihai sonucu olan nil olmayan bir değer bulur.

Tabii ki, bu aramanın teferruatlığı nedeniyle çoklu mirasın performansı tekli miras ile aynı değildir. Bu performansı iyileştirmenin basit bir yolu, miras alınan metotları alt sınıflara kopyalamaktır. Bu tekniği kullanarak sınıflar için index metametotu şöyle olacaktır:

setmetatable( c, {__index = function(t, k)
    local v = ara(k, ebeveyn)
    t[k] = v -- sonraki erişim için kaydet
    return v
end})

Bu püf noktayla, miras alınan metotlara erişimler yerel metotlara olan kadar hızlıdır (ilk erişim hariç). Dezavantajı, sistem çalıştıktan sonra metot tanımlarını değiştirmek zordur çünkü bu değişiklikler hiyerarşi zincirine nüfus edemez.

16.4 Gizlilik

Birçok kişi gizliliği nesne yönelimli bir dilin ayrılmaz bir parçası olarak görür; Her nesnenin durumu kendi iç meselesi olmalıdır. C++ ve Java gibi bazı nesne yönelimli dillerde, bir nesne alanı (instance-örnek değişkeni olarak da adlandırılır) veya bir metodun nesnenin dışından görünür olup olmayacağını kontrol edebilirsiniz. Smalltalk gibi diğer diller, tüm değişkenleri özel(private) ve tüm metotları herkese açık(public) yapar.  İlk nesne yönelimli dil Simula, herhangi tür bir koruma sunmazdı.

Lua'da şu ana kadar gördüğümüz nesnelerin ana tasarım, gizlilik mekanizmaları sunmuyor. Kısmen bu, nesneleri temsil etmek için genel bir yapı (tablolar) kullanmamızın bir sonucudur. Ama bu aynı zamanda bazı Lua'nın arkasındaki temel tasarım kararlarını yansıtır. Lua, birçok programcının katıldığı uzun soluklu uğraşlı büyük programlar oluşturmak için tasarlanmamıştır. Tam tersine Lua, tek veya küçük programcı gruplarını veya daha büyük bir sistemin bir parçası olan küçük ve orta ölçekli programları hedeflemektedir. Bu nedenle Lua çok fazlalık ve yapay kısıtlamalardan kaçınır. Bir nesne içindeki bir şeye erişimi istemiyorsanız, sadece bunu yapmazsınız.

Bununla birlikte Lua'nın bir başka amacı, programcının meta-mekanizmalarını birçok farklı mekanizmaya öykünmesine olanak tanıyan, esnekliktir. Lua'daki nesneler için temel tasarım, gizlilik mekanizmaları sunmasa da erişim kontrolü için nesneleri farklı şekilde uygulayabiliriz. Bu uygulama sık kullanılmasa da Lua'nın bazı ilginç kısımlarının keşfinin diğer başka problemlerde çözümün olabileceği hususunda öğreticidir.

Bu alternatif tasarımın ana fikri, her nesneyi iki tablo aracılığıyla temsil etmektir: biri durumu için; diğeri işlemleri veya arabirimi(interface) için. Nesnenin kendisine ikinci tablo yani arabirimini oluşturan işlemler aracılığıyla erişilir. Yetkisiz erişimi önlemek için bir nesnenin durumunu temsil eden tablo diğer tablonun bir alanında tutulmaz; bunun yerine, yalnızca metotların closure'ında tutulur. Örneğin banka hesabımızı bu tasarımla temsil etmek için aşağıdaki fabrika fonksiyonunu çalıştıran yeni nesneler oluşturabiliriz:

function yeniHesap(ilkBakiye)
    local self = {bakiye = ilkBakiye}

    local cekilen =function(miktar)
                        self.bakiye = self.bakiye - miktar
                    end

    local yatan = function(miktar)
                        self.bakiye = self.bakiye + miktar
                    end

    local bakiyeyiGetir =  function() return self.bakiye end

    return {
        cekilen = cekilen,
        yatan = yatan,
        bakiyeyiGetir = bakiyeyiGetir
    }
end

İlkin fonksiyon, içsel nesne durumunu tutmak ve yerel değişken self'de depolamak için bir tablo oluşturur. Ardından fonksiyon, nesnenin metotlarını oluşturur. Son olarak fonksiyon, gerçek metot uygulamalarına metot isimlerini eşleyen harici bir nesne oluşturur ve döndürür. Buradaki önemli nokta, bu metotların ekstra bir argüman olarak self'i almamasıdır;  bunun yerine, doğrudan self'e erişirler. Ekstra argüman olmadığından, bu tür nesneleri manipüle etmek için : sözdizimini kullanmayız. metotlar normal fonksiyonlar gibi çağrılır sadece:

acc1 = yeniHesap(100.00)
acc1.cekilen(40.00)
print(acc1.bakiyeyiGetir()) --> 60

Bu tasarım, self tablosunda saklanan her şeye tam gizlilik verir. yeniHesap döndükten sonra bu tabloya doğrudan erişim elde etmenin bir yolu yok. Sadece yeniHesap içinde oluşturulan fonksiyonlar aracılığıyla erişebiliriz. Örneğimiz, özel(private) tabloya yalnızca bir örnek(instance) değişkeni koysa da bu tabloda bir nesnenin tüm özel bölümlerini depolayabiliriz. aynı şekilde özel metotlar tanımlayabiliriz: public metotlar gibiler ama arayüzüne onları koymayız. Örneğin, hesabımız, belirli bir limitin üzerinde bakiyesi olan kullanıcılar için %10 ekstra bir kredi verebilir ancak kullanıcıların bu hesaplamanın ayrıntılarına erişmesini istemiyoruz. Bu işlevselliği aşağıdaki gibi uygulayabiliriz:

function yeniHesap (ilkBakiye)
    local self = {
     bakiye = ilkBakiye,
     LIM = 10000.00,
   }
   
   local ekstra = function ()
    if self.bakiye > self.LIM then
     return self.bakiye*0.10
    else
     return 0
    end
   end
  
   local bakiyeyiGetir = function ()
    return self.bakiye + ekstra()
   end

   <önceki gibi>


Yine, herhangi bir kullanıcının doğrudan ekstra fonksiyonuna erişmesinin bir yolu yok.

16.5 Tek-Metot Yaklaşımı

Nesne yönelimli programlama için önceki yaklaşımın özel bir durumu, bir nesne tek bir metoda sahip olduğunda oluşur. Bu gibi durumlarda, bir arabirim(interface) tablosu oluşturmamız gerekmez; bunun yerine, bu tek metodu nesne temsili olarak döndürebiliriz. Bu biraz garip geliyorsa, durumu closure'lar olarak tutan iteratör fonksiyonlarının nasıl oluşturulacağını gördüğümüz 7.1 bölümünü hatırlamaya değer. Durumu tutan bir iteratör, tek metotlu bir nesneden başka bir şey değildir.

tek metotlu nesneler için diğer ilginçlik, bu tek metodun aslında görünür argüman temelli farklı görevler gerçekleştiren bir metod sevkedicisi olduğunda ortaya çıkar. Böyle bir nesne için olası bir uygulama aşağıdaki gibidir:

function yeniNesne(deger)
    return function(aksiyon, v)
        if aksiyon == "getir" then
            return deger
        elseif aksiyon == "koy" then
           deger = v
        else
            error("gecersiz aksiyon")        end
    end
end

Kullanımı basittir:

d = yeniNesne(0)
print(d("getir")) --> 0
d("koy", 10)
print(d("getir")) --> 10

Nesneler için bu alışılmamış uygulama oldukça etkilidir. d("koy",10) sözdizimi tuhaf olmasına rağmen geleneksel d:koy(10) ' dan sadece iki karakter daha uzundur. Her nesne, bir tablodan daha masrafsız olan bir tek closure kullanır. Miras yok ancak tam gizliliğimiz var: bir nesne durumuna erişmenin tek yolu onun tek metodu.

Tcl/Tk, widget'ları için benzer bir yaklaşım kullanır. Tk'deki bir widget'ın adı widget üzerinde her türlü işlemi gerçekleştirebilen bir fonksiyonu(bir widget komutu) gösterir.

Hiç yorum yok:

Yorum Gönder