C# ile OOP (Nesne Tabanlı Programlama) Felsefesi
Nesne tabanlı programlama (ing. object oriented programming, kısaca OOP) bir problem çözme, düşünme ve tasarlama biçimi olarak değerlendirilebilir. OOP, programlama diline ait bir özellik değildir. Her ne kadar programlama dillerinin üzerinde bir kavram olsa da her programlama dili size farklı araçlar sunmuş olabilir. Bu yazıda OOP prensiplerini ve uygulamalarını C# ile örnekler yaparak sizlere anlatmaya çalışacağım.
Hayaliniz bu yazının sonunda bir “OOP-master” 🥷 olmak ise baştan o hayalleri söndüreyim. Bu yazı size OOP ile ilgili ilkeleri, kullanabileceğiniz araçları ve düşünme şeklini göstermeyi amaçlıyor. OOP ile yazılım üretme konusunda kendinizi geliştirmek ve iyi bir yere gelmek ise tamamen sizin elinizde. Her zaman olduğu gibi bu konuda da bol pratik ve tekrar ile zaman içerisinde istediğiniz noktaya gelebilirsiniz.
Başlamadan Önce
Bu uzun yazıya başlamadan önce değinmek istediğim üç konu var.
Birincisi; bu yazı kimler için? Bu yazı en azından temel düzeyde programlama bilen ve kendisini OOP konusunda geliştirmek isteyen kişiler için uygundur. Yazı içerisinde programlama, veri tipleri, değişkenler, fonksiyonlar, vb. gibi temel konulardan bahsetmeyeceğiz. Bu noktalarda eksikleriniz olduğunu düşünüyorsanız temel programlama bilginizi geliştirdikten sonra bu yazıyı okumanız sizin için daha anlamlı ve faydalı olacaktır diye düşünüyorum. Programlamanın temelleri ile ilgili internette pek çok ücretli ve ücretsiz kaynak bulabilirsiniz. İstediğinizi tercih edebileceğiniz gibi hazırlamış olduğum ücretsiz Udemy eğitimine de bir göz atabilirsiniz.
İkincisi; her ne kadar Türkçe bir içerik hazırlıyor olsam da özellikle teknik terimler için daha çok İngilizce kullanmayı tercih ediyorum. Çünkü bazı terimleri Türkçeye çevirdiğimizde anlam düşüklüğü olabiliyor. İngilizce iki farklı terimin Türkçesi aynı olabiliyor, daha da kötüsü anlamı tamamen kaybolabiliyor. O yüzden şimdiden kullandığım plaza Türkçesi için kusura bakmayın 👨💻
Son olarak da bu yazının içeriğine gelelim. Uzun bir yazı olacağı için anlatmaya başlamadan önce bu yazıda hangi konu ve kavramların yer aldığını baştan belirtmek istiyorum.
- Fonksiyonel Programlama ve Nesne Tabanlı Programlama Arasındaki Fark Nedir?
- OOP Olmadan Yazılım Geliştirilemez Mi?
sınıf (class), veri (alan/field), metot/fonksiyon (method/function) - Kapsülleme (Encapsulation) ve Veri Gizleme (Data Hiding) Nedir?
encapsulation, data hiding - Sınıf (Class) Kavramı Nedir? Nasıl Kullanılır? Nasıl tanımlanır?
class - Erişim Belirleyiciler (Access Modifiers) Nedir? Nasıl Kullanılır? Nasıl Tanımlanır?
public, private, protected, internal keywords - Yapıcı ve Yıkıcı Metotlar (Constructors and Destructors) Nedir? Ne İşe Yarar? Nasıl Kullanılır?
default constructor, constructor with parameter (parametreli constructor), copy constructor, destructor - const readonly static Kavramları Nelerdir? Nasıl Kullanılır?
const, readonly, static keywords - Özellik (Property) Nedir? Nasıl Kullanılır?
property, getter method, setter method - Operator Overloading (İşleç Aşırı Yüklemesi) Nedir? Ne İşe Yarar? Nasıl Kullanılır?
C# operators - Inheritance (kalıtım) Nedir? Nasıl Kullanılır?
is a relationship, base class, derived class, sealed keyword - Polymorphism (Çok biçimlilik) Nedir? Nasıl Kullanılır?
virtual, override, abstract keywords - Interface Nedir? Nasıl Kullanılır?
interface, abstraction - Enum Nedir? Nasıl Kullanılır?
C# enums - Exceptions (İstisnalar)
try, catch, finally, throw keywords - Generics (Genel Türler)
- Son Sözler
- OOP İçin Kullanabileceğiniz Kaynaklar
Fonksiyonel Programlama ve Nesne Tabanlı Programlama Arasındaki Fark Nedir?
Fonksiyonel programlama, modellemenin fonksiyonlarla yönetildiği bir yaklaşımdır. Fonksiyonlar uygulama veya bir veri ile ilgili durum değişimlerini kendi içerisinde tutmazlar (stateless). Kendisine gönderilen verileri işleyip sonuçları çağıran yere geri dönerler. İşlediği verilerle ilgili bir bilgi fonksiyon içerisinde yer almaz. Fonksiyonel programlama, operasyonun yoğun verilerin ise zayıf olduğu program modelleri için daha uygun bir yaklaşımdır.
Nesne tabanlı programlama (OOP) ise modellemenin gerçek dünya nesnelerinden esinlenilerek yapıldığı bir yaklaşımdır. Bu yaklaşımda veriler ve fonksiyonlar nesne adı verilen yapılarda bir arada bulunur (stateful). Nesne tabanlı programlama (OOP), operasyonun zayıf verinin ise yoğun olduğu program modelleri için daha uygun bir yaklaşımdır.
Zaman zaman OOP için daha iyi veya daha gelişmiş bir yöntem olduğuna dair yazılara/yorumlara denk geliyorum. Bu yöntemleri daha iyi veya kötü diye ayırmak doğru değil, ihtiyaçlarımıza göre birini veya diğerini tercih edebiliriz diyerek buraya noktayı koyalım ve ilk sorumuzu sorarak hikayemize başlayalım.
OOP Olmadan Yazılım Geliştirilemez Mi?
Gerçek dünya nesnelerden oluşmaktadır. Yazılımsal problemler de nesnelerin etkileşime geçmesi ile açıklanabilir. Problemlerin çözümünü bir yazılıma anlatmak/aktarmak için de nesnelerden yararlanmak ve nesne tabanlı düşünmek tasarım ve bakım sürecimizi kolaylaştıracaktır.
Madem havalı bir giriş yaptık, bir filozof edasıyla devam edelim. Problemleri OOP ile çözmek için yazılımcı olarak kendimize şu soruyu sormalıyız: Karşımdaki bu problemi hangi nesnelere nasıl bölebilirim?
Bunun için öncelikle karşımızdaki problemi çok iyi anlamalıyız. Problemimizi iyice anlayıp kafamızda bir çözüm ürettikten sonra bu çözümde hangi nesneler olduğunu tespit edebiliriz. Nesneler belli olduktan sonra da içerisinde nelerin olması gerektiğini çıkarabiliriz.
Gerçek dünyadaki bir nesne temel olarak iki bölümden oluşur; özellikler (attributes) ve davranışlar (behaviors).
Örnekler
--------
Nesne : uçak ✈️
Özellikler : kanat uzunluğu, yolcu kapasitesi, motor gücü, vb.
Davranışlar : kalkış yapma, iniş yapma, otomatik pilota alma, vb.Nesne : bilgisayar 🖥
Özellikler : ekran boyutu, ram, işlemci, vb.
Davranışlar : bilgisayarı açma, yeniden başlatma, vb.Nesne : kuş 🐦
Özellikler : tüy rengi, kanat genişliği, gaga büyüklüğü, vb.
Davranışlar : uçmak, kanat çırpmak, ötmek, vb.
Siz de birkaç nesne üzerinde özellikleri ve davranışları çıkararak pratik yapabilirsiniz.
Yazılım dünyasındaki nesnelerin modellerine sınıf (class), özelliklere veri/alan (data/field), davranışlara ise metot/fonksiyon (method/function) diyoruz.
Örnek: Araçlarla ilgili bir uygulama tasarlamamız gerektiğini düşünelim. Bu araç uygulaması için beş farklı araç türümüz olsun. Bunlar araba, tren, uçak, motosiklet ve bisiklet. OOP yaklaşımına göre her bir araç türü sınıf olarak düşünülmelidir. Bunun yanında araç türlerine göre farklı özellikler ve fonksiyonlar olabildiği için özellikler field, davranışlar da fonksiyon olacak şekilde aşağıdaki gibi bir tasarım ortaya çıkmaktadır.
Kapsülleme (Encapsulation) ve Veri Gizleme (Data Hiding) Nedir?
Nesnelerin, kendisine ait veri ve fonksiyonlarla bir bütün halinde olmasına kapsülleme (encapsulation) adı verilmektedir. Genel yaklaşım olarak bir nesne, kendi verilerine dışarıdan doğrudan erişime izin vermeme eğiliminde olmalıdır. Bunun yerine fonksiyon setleri sunarak verilerine erişim imkanı sunarak kontrollü olarak verilerin değiştirilmesini veya okunmasını sağlar, buna da veri gizleme (data hiding) adı verilmektedir. Encapsulation ve data hiding OOP yaklaşımının temelini oluşturan iki önemli konudur.
Örnek: Otomatik vites bir arabamız olduğunu düşünelim. Şimdi arabamızın çok basit bir modelini oluşturacağız.
Verilerimiz: vites, hız
Fonksiyonlarımız: gaza basmak, frene basmak, vitesi göstermek, hızı göstermek
Araba modelimize (model=sınıf adı) Car
ismini verelim. Modelimizde verilerimiz için sırasıyla iki tane integer field (gear, speed
) kullanacağız. Fonksiyonlarımız ise sırasıyla SpeedUp()
, SlowDown()
ve PrintGear()
olacak. Modelimizi oluşturduktan sonra bu modelimizden istediğimiz sayıda araba örneği (ing. instance) üretip bunları uygulamamızda kullanabiliriz.
Örneğimize bakacak olursak, Car
sınıfındaki gear
ve speed
isimli iki field’a doğrudan erişmiyoruz. Bunun yerine PrintGear()
, SlowDown()
ve SpeedUp()
fonksiyonlarına erişerek dolaylı olarak verileri değiştiriyoruz veya okuyoruz.
Sınıf (Class) Kavramı Nedir? Nasıl Kullanılır? Nasıl tanımlanır?
Sınıf kavramından çok kısa bahsetmiştik, şimdi biraz detaylandıralım.
OOP için sınıf, nesneleri tanımlamada/modellemede kullandığımız bir veri tipidir. Sınıflar nesneler için bir şablon görevi görmektedirler. Şunu unutmamak gerekir; bir sınıf tanımı yapmak nesneler üretmemizi sağlamaz. Sınıflar nesne üretmek için kullanacağımız bir araçtır. Yani, sınıflarımızı yazarken bu sınıflardan oluşturacağımız nesnelerde yer alacak verileri ve fonksiyonları belirtiyoruz. Somutlaştırmak adına Car ismini verdiğimiz sınıfın yazılımsal modelini oluşturalım.
Örnekte de görülebileceği gibi bir sınıfı class SınıfAdı
ile tanımladıktan sonra {}
içerisinde, bu sınıfta yer almasını istediğimiz tüm verileri ve fonksiyonları tanımlayabiliyoruz. Örneğimizde dikkatinizi iki noktaya çekmek istiyorum. Birincisi; modelimizi tasarlamaya başlarken bahsetmediğimiz ChangeGear()
isimli bir fonksiyonumuz daha var. İkincisi ise fonksiyonları tanımlarken public
ve private
şeklinde bazı ön ekler kullanmışız. Bunlar da bizi başka bir konuya götürüyor.
Sınıf (Class) Üyelerine Nasıl Erişilir?
Bir uygulama geliştirirken bir yazılımcı olarak kendinizi iki noktada bulabilirsiniz, bir sınıfın geliştiricisi veya kullanıcısı.
Eğer bir sınıf geliştiriyorsanız, bu sınıfı kullanacak yazılımcıların ihtiyaç duyabileceği tüm veri ve fonksiyonları düşünerek bir tasarım yapmalı ve sınıfı kullanacak kişilerin doğrudan ihtiyaç duymayacağı tüm veri ve fonksiyonların dışarıdan erişimini engellemelisiniz.
Eğer bir sınıfı kullanıyorsanız, bu sınıfın uygulamanız için ihtiyaç duyacağınız tüm fonksiyonları sağladığından emin olmanız gerekmektedir.
Örnek: Bizim araba sınıfımız bir otomatik vites arabayı temsil ediyordu. Bu otomatik vites aracı üreten firma sınıf geliştiricisi, aracı satıp kullanan bizler ise sınıf kullanıcısı olalım.
Firma, aracı üretirken aracın o anki hızını ve hangi viteste olduğunu aracın içerisinde saklıyor. Biz kullanıcılar olarak aracın hızını veya o anki vitesi doğrudan değiştiremiyoruz. Bunun yerine üretici firma gaz ve fren pedalları sayesinde aracın hızını dolaylı olarak değiştirmemize imkan veriyor. Fakat buna rağmen aracın vitesini değiştirmemize izin vermiyor. Aracın hızı, devri, vb. bilgileri toplayarak içeride kendisi aracın hangi viteste olacağına karar veriyor. Bize de sadece o an aracın kaçıncı viteste olduğunu görme imkanı sağlıyor. Bu sayede araç dördüncü viteste giderken yanlışlıkla birinci vitese almamızı engellemiş yani aracı korumuş oluyor. Biz kullanıcı olarak sadece gaz ve frene müdahale ederek aracın üzerindeki hız ve vites verilerini dolaylı olarak değiştiriyoruz.
Erişim Belirleyiciler (Access Modifiers) Nedir? Nasıl Kullanılır? Nasıl Tanımlanır?
OOP yönteminde sınıf üyelerinin erişilebilirliğini yönetmek için erişim belirleyici (access modifier) adı verilen bazı anahtar kelimelerden yararlanıyoruz. Bunlar public, private, protected ve internal.
private
olarak tanımlanmış sınıf üyeleri sadece o sınıfın içerisinden erişilebilirken public
olarak tanımlanmış üyeler dışarıdan da erişime açıktır. protected
anahtar kelimelerinin ne işe yaradığına sonra geleceğiz. internal
ise bu yazının konusu olmayacak.
Buradan yola çıkarak Car sınıfımızı modellerken dışarıdan erişilebilir olan fonksiyonlarımızın başına public
anahtar kelimesini ekledik. Aracımız otomatik vites olduğu için vites değiştirilmesine doğrudan izin vermek istemiyorduk. Bu nedenle ChangeGear()
metodumuzu private
olarak tanımladık. Field’larımıza baktığımızda ise public
veya private
gibi bir erişim belirleyici atamadığımızı görüyoruz.
Önemli! C#’ta bir sınıf üyesini tanımlarken erişim seviyesini yazmazsak private olduğu anlamına gelir.
Bu da demek oluyor ki önüne private
yazmasak dagear
ve speed
field’larına dışarıdan erişim imkanı bulunmamaktadır. Hemen uygulamamızda deneyerek görebiliriz.
Yapıcı ve Yıkıcı Metotlar (Constructors and Destructors) Nedir? Ne İşe Yarar? Nasıl Kullanılır?
Constructors (Yapıcı Metotlar)
Bir sınıftan nesne üretmek için ingilizce constructor adı verilen özel yapıcı metotlardan yararlanmaktayız. Yapıcı metotlar biz sınıf içerisinde tanımlasak da tanımlamasak da yer alırlar ve bir sınıftan nesne (diğer isimleri; örnek, instance) üretilirken otomatik olarak çağrılırlar.
Yapıcı metotlar bir nesnenin hayatına başlarken ihtiyaç duyabileceği her şeyi ayarlamak için kullanılır. Buna en klasik örnek sınıftaki verilerin başlangıç değerlerini belirlemek olacaktır. Bunun dışında bir dosya üzerinde işlem yapılacaksa bu dosyayı açmak veya bir sunucu üzerinde işlemler yapacaksa bu sunucunun bağlantı işlemlerini tamamlamak da yine örnek olarak verilebilir.
Önemli! Yapıcı metotlar parametresiz olabileceği gibi ihtiyaç duyduğu kadar parametre de alabilir. Ancak yapıcı metotlar normal metotlar gibi geriye bir değer dönmez ve bir dönüş tipi de yoktur (yani void de yazmıyoruz). Yapıcı metotların ismi sınıf ismi ile aynı olmak zorundadır.
Hemen Car sınıfımıza bir default constructor ekleyelim. Bu constructor ile sınıftan bir nesne üretilirken gear ve speed alanlarının ilk değerinin sıfır olmasını sağlayalım.
Car sınıfımıza constructor’ı ekledikten sonra bu sınıfın kullanımıyla ilgili hayatımızda herhangi bir değişiklik olmadı.
Şimdi de Car sınıfına default constructor yerine parametreli bir constructor yazalım. Bunun için Car sınıfıma yeni bir özellik daha ekleyeceğim. Artık Car sınıfımda hız ve vites dışında arabanın rengi de tutulsun ve renk bilgisi constructor’a parametre olarak gelsin.
Car sınıfındaki default constructor’ı parametreli bir constructor ile değiştirdik. Default constructor ortadan kalktığı için artık ana uygulamamızda new Car();
diyerek Car sınıfından bir nesne üretmemiz mümkün değil, bunun yerine parametreli constructor’ı tetiklememiz gerekiyor. Bunun için ana uygulamada aşağıdaki şekilde bir değişiklik yapıyor ve bir kırmızı araba üretiyoruz.
Peki ya ana uygulamamızda bazen renk verip bazen vermeden araba üretmek istersek? O zaman birden fazla constructor’a ihtiyacımız olacak. Bunun için Car sınıfımıza eklediğimiz parametreli constructor’a dokunmadan bir tane de default constructor ekliyoruz. Ek olarak Car sınıfımıza aracın rengini ekrana yazdıran bir PrintColor()
metodu da ekleyelim.
Artık iki farklı constructor olduğu için istersek renk belirtmeyerek standart renkte veya rengini bizim belirlediğimiz bir araba üretebiliriz. Hemen iki constructor’ı da kullanalım.
Constructor’lar ile ilgili son olarak parametreli constructor’ın özel bir türü olan copy constructor’dan bahsedelim. Copy constructor, bir nesnenin kopyasını oluşturmamızı sağlar. Yani öncelikle kendimize bir nesne üretiyoruz, belki bu nesnenin bazı özelliklerini değiştirip özelleştiriyoruz ve bu özelleşmiş halinden bir tane kopya elde etmek istiyoruz. Bu imkanı sağlamak için sınıfa bir copy constructor tanımlanabilir.
Yukarıdaki copy constructor’a baktığımızda parametre olarak Car sınıfından bir nesne alıyor ve aldığı nesnenin gear, speed ve color özelliklerinin o anki halini yeni oluşan nesneye atıyor.
Yukarıda cloneCar
nesnesini, car
nesnesinin o anki halini baz alarak oluşturduk ve artık elimizde speed = 2
ve color = "red"
olan iki bağımsız nesne oldu. Sonrasında car
ve cloneCar
bağımsız şekilde verilerini yönetmeye başladılar.
Sanırım nesne üretmek için kullandığımız yapıcı (constructor) metotlar hakkında yeterince konuştuk. Şimdi sıra geldi yıkıcı (destructor) metotlara.
Destructors (yıkıcı metotlar)
Destructor da tıpkı constructor gibi otomatik olarak çağrılmaktadır. Nasıl constructor metotlar nesne oluştururken çağrılıyorsa destructor metotlar da nesne bellekten silinirken çağrılıyor (Bunun nasıl olduğu çok başka ve detaylı bir konu).
Yine constructor metotlarda olduğu gibi destructor metotlar da sınıf ismiyle aynı isimde olmak zorunda ancak bir farkla, destructor metodun isminin başına tilda (~) işareti ekliyoruz yine bir dönüş değerleri olmuyor ve parametre alamıyorlar. Parametre almadıkları için bir sınıfa ait birden fazla destructor metodu tanımlama şansımız da yok.
Ayrıca sınıflara destructor metot yazmak zorunda değiliz, tıpkı hiç constructor yazmadığımızda default constructor çağrıldığı gibi destructor da arka planda kendiliğinden çağrılıyor.
Destructor ne zaman kullanılır?
Açıkçası C# gibi bir dilde uygulama geliştiriyorsanız cevap neredeyse asla. Destructor metotlar C++ gibi bellek yönetiminin sizin elinizde olduğu dillerde oldukça önemli ve yazılması neredeyse zorunludur. Aksi durumda belleği sizin erişemediğiniz, bir işinize de yaramayan bir sürü çöp veriyle doldurmanız mümkün. Yine de OOP konuşurken destructor’ı atlamak olmaz diye düşünerek sizinle paylaşmak istedim.
const readonly static Kavramları Nelerdir? Nasıl Kullanılır?
OOP kullanımında sınıf üyelerimizi tanımlarken kullanabileceğimiz üç güzel anahtar kelimeden bahsedeceğiz.
const
İngilizce constant kelimesinin kısaltmasıdır, sabit anlamına gelir. const, bir değişkeni tanımlarken atanan değerin uygulamanın hiçbir yerinde değiştirilmemesini sağlayan anahtar kelimedir. Şu şekilde tanımlanabilir;
Bilgi! constant alanlar tanımlanırken isimlendirmeyi büyük harf kullanarak yapmak genel kabul görmüş bir kullanımdır.
Bir sınıfa const kullanarak bir özellik tanımladığımızda, sınıf içerisinden veya sınıf dışarısından değiştirilmesi mümkün değildir. Bu nedenle aşağıdaki gibi bir atama yapılamaz. Sadece okuma amaçlı kullanılabilir.
const ne zaman kullanabiliriz?
Uygulama içerisinde zamanla hiç değişmeyeceğini bildiğimiz değerler için const
kullanabiliriz. Hemen bunu bir örnekle destekleyelim. Car
sınıfında maxSpeed
değerinin constructor’da sabit olarak 100
olarak atıldığını görüyoruz. Yani ürettiğimiz araçların hızı ne yaparsak yapalım 100'ün üstüne çıkamıyor. Bu yüzden maxSpeed
const
olarak tanımlanabilir. Hatta bunu const
yaparak herhangi bir şekilde maxSpeed
i değiştirilemez yapmak daha doğru olacak.
readonly
Öte yandan readonly, sınıf içerisindeki bir alana sadece nesne oluşturulurken değer ataması yapılmasına izin verir. Constructor çalıştıktan sonra nesne üzerindeki readonly alanlar const gibi çalışır.
Yukarıdaki birinci alternatif const gibi (ama aynı değil!) çalışır, değeri sabit olarak atarız ve bir daha değiştirilemez. İkinci alternatif de aslında birinci gibi sadece değer atamasını constructor içerisinde yapıyoruz, büyük bir fark yok. Ancak üçüncü alternatif bize CompanyName
bilgisini dışarıdan alma ve atama imkanı sunuyor. Bu sayede dışarıdan gönderilen bir bilginin sınıf içerisinde tutulmasını ve bir daha hiç değiştirilmemesini sağlamış oluyoruz.
readonly ne zaman kullanabiliriz?
Kullanım alanını doğrudan bir örnekle göstermek sanırım daha anlaşılır olacak.
Örnek: Uygulamamızda araçların rengini sonradan değiştiremediğimizi düşünelim. Bu durumda Car sınıfından bir nesne üretirken renk ataması yapıp sonradan sınıf içerisinden veya dışarısından bu bilgiyi değiştiremeyeceğimiz bir kurgu yapmamız gerekiyor.
Sadece color
’ı tanımlarken readonly
eklememiz yeterli oldu. readonly yazmadan önce de color zaten private olduğu için sınıf dışarısından değiştirilemiyordu. Ancak Car sınıfına SetColor(string newColor)
gibi bir fonksiyon yazarak araç renginin dışarıdan da içeriden de değiştirilmesine imkan verebilirdik. readonly kullanarak bunun da önüne geçtik. Artık bilerek veya yanlışlıkla bir arabanın rengi sonradan değiştirilemez.
static
Normal şartlar altında bir sınıftan üretilen her nesne, o sınıftaki tüm veri alanlarından bir tane kendi üzerinde tutar ve bu sayede her nesnenin verisi birbirinden bağımsızdır. Sınıf seviyesinde bazı bilgilerin ortak olmasını, yani bir sınıftan üretilmiş tüm nesnelerin aynı veriye erişmelerini isteyebiliriz. İşte tam bu noktada sahneye static çıkıyor. static olarak tanımlanan değişkenler bir sınıfın tüm nesneleri arasında ortak olarak paylaşılmaktadır. Ancak const ve readonly’de gördüğümüz gibi bir değer atama kısıtı yoktur. Yani istediğimiz zaman değeri değiştirilebilir.
static ne zaman kullanabiliriz?
Bir field’ın ilgili sınıftan oluşturulan tüm nesneler arasında paylaşılmasını istediğimiz durumlar için kullanışlıdır. Ne demek istediğimi tam olarak anlayamamış olabilirsiniz çok normal :) Hemen bir örnekle durumu netleştirmeye çalışalım.
Örnek: Car sınıfı kullanılarak üretilen toplam Car nesnelerinin sayısını tutmak istiyoruz. Bu sayı Car sınıfının dışından değiştirilemesin ve Car sınıfının constructor metodu her çağrıldığında üretilen nesne sayısı bir artsın. Ana uygulamamızda ise en son kaç adet nesne üretildiği bilgisini Car sınıfından okuyabilelim.
Çözüm olarak sınıfımıza createdCarCount
isminde bir static alan ekledik. Dışarıdan değiştirilememesi için private olarak tanımladık (hatırlayın, zaten default erişim yetkisi private olduğu için özellikle yazmamıza gerek yoktu). Car sınıfını kullanan biri istediği zaman kaç adet araç üretildiğini görsün diye GetCreatedCarCount()
isimli bir fonksiyon ekledik. Son olarak createdCarCount
alanını yazmış olduğumuz üç constructor içinde de bir arttırdık ki bir şekilde yeni Car nesnesi üretildiğinde üretilen araç sayısı artsın.
Car sınıfımız son halini aldı ve artık kullanıma hazır. Ana programımızda da bir takım değişiklikler yaparak testimizi gerçekleştirelim.
Uygulamamız car
ve cloneCar
nesnelerinin gear
, speed
ve color
verilerini saklamak için bellekte ayrı ayrı yer aldı. Ama createdCarCount
iki nesne için de ortak bir bellek alanında duruyor ve bu sayede ortak şekilde kullanılabiliyor.
static alanlar nesnelerden bağımsız, sınıf seviyesinde yer alır demiştik. Bu da bize bir sınıftan hiç nesne üretmeden static değerlere ulaşma imkanı vermektedir. Bir sınıftan nesne oluşturmadan SınıfAdı.StaticDeğişkenAdı
şeklinde kullanarak içerisindeki değere erişebiliriz. Tabi static değişkenin aynı zamanda public (yani dışarıdan erişime açık) olması gerekmektedir. Aksi durumda yetkimiz olmadığı için bu değere erişemeyiz.
Biz de Car sınıfına static değişkenimizi private
ile tanımladığımız için dışarıdan erişilmesine izin vermiyoruz. Yani Car.createdCarCount
diyerek bir nesne üretmeden bu bilgiye erişme şansımız yok. Üretilen araba sayısını görmek için GetCreatedCarCount()
metodunu kullanmıştık. Peki bir nesne üretmeden değişkenlere ulaşamıyorum ama bu metoda ulaşmayı denesem nasıl olur? Bu metot public
olduğu için bir şekilde nesne üretmeden metoda erişmenin yolunu bulabilirsek üretilen araba sayısına da erişmiş olacağız.
Bunun için Car.GetCreatedCarCount()
yazıp kodumuzu çalıştırmayı deneyebilir ve daha derlenirken hata aldığını görebiliriz. Çünkü tıpkı sınıfa ait verilerde olduğu gibi metotlara da bir nesne üzerinden erişilmektedir. Eğer bir metoda nesne üzerinden değil de doğrudan sınıf üzerinden erişmek istiyorsak bu metodu da static yapmamız gerekmektedir. Car sınıfımızda yer alan GetCreatedCarCount()
metodumuzu static olarak değiştirelim.
Artık Car.GetCreatedCarCount()
diyerek hiç nesne üretmeden metodumuza erişebilir hale geldik. Kodumuzu bu haliyle çalıştırmayı denediğimizde bu sefer de Car sınıfından nesneler üreten ana sınıfımız yani Program sınıfında bazı hataların olduğunu göreceğiz. Çünkü, bir sınıf üyesi (field veya function olabilir fark etmez) static yapılırsa artık bir nesne üzerinden çağrılamaz.
Bu nedenle Program
sınıfımızda Car
sınıfından üretilen nesneler üzerinden yaptığımız GetCreatedCarCount()
fonksiyon çağrılarını değiştirip direk Car
sınıfı üzerinden yapacak şekle getiriyoruz.
Özellik (Property) Nedir? Nasıl Kullanılır?
Encapsulation bize bir sınıftaki verilere doğrudan erişmek yerine bir fonksiyon üzerinden kontrollü şekilde erişmemizi söylemişti. C# dilinde property, encapsulation için kullanabileceğimiz veri ve fonksiyonu içerisinde barındırabilen yapılardır. Car sınıfımıza property ekleyerek kendisini yakından tanıyalım.
Örnek: Arabamıza istediğimiz zaman sunroof ekleyip çıkarabilmek istiyoruz. Bunun için Car sınıfında arabanın sunroof’u var mı yok mu diye bir bilgi tutacağız ve bu bilginin property üzerinden değiştirilmesine izin vereceğiz.
Birinci yöntemde hasSunroof
isimli bir alan ekledik, bu alan private
olduğu için sınıf dışından erişilebilir değil. Aynı zamanda HasSunroof
isimli bir property ekleyerek hasSunroof
alanına erişimi mümkün kıldık. Program.cs
içerisinde de bir Car nesnesi ürettik. Console.WriteLine
ile ekrana bu değeri yazdırırken bir atama (yani değer değiştirme) yapmadığımız için property’mizin sadece get metodu tetiklendi ve private olan hasSunroof
değişkenin içindeki değeri ekrana yazdık. Sonrasında property üzerinden bir değer ataması yaptık, bu nedenle property’nin set metodu tetiklendi. set metodunda da gelen değeri private olan hasSunroof
değişkenine atadık ve tekrar property kullanarak ekrana değer yazdırmak istediğimizde bu sefer false yerine true yazdığını gördük. Çünkü property’nin get metodu az önce değiştirdiğimiz private hasSunroof
değişkeninin içindeki değeri bize dönüyor.
İkinci yöntem ise aslında birinci yöntemin tamamen aynısı! C# bize aynı işlevi daha az kod yazarak yapmamızı sağlamak için böyle bir imkan sunmuş. Hiç private bir değişken tanımlamadan, get/set metotlarının içini doldurmadan tek satır yazarak arka planda aynı şeyi yapmasını sağlayabiliyoruz.
Bir property’de get/set metotlarının ikisi de olabileceği gibi sadece get veya set metodundan da oluşabilir. Örneğin, bizim Car sınıfımızda vites (gear) alanını dışarıdan değiştiremiyorduk ama dışarıya bu bilgiyi okuması için verebiliriz. Şimdiye kadar bunu PrintGear()
metodu yardımıyla ekrana yazarak yapıyorduk, şimdi property ile yapalım.
Car sınıfından PrintGear()
fonksiyonunu sildik, bunun yerine bir tane Gear
isimli property ekledik ve buna sadece get metodu yazdık. get metodumuzun içinde de private olan gear
değerini dönüyoruz. Bu sayede Program
sınıfından carObject.Gear
diyerek vites bilgisini okuyabiliyouz ama carObject.Gear = 5
diyerek vitesi değiştiremiyoruz, çünkü Gear property’sinde bir set metodu yok.
Son olarak property’de access modifier (erişim belirleyici) kullanımına bakalım. Car sınıfımızda private olarak tuttuğumuz bir speed
değeri vardı. Bu veri SpeedUp()
ve SlowDown()
metotları üzerinden değiştirilebiliyordu. Sınıfımızdaki bu metotlar yine dursun, biz sadece speed
yerine bir property tanımlayıp private değişkenini ortadan kaldıralım (HasSunroof örneğindeki 2. yöntemde olduğu gibi).
Sınıfımıza Speed isimli yeni bir property tanımladık ve speed değişkenimizi sınıftan kaldırdık. Ve Car sınıfındaki speed değişkenini kullanan yerleri Speed property’sini kullanacak şekilde değiştirdik. Artık Speed sınıf dışından da okunabilir hale geldi, Program sınıfı üzerinden bir Car nesnesi üretip aracın o anki hızını ekrana yazabiliriz.
Buraya kadar her şey harika! Ama atladığımız şöyle bir nokta var, Speed property’sinde hem get hem set metodu olduğu için Program sınıfından aracın hızına doğrudan müdahale edebilir hale geldik. car.Speed = 300000;
yazarak aracın bir anda ışık hızına çıkmasını sağlayabiliriz. Verilere doğrudan erişilmesini istemediğimiz için bir şekilde bu problemi gidermemiz gerekiyor.
Speed
’in değiştirilememesi için tıpkı Gear
(vites) property’sinde yaptığımız gibi set metodunu kaldırabiliriz. Bu Speed
’in dışarıdan değiştirilmesini engelleyecektir. Ancak bunu denediğinizde göreceksiniz ki set metodunu tamamen kaldırdığımızda sadece dışarıdan değil sınıf içerisinden de bu değeri değiştiremez hale geleceğiz. Yani SpeedUp()
ve SlowDown()
metotlarımız da artık Speed
’i değiştiremeyecek 😭
Demek ki bizim ihtiyacımız olan şey get metoduna heryerden, set metoduna ise sadece sınıf içerisinden erişilebilen türden bir property yazmak. İşte tam bu noktada access modifier konusu tekrar devreye giriyor. İhtiyaç duyduğumuzda bir property’nin get veya set metodunu private yaparak dışarıdan erişilemez olmasını sağlayabiliyoruz. Biz de burada Speed
property’sinin set metodunu aşağıdaki şekilde değiştirerek dışarıdan erişilemez hale getirebiliriz.
Ve istediğimiz şeyi başardık, artık Speed
’i Program
sınıfı üzerinden okuyabiliriz ama bunu değiştirmek istediğimizde bize hata verecektir.
Operator Overloading (İşleç Aşırı Yüklemesi) Nedir? Ne İşe Yarar? Nasıl Kullanılır?
Bu konu İngilizce terimler kullanma nedenimin özeti diyebiliriz 😄 Türkçe adından anlam çıkarabilen varsa kendisini tebrik etmek isterim. Operator overloading’e geçmeden önce kafanızda şu soru olabilir: Operatör ne ola ki?
C# operatörleri, adını bilmesek de sıklıkla kullandığımız bir takım arkadaşlarımız. Aşağıya kendilerinin bir listesini yazsam gördüğünüzde tanıyacağınızdan eminim.
Operatör olayını netleştirdiğimize göre ufak bir ek bilgi daha verip operator overloading’e gelelim. Üsttteki listede görünen her operatör “overloading” için uygun değildir, C# dili bazılarına izin verirken bazılarına vermiyor. Doğrudan operator overloading için kullanabileceğimiz operatörlerin listesi şu şekilde;
Operator overloading, bir operatörün standart davranışını değiştirmemizi ve ihtiyacımıza göre davranışlar sergilemesini sağlar. Hala anlaşılır olmadığını tahmin ediyorum. Bu nedenle en iyisi bir operatörün davranışını hangi durumlarda, neden ve nasıl değiştirdiğimize bir örnek üzerinden bakalım.
Örnek: İki adet Car sınıfından oluşturulan nesnenin birbirine eşit olup olmadığını kontrol etmek istiyoruz. İki nesnenin eşit olması ne demek önce bunu bir tanımlamamız lazım. İki integer değişkenin eşit olması için ikisinde de aynı sayının olması gerekiyor. Veya iki metnin eşit olması için de aynı durum geçerli. Fakat nesneler içerisinde birden fazla ve farklı farklı türlerde veriler barındırabilirler. Bu nedenle her nesne için “eşitlik” kavramının ne olduğunu belirlememiz lazım. Car sınıfı için şöyle bir tanım yapabiliriz; eğer iki araba aynı renkteyse ve ikisinin de sunroof’u varsa bu iki araba eşittir.
Eşittir operatörünü Car sınıfına özel olarak yeniden anlamlandırmak için Car sınıfının içerisine özel bir fonksiyon yazacağız.
Yeniden yazacağımız ==
fonksiyonu Car sınıfından üretilen tüm nesneler için geçerli ve ortak olacağı için metodumuz static olarak tanımlanıyor. İki tane nesneyi karşılaştırıp eşitliğini kontrol edeceğimiz için fonksiyonumuz iki tane parametre alıyor. Fonksiyonun içerisinde de artık sınıfımıza özel olarak belirlediğimiz eşitlik kontrolünü yazıyoruz. Bu kısım tamamen elimizdeki sınıfın özelliklerine ve ihtiyaçlarımıza bağlı olarak değişebilir. Car sınıfına eklememizi yaptıktan sonra artık Program sınıfında iki nesne üretip eşitliğini kontrol edebiliriz.
Yukarıdaki uygulamamızı çalıştırarak cardOne
ve cardTwo
nesnelerinin eşit, cardOne
ve cardThree
nesnelerinin ise farklı olduğunu görebiliriz. Çünkü araçların üçünde de sunroof yok, birinci ve ikinci araç beyaz iken üçüncü araç kırmızı.
Inheritance (kalıtım) Nedir? Nasıl Kullanılır?
OOP yaklaşımının avantajlarından biri de benzer ihtiyaçlar için aynı kodu tekrar tekrar yazmamızın önüne geçmek yani bir kodun tekrar kullanılabilmesini sağlamaktır. Kalıtım da bunu sağlamak için kullanabileceğimiz özelliklerden biridir. Kalıtım, ismini biyolojiden almaktadır. Nasıl ki anne ve babanın DNA’sındaki bazı özellikler çocuklara kalıtsal olarak aktarılıyor OOP içinde de benzer bir kalıtsal aktarım yöntemi mevcuttur. Yani bir sınıf, kendisinden türeyen başka bir sınıfa kendi özelliklerini aktarır.
is a relationship
- Papatya, gül ve menekşe bir (is a) çiçektir.
- Kedi, fare ve köpek bir (is a) hayvandır.
is a relationship, bir türün aynı zamanda başka bir tür olduğunu söyler. Kalıtım, sınıflar arasında is a relationship kurmamızı sağlar ve kalıtım sayesinde genelden özele doğru giden sınıflar zinciri oluşturabiliriz.
Örnek: Aşağıda bir katılım zinciri görüyoruz.
Vehicle (araç)→ Water Vehicle (su aracı) → Yacth (yat)
Yat bir su aracıdır. Aynı zamanda yat bir araçtır. Aynı zamanda su aracı bir araçtır. Ama her araç bir su aracı veya yat değildir.
Kendi uygulamamızdaki Car sınıfının genel halini oluşturacak bir sınıf ekleyelim ve adına örneğimizdeki gibi Vehicle
diyelim. Tüm araçların bir markası olduğunu ve korna çalabildiğini düşünerek sınıfımıza Brand
isimli bir property ve Horn()
isimli bir fonksiyon ekleyelim.
Vehicle sınıfımız hazır olduğuna göre Car sınıfımıza gelerek sınıf tanımını yaparken bu sınıfın hangi sınıftan türeyeceğini class Car : Vehicle
diyerek belirtiyoruz. Bu kadarcık bir değişiklikle Car sınıfı Vehicle sınıfında yer alan tüm üyelere sahip oldu. Program sınıfımıza gidip testimizi yapalım.
Car
sınıfımızda Brand
ve Horn()
doğrudan tanımlı olmadığı halde Vehicle
sınıfından türediği için bu özellikleri kullanabiliyoruz.
Not: Bir türetme yapılırken baz alınan sınıfa base class, türetilen sınıfa ise derived class adı verilir.
Derived class, base class’ta yer alan tüm üyeleri kendi üzerine alır demiştik. Bu noktada bir hata yok ancak şöyle bir detay var ki base class üzerindeki tüm elemanlara sahip olsa da bazılarına erişemeyebilir. Buna access modifier’lar karar vermektedir.
- Base class’ta yer alan public üyeler hem derived class tarafından hem de bu derived class’tan oluşturulan nesneler tarafından erişilebilir.
- Base class’ta yer alan private üyelere ne derived class ne bu derived class’tan oluşturulan nesneler erişebilir.
- Şu ana kadar hiç kullanmadığımız ama access modifier konusunda “buna sonra geleceğim” dediğim üçüncü erişim belirleyici protected ile tanımlanmış üyelere ise derived class erişilebilir iken bu derived class’tan oluşturulan nesneler erişemez.
Yazımızın başında kalıtım ile genelden özele doğru giden bir sınıflar zinciri kurabileceğimizi söylemiştik. Peki bu nereye kadar devam edebilir? Özellikle bir engel koyulmadığı sürece teorik olarak sonsuza kadar sınıflar birbirinden türeyebilir. Ancak biz bir sınıfın artık olabilecek en özel en gelişmiş hali olduğuna ikna olur ve bu sınıftan başka bir sınıf türetilmesini istemezsek o sınıfı tanımlarken başına sealed
(ing. mühürlenmiş) anahtar kelimesi eklememiz yeterli. Bu artık o sınıftan başka bir sınıf türetilemeyeceği anlamına gelmektedir.
Örneğin, bizim örneğimizde Vehicle sınıfından Car sınıfını türettik. Artık Car sınıfından başka bir sınıf türetilmesini istemiyorsak sınıf tanımını aşağıdaki şekilde değiştirebiliriz.
Polymorphism (Çok biçimlilik) Nedir? Nasıl Kullanılır?
OOP konseptlerinden biri olan polymorphism, kalıtım ile türetilen sınıflarımız olduğunda karşımıza çıkan bir kavramdır. Kalıtım bir sınıfın sahip olduğu field ve fonksiyonların başka bir sınıfa aktarılmasına imkan sağlıyordu, polymorphism ise bu metotların farklı işlevlerinin olmasına olanak sağlar.
En son Vehicle
sınıfımıza Horn()
isimli bir fonksiyon eklemiş ve bunun Car
sınıfında da kullanılabildiğini görmüştük. Şimdi uygulamamıza ikinci araç türü olarak tren ekleyeceğiz. Bunun için Train
isimli yeni bir sınıf tanımlayıp Vehicle sınıfından türetmemiz yeterli. Artık Vehicle’da yer alan Brand
property’si ve Horn()
metodu olan bir sınıfımız daha var.
Vehicle
içerisindeki Horn()
metodu bize Godfather film müziğini korna olarak çalıyordu, bu metodu Car
ve Train
sınıflarına da inheritance ile aktardığımız için aynı şekilde davranacak. Ancak ben istiyorum ki Car
sınıfı Godfather temalı korna çalmaya devam ederken Train
sınıfı çuf çuff diye kornasını çalsın.
Hemen Train
sınıfımıza Horn()
isimli bir metot ekledik ve Vehicle
sınıfından gelen Horn()
yerine bunu kullanmasını sağladık. Car
sınıfımızda ise bir değişiklik istemediğimiz için aynı şekilde kaldı.
Şimdi Program
sınıfımıza gidip bir araç, bir araba ve bir tren oluşturup hepsinin Horn()
metodunu çağırarak ekrana yazmasını sağlayalım.
Uygulamamızı çalıştırdığımızda istediğimiz gibi çıktı ürettiğini görebiliriz.
* godfather theme sound *
* godfather theme sound *
* choo choo *
Aslında buraya kadar pek de şaşırtıcı veya sihirli bir şey yapmadık. Sadece base sınıfta yer alan bir fonksiyon ismiyle derived sınıfta da bir fonksiyon tanımlayabildiğimizi ve bu durumda derived sınıftaki fonksiyonun geçerli olduğunu gördük. Şimdi Program sınıfımızda ufak bir değişiklik yapalım.
Bi dakika! Vehicle sınıfından üç tane değişken tanımlayıp bunlardan birine Vehicle nesnesi, birine Car nesnesi, birine de Train nesnesi atamışız? Nasıl oldu da böyle bir kod yazabildik? Şimdiye kadar hiç int a = “test";
veya string b = true;
gibi eşitliğin sağına ve soluna farklı veri tipleri atamamıştık. Zaten böyle bir atama yapmak istediğimizde kodumuz da derlenmez. Birden hayatımızda ne değişti de Vehicle’a Car veya Train türünden nesneler üretip atayabildik?
Hatırlarsak bölümün başında polymorphism için çok biçimlilik demiştik. Bir değişken kendisinden türetilen sınıflar varsa o sınıfların biçimine bürünebilir. Burada da Car
sınıfı Vehicle
sınıfından türediği için Car car = new Car();
demek yerine Vehicle car = new Car();
diyebiliyoruz.
Yine hatırlayacak olursak inheritance konusunda is a relationship‘ten bahsetmiş, sınıflar arasında kalıtımsal bir ilişki kurmak bu iki sınıf arasında bir is a ilişkisi kurmak anlamına gelir demiştik. Buradan yola çıkarsak biz Car
sınıfımızı Vehicle
sınıfından türettiğimiz için aslında her araba bir araçtır (ama her araç bir araba değildir) diyebiliriz. Yukarıda da bunun yazılımsal karşılığını gördük. Vehicle
türündeki bir değişkene istersek bir Vehicle
istersek bir Car
nesnesi atayabiliyoruz.
Kafanızın yeterince karıştığını düşünüyorsanız hiç merak etmeyin, oldukça normal. Derin bir nefes alın ve uygulamamızı tekrar çalıştırıp sonuçlara bakalım.
* godfather theme sound *
* godfather theme sound *
* godfather theme sound *
Az önce Train
sınıfından oluşturduğumuz nesnenin kornası değişti ve o da Godfather müziği çalmaya başladı. Kendisi bir tren olduğu ve Train
sınıfına yeni bir Horn()
fonksiyonu yazdığımız halde neden gidip base sınıftaki korna fonksiyonu çalıştı? Çünkü train
değişkenimizin o anki veri tipi Train
değil Vehicle
. O anki veri tipine ait olan Horn()
isimli fonksiyonu bulup bunu çalıştırdı. Bu nedenle * godfather theme sound *
görüyoruz.
Ama bizim istediğimi şey tam olarak bu değildi. Biz Train
sınıfından ürettiğimiz bir nesneyi Vehicle
tipindeki bir değişkende saklamak ama aynı zamanda Train
için yazmış olduğumuz o güzel ve özel kornanın çalmasını istiyoruz.
Bunun olması için Vehicle
sınıfında değiştirilebilir olmasını istediğimiz Horn()
fonksiyonunu virtual, Train
sınıfındaki Horn()
fonksiyonunu ise override anahtar kelimesi ile tanımlamamız gerekiyor.
Train
ve Vehicle
sınıflarında değişikliklerimizi yaptıktan sonra tekrar çalıştırdık ve sonuç istediğimiz gibi oldu.
* godfather theme sound *
* godfather theme sound *
* choo choo *
virtual
Sınıftaki bir metodun türetilmiş sınıflar tarafından değiştirilebilir olmasını istiyorsak ilgili metodu virtual olarak tanımlamamız gerekiyor. virtual, sınıftaki metodun bir implementasyonunun olduğunu ve istenirse derived sınıflar tarafından bunun kullanılabileceğini veya yenisinin yazılabileceğini bize anlatan bir anahtar kelimedir.
override
Bir sınıftan türetilmiş olan sınıflar base sınıftaki metotları kullanmak yerine kendi ihtiyaçlarımıza göre yenisini yazmak isterse bu fonksiyonun base sınıftaki fonksiyonun yerine geçtiğini anlatmak için kullandığımız bir anahtar kelimedir.
Örnek: Bankalara hizmet veren bir şirket olduğumuzu düşünelim. Kredi kullandırım ile ilgili bir kütüphane geliştirdik ve banka da bizim kredi hesaplamalarımızı kullanıyor.
İlerleyen süreçte banka yeni bir ürün çıkarıyor ve bu ürüne özel faiz hesaplama yöntemini farklılaştırmak istiyor. Var olan faiz hesaplama metodumuz virtual olarak tanımlanmış ise bu sınıftan yeni bir sınıf türetip override ile yeni bir faiz hesaplama metodu yazmak yeterli olacaktır.
abstraction
Interitance ve polymorphism konularını konuşurken bir base sınıf ve bundan türettiğimiz derived sınıflar vardı. Uygulamamızda istediğimiz zaman base sınıftan veya derived sınıftan bir nesne üretip kullanabiliyorduk. Ancak bazı durumlarda base sınıflardan nesneler üretilemesin sadece derived sınıflardan nesneler üretilsin isteyebiliriz.
Örnek: Araç üretimi yapan bir şirkete yazılım geliştirdiğimizi düşünelim. Şirket yeni bir araba ürettiğinde Car sınıfından yeni bir nesne oluşturup arabayla ilgili bilgileri bu nesneye aktaracaktır. Benzer şekilde tren ürettiğinde Train sınıfından bir nesne oluşturup bilgileri bu nesneye aktaracaktır.
Peki araç üretimi yapan bu şirketin araba, yat, tren, bisiklet vb. yerine genel bir araç üretmesi mümkün mü? Pek değil. Araç bizim için araba, yat, tren, bisiklet gibi araç türlerinin hepsini kapsayan soyut bir kavram. Bu nedenle uygulamamızda Vehicle sınıfından bir nesne üretip özellikler atayabilmek veya metotlarını çağırarak işlemler yapabilmek çok mantıklı değil.
İşte bu soyut sınıfları tanımlamak ve uygulamanın herhangi bir yerinde bu soyut sınıflardan nesneler oluşturulmamasını sağlamak için abstraction (soyutlama) tekniğini kullanıyoruz. Bunun için sınıf tanımı yaparken sınıf adının önüne abstract anahtar kelimesini eklememiz yeterli.
Artık uygulamamızın herhangi bir yerinde Vehicle sınıfından bir nesne üretmemiz mümkün değil. Bunu yapmaya çalıştığımızda kodumuz derlenmeyecek.
Bir sınıfı abstract olarak tanımlayabildiğimiz gibi abstract sınıf içerisinde yer alan bir metodu da abstract olarak tanımlayabiliriz.
Vehicle
sınıfındaki Horn()
metodumuz artık abstract yani soyut oldu. Fark ettiyseniz artık Horn()
metodumuz virtual değil ve metodumuz bir imzadan ibaret (metodun bir içeriği yok). Çünkü soyut bir metot sadece kendisinden türeyecek sınıfları ilgili metodu yazmaya zorlamak için tanımlanmaktadır.
Örnek: Uygulamamızda Vehicle sınıfındaki Horn() metodunu abstract yaptık. Artık Vehicle sınıfından türeyen Car ve Train sınıflarının hazır olarak kullanabileceği bir Horn() metodu yok. Bu nedenle her sınıf kendi ihtiyacına göre bir Horn() metodu yazmak zorunda. Bizim Train sınıfımız için Horn() metodu yazmıştık. Car sınıfında ise Vehicle’dan gelen hazır metodu kullanıyorduk. Artık ona da bir Horn() metodu yazmamız gerekecek.
Ve Car
sınıfımız da artık kendisine özel bir Horn()
metodu ile tekrar çalışmaya hazır hale geldi.
Interface Nedir? Nasıl Kullanılır?
OOP kullanırken soyutlama yapmanın bir diğer yolu da interface kullanmaktır. Interface, içerisinde sadece soyut metot ve özellikler barındıran tamamen soyut bir sınıftır.
- Interface’lerin kolay ayırt edilebilmesi için isimlendirilirken ilk harfinin I olması çok yaygın kullanılan bir standarttır. Bu sayede kodda gördüğümüzde kendisinin bir sınıf değil interface olduğunu rahatlıkla anlayabiliriz.
- Interface’ler abstract sınıfların aksine sadece metot ve property barındırabilirler, field ise bir interface içerisinde yer alamaz.
- Interface üyelerinin tümü public ve abstract’tır.
- Interface içerisinde yer alan tüm metotlar doğası gereği abstract olduğu için interface kullanan sınıflarda implemente edilmek zorundadır. Bu metotlara override yazılmaz. (hatırlayacak olursak abstract bir sınıftan türetildiğinde override yazmamız gerekiyordu)
- interface, tamamen soyut bir sınıf olduğu için standart abstract sınıflarımız gibi bunlar üzerinden de yeni nesneler üretilemez.
Örnek: Vehicle abstract IVehicle isimli bir interface ile değiştirelim.
Vehicle
abstract sınıfımızı interface olarak değiştirdik ve genel kullanıma uyması için adını da IVehicle
yaptık. Eski Vehicle
’dan türettiğimiz Train
ve Car
sınıflarında da düzenlemelerimizi yapmamız gerekiyor.
IVehicle
’da imzasını (tanımını) verdiğimiz Horn()
metodumuzu iki sınıfta da implement ettik, yine abstract sınıftan farklı olarak Brand
özelliği de her sınıfta implemente etmemiz gerekti. Çünkü bir interface‘i kontrat gibi düşünebiliriz. Interface kullanan sınıf, interface içerisinde yer alan tüm üyelerin implementasyonunu yapacağını garanti eder.
Peki neden abstract sınıflar varken interface kullanalım? C#’ta inheritance sadece bir sınıftan yapılabiliyor. Yani Car sınıfına iki tane base sınıf vermek istesek de bunu yapma şansımız yok. Öte yandan bir sınıfa istediğimiz kadar interface verebiliyoruz. Bu yüzden sınıfların daha fazla genişletilmesine olanak sağlamaktadır.
Örnek: Üretilen tüm trenlere özel bir numara atanmaktadır. Bu özel numarayı tutmak için TrainId isimli bir özellik ve istendiğinde bu bilgiyi ekrana yazacak bir PrintTrainId() metodu olmasını istiyoruz.
Problemimizi çözmek için ITrainId
isimli yeni bir interface tanımladık ve Train
sınıfında IVehicle
varken ITrainId
de ekleyebildik. Son olarak ITrainId
içerisinde yer alan TrainId
özelliğini ve PrintTrainId()
metodunu implemente ettik. Ek olarak Train
sınıfına bir parametreli constructor ekledik, artık trainId
gönderilmeden bir tren üretilmesi mümkün olmayacak ve TrainId
özelliğinin set metodunu private yaptığımız için Id bilgisi dışarıdan değiştirilemeyecek. Hemen Program
sınıfımızda yeni trenler oluşturup denemeler yapalım.
Program
sınıfımızda Train
sınıfının kullanımına ilişkin üç farklı örnek yer alıyor.
- İlk olarak standart değişken tanımlama şeklimizle bir
Train
nesnesi üretiyoruz,Horn()
vePrintTrainId()
metotlarını çağırıyoruz. Burada herhangi bir problem yok. - İkinci olarak
Train
nesnesi üretiyor bu seferIVehicle
türünden bir değişkende tutuyoruz. Abstraction konusunda bir nesneyi base sınıf türünden tanımlayabildiğimizi söylemiştik. Burada da benzer bir durum var.Train
sınıfını tanımlarken bu sınıfın hemITrainId
hem deIVehicle
interface’lerine sahip olmasını sağladığımız için böyle tanımlama yapmakta da problem yok. Ancak değişkenimizIVehicle
olduğu içinTrain
sınıfından bir nesne üretmiş olsak bileHorn()
metoduna erişebilirkenPrintTrainId()
metoduna erişemiyoruz. SadeceIVehicle
interface’inde belirtilmiş olan özellik ve metotlara erişim imkanımız var. - Son olarak bir
Train
nesnesiniITrainId
türünden bir değişken üzerinde saklıyoruz. İkinci örneğin tersi olarak bu sefer deITrainId
interface’i üzerindekiPrintTrainId()
metoduna erişimde bir problem yaşamazkenHorn()
metoduna erişemiyoruz.
Burada bahsettiğimiz gibi bir takım özelliklere ve metotlara erişemiyor olmamız bunların var olmadığı anlamına gelmiyor. Train
sınıfından bir nesne ürettiğimiz için aslında hepsi var sadece biz değişken tanımlarken Train
sınıfının bir alt kümesini kullandığımız için nesnenin o kadarlık kısmını görebiliyoruz.
Enum Nedir? Nasıl Kullanılır?
C# dilinde enum’lar uygulama içerisinde kullanılacak sabitleri gruplamak için kullanabileceğimiz özel sınıflar olarak tanımlanabilir. Daha çok kategorik veriler (haftanın günleri, aylar, renkler, yazı tipleri vb.) için kullanılmaya uygun bir yapıdır. Sınıf oluşturmak için kullandığımız class anahtar kelimesi yerine enum kullanarak tanımlama yapıyoruz.
Örnek: Şimdiye kadar arabalarımıza istediğimiz gibi renk verebiliyorduk. Hatta sadece istediğimiz rengi vermekten öte istediğimiz herhangi bir metni yazabilmekteydik. Artık araçların üretilebileceği renkler beyaz (white), mavi (blue), sarı (yellow) ve siyah (black) olarak kısıtlansın ve rastgele girişler yapılamasın istiyoruz.
Uygulamamızda kullanabileceğimiz renkleri belirledik. Tanımlama yaparken alabileceği tüm değerleri arada virgül olacak şekilde yazıyoruz. Color enum’ı hazır olduğuna göre Car sınıfımızda ihtiyaçlarımızı karşılayacak şekilde düzenlemeler yapabiliriz.
string
olarak tanımladığımız color
alanımızı Color
enum tipine dönüştürdük. Aynı zamanda parametreli constructor’da string
olarak aldığımız renk değerini Color
enum tipine çevirdik. Bu sayede Car
sınıfından bir nesne üretilirken Color
enum’ından bir değer almasını zorunlu hale getirmiş olduk. Buna göre Program
sınıfımıza da örnek olarak nesneler oluşturalım.
Örnekte de görüldüğü gibi bir enum’daki verileri okumak için enum adı nokta enum içerisindeki değerlerden biri şeklinde kullanabiliyoruz. Bu uygulamayı çalıştırdığımızda aşağıdaki şekilde ürettiğimiz araç nesnelerinin renklerini ekrana yazacaktır.
White
Blue
Black
Kodlarımızın içine sabit değerler yazmak yerine enum kullanarak kodun okunurluğunu ve anlaşılırlığını da arttırmış oluruz.
Exceptions (İstisnalar)
Bir uygulama içerisinde her zaman işler istediğimiz gibi gitmeyebiliyor. Bazı durumlarda bilerek uygulamanın hata almasını isteyebiliriz (örneğin kullanıcı giriş ekranında kullanıcı adı yazılmadıysa) bazı durumlarda ise bizim öngöremediğimiz problemler ortaya çıkabilir ve uygulamamızın standart akışı bozulabilir (örneğin uygulamamız internetten bir fotoğraf indiriyor ancak uygulamanın çalıştığı bilgisayarın internet bağlantısı koptu).
Uygulamalar çalışırken beklenmeyen veya istenmeyen durumların olması durumunda uygulamamız bir hata atar, buna ingilizce “throws an exception” denmektedir.
Bilgi! Aslında exception kavramı doğrudan OOP ile ilgili değildir. Yine de OOP anlatılırken hep bahsi geçen bir arkadaşımız olduğu için kendisini de bir bölüm olarak ekledim. Okumayı sevenler buradan konuyla ilgili tartışmaya bakabilir.
try/catch/finally
Exception’ları yönetmek için try/catch/finally bloklarından yararlanırız.
try
, bize çalışırken hata alması durumunda bir kontrole girmesini sağlayan kod parçaları yazmamızı sağlar.catch
, kontrol edilen bir kod parçasında hata olması durumunda bunu fark edip nasıl davranacağımızı belirlememizi sağlar.finally
, bir kod parçası hata alsa da almasa da her durumda çalışması gereken işlerin yazıldığı bölümdür. Örneğin; bir sisteme giriş yaptık ve işlemler gerçekleştiriyoruz. Bu işlemlerimizi başarılı şekilde tamamladığımızda sistemden çıkış yapmalıyız. İşlemlerimizi yaparken bir noktada hata alsak ve işimiz yarıda kalsa bile güvenlik gereği yine de çıkış yaptığımızdan emin olmalıyız. İşte bu gibi durumlarda finally kullanılabilir.
try kullanıldığı anda catch kullanımı zorunludur ancak finally kullanımı ihtiyaca bağlı olarak opsiyonel bırakılmıştır. try/catch/finally şablonu aşağıdaki gibidir.
Şimdi uygulamamızın hata almasını sağlayacak bir değişiklik yapalım.
Uygulamamızı bu şekilde çalıştırdığımızda aşağıdaki gibi bir hata aldığımızı göreceğiz ve uygulamamız hata aldığı noktada sonlanacak. Yani anotherCar
değişkenine yeni bir Car
nesnesi atayıp onun PrintColor()
metodunu çağıramayacak.
An unhandled exception of type ‘System.NullReferenceException’ occurred in ….: ‘Object reference not set to an instance of an object.’
Var olmayan bir nesnenin bir üyesine erişmeye çalıştığımızda C# bize NullReferenceException hatası verir. Burada da bir örneğini gördük. Şimdi Program sınıfımızı düzenleyelim ve bu hatayı aldığında uygulamanın durmasını engelleyelim, onun yerine ekrana bir mesaj yazmasını sağlayalım.
Ve uygulamamızı tekrar çalıştırdığımızda bu sefer anotherCar
’a ait PrintColor()
metodunun çalışabildiğini gördük. Çünkü catch ile hatayı yakaladık ve uygulamayı sonlandırmak yerine hata mesajını ekrana yazmasını istedik.
Something went wrong…
Error detail: Object reference not set to an instance of an object.
White
throw
Uygulama geliştirici olarak biz de bazı durumlarda diğer sınıflara yaptıkları yanlış işlemler için hata atmak isteyebiliriz. Bunun için throw anahtar kelimesini kullanıyoruz.
Örnek: Train sınıfından bir nesne oluştururken trenlere bir Id verilmesini zorunlu hale getirmiştik. Şimdiye kadar Id bilgisinin hiç kontrolünü yapmadık. Halbuki biz Id bilgisinin negatif olmamasını istiyoruz. Bunun için bir kontrol eklenmesi gerekiyor.
Hemen Program sınıfımızda negatif değer atamaya çalışarak sonuçları görelim.
Uygulamamızı çalıştırdığımızda şöyle bir çıktı göreceğiz;
Error detail: Train Id cannot be negative.
throw
ile kullandığımız Exception
sınıfı, atabileceğimiz hataların en genelidir. Ancak içinde bulunduğumuz duruma göre spesifik hatalar vermek yazdığınız sınıfı kullanan diğer kişilerin hataları anlaması için daha iyi olacaktır. Bu nedenle C#’ta yer alan pek çok farklı hata çeşidinden ilgili olanı kullanabilir veya kendi hata türlerinizi yazabilirsiniz (Kendi hata türlerimizi oluşturmak için Exception
sınıfından bir sınıf türetmemiz yeterli). Train
sınıfındaki hata verdiğimiz yeri değiştirip yine C#’ta hazır olarak yer alan farklı bir hata türü atalım.
Uygulamamızı bu şekilde tekrar çalıştırdığımızda hata mesajının değiştiğini görebiliriz.
Error detail: Value cannot be negative. (Parameter ‘trainId’)
Exception sınıfının hata atmak için kullanılabilecek en genel sınıf olduğunu söylemiştik. try/catch bloklarında catch’e Exception sınıfından bir değişken tanımlayarak ilgili blok içerisindeki tüm hataları yakalayabiliyoruz. Ama bazı durumlarda sadece spesifik hata türlerini yakalamayı veya daha iyisi spesifik hata türlerini ayrı yakalamayı isteyebiliriz. Bunun için catch bloklarımızı aşağıdaki şekilde çoğaltmamız yeterli olacaktır.
Argument exception thrown. Detail: Value cannot be negative. (Parameter ‘trainId’)
try
içerisinde attığımız ArgumentOutOfRangeException
’ı alıp, bu özel exception tipine ait bir catch bloğu olup olmadığını kontrol ediyor. Eğer varsa o catch bloğu içerisine girip diğerlerine girmiyor (bu nedenle uygulama çıktımızda sadece “Argument exception thown. ….” şeklinde bir mesaj gördük “Generic exception thrown. …” ise görmedik. Burada önemli bir detay; catch
bloklarının sırası önemli. Yani gidip ilk başa Exception
sonra ArgumentOutOfRangeException
olan catch bloğu yazarsak hepsi ilk Exception
bloğuna girer, çünkü tüm exception türleri aslında Exception
sınıfından türetiliyor (alın size kalıtım konusuna bir örnek😊).
Generics (Genel Türler)
Yazı boyunca yaptığımız her şey yani tüm OOP yaklaşımı, biz yazılımcıları kod tekrarından uzaklaştırmak üzere düşünülmüş yöntem ve tekniklerden oluşuyor. Generics de kod tekrarını azaltmak için tasarlanmış oldukça kullanışlı ve akıllıca bir yöntem olarak karşımıza çıkıyor.
Bir sınıf veya bir metodun generic olarak tanımlanması, bu sınıfın farklı veri türleri için kullanılabileceğini ifade etmektedir. Ne demek istedim ben şimdi?
C# ile bir miktar kod yazma tecrübeniz varsa büyük ihtimalle farkında olmadan generic kullanmışsınızdır. Hemen testimizi yapalım;
List<string> names = new List<string>();
Yukarıdaki gibi bir liste kullandıysanız tebrikler! Ne olduğunu bilmeseniz de generic kullanmışsınız.
Bu tanımlamaya biraz yakından bakalım. Aslında bir List
sınıfı var, biz bu sınıftan bir nesne oluşturmak istemişiz ve bu nesneyi temsil eden değişkene de names
adını vermişiz. Bir de ortada <>
işaretleri ve bir string
ifadesi var. İşte generic tam da bu noktada devreye giriyor. <string>
yazarak “içerisinde string tipinde veriler tutacağım bir liste oluşturmak istiyorum” demiş oluyoruz. Bu tanımlamanın ardından names listesine sadece metin ekleyebiliriz.
List<int> numbers = new List<int>();
Şimdi de bu liste tanımına bakalım. Aslında aynı List
sınıfını kullanıyoruz ama bu sefer <int>
diyerek listemizde tam sayı veriler tutacağımızı belirttik. Bu noktadan sonra numbers
listesine bir metin veya farklı veri türünde değer atamamız mümkün değil.
Bu örneklerde kilit nokta şu; iki tanımı da yaparken aynı List
sınıfını kullandık. Aslında elimizde tek bir sınıf var ama bu sınıf ihtiyacımıza göre farklı türde veriler kullanabiliyor. Eğer generic kullanımı olmasaydı tek bir List
sınıfı yerine ListInteger
, ListString
, ListBool
, vb. gibi bir sürü birbirine benzer işler yapan liste sınıfı yazmamız gerekecekti. Belki kod tekrarını bir ölçüde önlemek için abstract bir List sınıfı yazıp ortak fonksiyonları burada tutacak ve ondan farklı List sınıfları oluşturacaktık. Generic sayesinde bunların hiçbirini yapmamıza gerek kalmadan tek bir List sınıfı ile ihtiyacımızı karşılayabildik. Peki bir sınıfın böyle farklı türde veriler almasını nasıl sağlıyoruz?
Örnek: Generic sınıf yazımın mantığını kavramak için C#’ta kullanmış olduğumuz generic List sınıfının basit bir versiyonunu yazalım. Adı SimpleList olan bu liste yine içerisinde farklı türden veriler saklayabilir olsun. Listeye sadece eleman ekleme ve eleman sayısını öğrenme özellikleri koyalım. Durumu iyice basitleştirmek için de listedeki maksimum olabilecek eleman sayısını 100 yapalım.
Bir generic sınıf tanımlarken sınıf adından sonra <>
işaretlerinin arasına bir isim vererek bu sınıftan bir nesne oluştururken farklı veri türleri verilmesini sağlamış oluyoruz. <>
işaretlerinin arasına yazdığımız T
ise aslında dışarıdan bize söylenecek olan veri tipini sınıf içerisinde temsil eden bir isimden ibaret. T harfinin özel bir anlamı yok, istersek T yazmak yerine class SimpleList<SelamCanimNaber>
de yazabilirdik. Sadece sınıfın içerisinde T’yi kullandığımız diğer yerleri de aynı isimle değiştirmemiz gerekecekti.
SimpleList
sınıfımızın içerisinde T
geçen diğer yerlere de bir bakalım. items
listesini tanımlarken ve oluştururken T
kullanmışız. Bir de Add()
metodunun parametresini T
tipinde yapmışız. T
’yi kullandığımız yerlerin hepsi aslında veri tipine ihtiyaç duyduğumuz noktalar. Şöyle düşünürsek muhtemelen kafamızda her şey çok daha net olacak. Program sınıfında aşağıdaki gibi bir kod yazdığımızı varsayalım;
Bizim generic sınıfımız myIntegerList
için şu şekilde görünecek;
Aynı sınıf myStringList
için ise şu şekilde görünecek;
Dikkat ettiyseniz T
yazan yerler int
ve string
olarak değişmiş gibi oldu. İşte int
ve string
için yukarıdaki gibi iki sınıf yazmak yerine generic sayesinde tek bir sınıf yazdık ve sadece listeden bir nesne üretirken hangi veri tipi için üreteceğini bize belirtmesini istedik. Artık o nesne için sınıfımızdaki T
tipi bize nesne üretilirken verilen veri tipi olarak davranmaya başladı.
Generic’ler kod tekrarını ciddi ölçüde azaltan ve aynı fonksiyonları aynı isimlendirmeleri farklı yerlerde kullanmamızı sağlayarak kodumuzda ortak bir dilin oluşmasına ciddi ölçüde fayda sağlayan bir yapıdır. Kullanımı ilk başta biraz garip gelse de bir iki tane örnek sınıf hazırladıktan sonra rahatlıkla anlaşılabilir. Aslında generic konusu burada bitmiyor ama tüm detaylarına girerek daha fazla kafa karışıklığına sebep olmamak için şimdilik burada kesiyorum.
Son sözler
Eveeet.. Uzun, oldukça uzun bir yazı olduğunun farkındayım. Sabırla okuyup buraya kadar gelebilen herkese teşekkür ederim. Umarım bu yazı OOP kavramları konusunda genel bir fikir sahibi olmanızı sağlamıştır. OOP konusu okurken basit gibi görünebilir ancak konu uygulamaya geldiğinde tasarımı düzgün yapmak ve sınıfları doğru kurgulamak göründüğünden zor olabiliyor. Bunun için sizlere tavsiyem bol bol örnekler yapmayı denemek ve farklı tasarım örnekleri incelemek.
Bazı dış kaynaklar paylaşarak yazımı noktalıyorum. Başka bir yazıda görüşmek üzere!
OOP İçin Kullanabileceğiniz Kaynaklar
- Wikipedia — Nesne yönelimli programlama
- Wikipedia — Object oriented programming
- W3Schools — OOP
- Hackerrank — OOP örnekleri (Java)
- İTÜ Bilgisayar Mühendisliği OOP Ders Notları (C++)
- Medium — OOP: Everything you need to know about Object Oriented Programming