Yazılım8 dk okuma

Nesne Yönelimli Programlama (OOP) Nedir?

OOP, veri ve davranışı nesnelerde birleştiren bir programlama yaklaşımıdır. Dört temel kavramı gerçek kod örnekleriyle, tekrarsız şekilde anlatıyoruz.

Nesne Yönelimli Programlama (OOP) Nedir?

Nesne yönelimli programlama (OOP), bir programı birbiriyle iletişim kuran nesneler etrafında kurma yaklaşımıdır. Her nesne kendi verisini (özellik) ve bu veri üzerinde çalışan işlevlerini (metot) bir arada tutar. Java, C#, Python ve C++ gibi diller bu yaklaşımı destekler; günümüzün büyük yazılım sistemlerinin çoğu OOP ile yazılır.

Bu yazıda OOP'nin dört temel kavramını tek tek, gerçek kod örnekleriyle ve tekrara girmeden anlatıyoruz. Ardından ne zaman OOP kullanmanın anlamlı olduğuna, kompozisyon alternatifine, SOLID ilkelerine ve sık yapılan hatalara bakıyoruz.

Nesne, sınıf ve örnek: temel sözlük

Bir sınıf (class), bir nesnenin nasıl olacağını tanımlayan şablondur. Bir nesne (object/instance) ise bu şablondan üretilmiş somut bir örnektir. "Araba" bir sınıfsa, garajınızdaki gerçek araba bir nesnedir. Python'da bu ilişki şöyle görünür:

class Araba:
    def __init__(self, marka, hiz=0):
        self.marka = marka
        self.hiz = hiz

    def hizlan(self, miktar):
        self.hiz += miktar

araba1 = Araba("Toyota")
araba1.hizlan(20)
print(araba1.hiz)  # 20

Araba sınıfı bir şablon; araba1 bu şablondan üretilmiş bir nesne. marka ve hiz özellik (veri), hizlan ise metot (davranış). Aynı sınıftan istediğiniz kadar nesne üretebilirsiniz, her biri kendi verisini taşır.

OOP'nin dört temel kavramı

OOP genelde dört kavram üzerinden anlatılır. Aşağıda her birini bir kez, net bir tanım ve tek bir kod örneğiyle ele alıyoruz.

Kapsülleme (encapsulation)

Kapsülleme, bir nesnenin verisini ve bu veriyi değiştiren metotları bir arada tutup dışarıdan kontrolsüz erişimi engellemektir. Amaç veriyi gizlemek değil, veriye nasıl erişileceğini kontrol etmektir:

class BankaHesabi:
    def __init__(self, bakiye):
        self._bakiye = bakiye

    def para_yatir(self, miktar):
        if miktar > 0:
            self._bakiye += miktar

    def bakiye_goster(self):
        return self._bakiye

Bakiye doğrudan dışarıdan değiştirilebilseydi (hesap.bakiye = -500 gibi) hatalı durumların önüne geçilemezdi. para_yatir metodu, negatif miktarları reddederek bu kontrolü sağlıyor. Kapsülleme, bir nesnenin iç detaylarını değiştirdiğinizde bu değişikliğin geri kalan koda minimum etkiyle yansımasını da kolaylaştırır.

Kalıtım (inheritance)

Kalıtım, bir sınıfın başka bir sınıfın özelliklerini ve metotlarını devralmasıdır. Amaç, ortak davranışı tekrar tekrar yazmak yerine bir kez tanımlayıp paylaşmaktır:

class Arac:
    def __init__(self, marka):
        self.marka = marka

    def bilgi(self):
        return f"{self.marka} bir araçtır"

class Araba(Arac):
    def bilgi(self):
        return f"{self.marka} bir arabadır"

Araba, Arac sınıfının __init__ metodunu devralır ama bilgi metodunu kendi ihtiyacına göre yeniden tanımlar (override). Kod tekrarı azalır: ortak alanlar bir kez, üst sınıfta yazılır.

Çok biçimlilik (polymorphism)

Çok biçimlilik, aynı isimdeki bir metodun farklı sınıflarda farklı davranmasıdır. Yukarıdaki Arac ve Araba sınıflarının ikisinde de bilgi metodu var ama farklı sonuç döndürüyor. Bunun pratik faydası, bir listeyi gezerken hangi nesnenin hangi sınıftan geldiğini bilmenize gerek kalmamasıdır:

araclar = [Arac("Genel"), Araba("Toyota")]
for a in araclar:
    print(a.bilgi())
# Genel bir araçtır
# Toyota bir arabadır

Kod, listedeki her elemanın tam olarak hangi sınıftan olduğunu kontrol etmek zorunda kalmadan bilgi() metodunu çağırıyor; her nesne kendi davranışını sergiliyor. Bu, yeni bir araç türü eklendiğinde mevcut döngü kodunu değiştirmenize gerek kalmaması anlamına gelir.

Soyutlama (abstraction)

Soyutlama, bir nesnenin gereksiz iç detaylarını gizleyip yalnızca dışarıdan gerekli olan davranışı göstermektir. Bir arayüz (interface) veya soyut sınıf, hangi metotların bulunması gerektiğini tanımlar; bu metotların içeride nasıl çalıştığını değil. araba1.hizlan(20) çağrısını yaparken motorun içindeki mekanizmayı bilmenize gerek yok – sadece hizlan metodunun ne yaptığını bilmeniz yeterli. Soyutlama, kapsüllemeyle iç içe çalışır: kapsülleme veriyi korur, soyutlama karmaşıklığı kullanıcıdan gizler.

Kompozisyon: kalıtımın bir alternatifi

Kalıtım her zaman en doğru araç değildir. Bir sınıf, başka bir sınıfı miras almak yerine, o sınıfın bir örneğini kendi içinde taşıyabilir; buna kompozisyon denir. "Araba bir Araçtır" (kalıtım) ile "Araba bir Motora sahiptir" (kompozisyon) arasındaki fark, ilişkinin doğasını doğru modellemekle ilgilidir:

class Motor:
    def __init__(self, guc):
        self.guc = guc

    def calistir(self):
        return f"{self.guc} beygirlik motor çalışıyor"

class Araba:
    def __init__(self, marka, motor):
        self.marka = marka
        self.motor = motor  # kompozisyon: Araba bir Motora sahip

    def baslat(self):
        return f"{self.marka}: {self.motor.calistir()}"

Burada Araba, Motor sınıfından kalıtım almıyor; onu bir özellik olarak içinde taşıyor. Bu yaklaşım sınıflar arasında daha gevşek bir bağ kurar ve aynı motoru başka bir araca – örneğin bir kamyona – kolayca ekleyebilmenizi sağlar. Yazılım camiasında sık duyulan "kalıtım yerine kompozisyonu tercih edin" önerisi buradan gelir: kalıtım sıkı bir bağ kurar, kompozisyon parçaları daha bağımsız tutar.

Statik Üyeler: Sınıfa mı, Nesneye mi Ait?

Şimdiye kadarki örneklerde her özellik ve metot, o sınıftan üretilen her nesneye ayrı ayrı ait oldu. Ancak bazı veriler nesneye değil, sınıfın kendisine ait olmalıdır — örneğin oluşturulan toplam nesne sayısı. Bunun için statik (static) üye kullanılır:

class Araba:
    toplam_araba_sayisi = 0  # sinifa ait, tum nesneler paylasir

    def __init__(self, marka):
        self.marka = marka  # nesneye ait
        Araba.toplam_araba_sayisi += 1

araba1 = Araba("Toyota")
araba2 = Araba("Renault")
print(Araba.toplam_araba_sayisi)  # 2

Statik üyeler, tüm nesneler arasında paylaşılan ortak bir sayaç, sabit veya yardımcı fonksiyon tanımlamak istediğinizde kullanılır; her nesnenin kendi kopyasını tutmasına gerek olmayan veriler için idealdir.

Sınıf mı fonksiyon mu? OOP ne zaman anlamlı

OOP her problem için doğru araç değil. Küçük bir betik (script) ya da tek seferlik bir veri dönüşümü için sınıf tanımlamak, çözümden çok karmaşıklık ekler. OOP'nin değeri, birden fazla nesne türünün birbiriyle etkileşime girdiği, durumun (state) zamanla değiştiği ve birden fazla kişinin aynı kod tabanı üzerinde çalıştığı projelerde ortaya çıkar.

Örneğin, bir CSV dosyasındaki sayıları toplayıp ortalamasını yazdıran bir betik için sınıf tanımlamak gereksiz; birkaç fonksiyon yeterli. Ama bir sipariş yönetim sistemi – müşteri, sipariş, ürün ve ödeme gibi birbiriyle ilişkili birden fazla varlığın bulunduğu bir yapı – için OOP, bu varlıklar arasındaki ilişkiyi ve zamanla değişen durumu yönetmeyi büyük ölçüde kolaylaştırır.

DurumOOP mantıklı mı?
Tek seferlik veri dönüşüm betiğiGenelde hayır – sade fonksiyonlar yeterli
Birden fazla varlık türü olan bir sistem (kullanıcı, sipariş, ürün gibi)Evet – her varlık kendi verisini ve davranışını taşıyabilir
Büyük bir ekibin uzun süre bakımını yapacağı bir kod tabanıEvet – net sınırlar ve kapsülleme bakımı kolaylaştırır
Basit, durumu olmayan bir hesaplama (örn. iki sayıyı toplama)Hayır – doğrudan bir fonksiyon yeterli

OOP ile Fonksiyonel Programlama Arasındaki Fark

OOP'nin en sık karşılaştırıldığı yaklaşım, fonksiyonel programlamadır. OOP, durumu (state) nesnelerin içinde tutup bu durumu değiştiren metotlar tanımlarken; fonksiyonel programlama durumu değiştirmemeyi (immutability) ve aynı girdiyle her zaman aynı çıktıyı üreten "saf fonksiyonlar" yazmayı önceler. İkisi birbirinin zıttı değil, farklı problemlere daha uygun iki araçtır: bir sipariş sisteminde müşterinin sepeti zamanla değişir, bu durumu bir nesnede tutmak doğaldır; bir veri dönüşüm hattında (bir listeyi filtrele, dönüştür, topla) fonksiyonel stil genelde daha kısa ve test edilmesi daha kolay kod üretir. Modern diller (Python, JavaScript, C#, Java) artık ikisini bir arada destekler; katı bir "ya OOP ya fonksiyonel" ayrımı yerine, hangi problemin hangi stile daha uygun olduğuna bakmak daha pratik bir yaklaşımdır.

AppFellas

Fikrinizi konuşalım.

Ücretsiz keşif görüşmesinde projenizi, bütçenizi ve zaman çizelgenizi netleştirelim.

SOLID: OOP kodunu dağınıklıktan koruyan 5 ilke

SOLID, büyüyen bir OOP kod tabanının okunabilir ve değiştirilebilir kalması için sık referans verilen beş ilkenin baş harflerinden oluşur. Bunları ezbere kural olarak değil, bir sınıfı değiştirirken "bu değişiklik neden zor oldu" sorusuna cevap arayan bir kontrol listesi gibi düşünmek daha faydalı:

  • Tek Sorumluluk: Bir sınıfın değişmesi için tek bir nedeni olmalı; hem veritabanına yazan hem e-posta gönderen bir sınıf, iki farklı sorumluluğu karıştırır.
  • Açık/Kapalı: Sınıflar yeni davranış eklemeye açık, mevcut ve test edilmiş koda dokunmaya kapalı olmalı.
  • Liskov Yerine Geçme: Bir alt sınıf, üst sınıfın kullanıldığı her yerde sorunsuz onun yerine geçebilmeli; Araba, Arac'ın beklediği davranışı bozmamalı.
  • Arayüz Ayrımı: Her şeyi yapan tek bir büyük arayüz yerine, küçük ve amaca özel arayüzler tercih edilmeli.
  • Bağımlılıkların Tersine Çevrilmesi: Kod, somut bir uygulamaya değil soyut bir arayüze bağımlı olmalı; böylece o uygulama değiştiğinde geri kalan kod etkilenmez.

Sık yapılan hatalar

  • Her şeyi sınıfa sokmak: Basit bir betiği bile gereksiz sınıf hiyerarşileriyle karmaşıklaştırmak, kodu okumayı zorlaştırır.
  • Kalıtımı her yerde kullanmak: Bazen "içerir" ilişkisi (kompozisyon), "bir türüdür" ilişkisinden (kalıtım) daha uygun; her ortak özellik kalıtım gerektirmez.
  • Kapsüllemeyi atlamak: Tüm özellikleri doğrudan dışarıya açık bırakmak, kontrolsüz değişikliklere ve tutarsız verilere yol açar.
  • SOLID'i ezbere uygulamak: Her ilkeyi mekanik şekilde uygulamaya çalışmak, küçük bir projede gereksiz katman ve dosya sayısına yol açabilir.

Hangi diller OOP destekler?

Diller OOP'yi ne kadar zorunlu kıldıklarına göre de ayrışır:

DilOOP desteği
JavaTamamen nesne yönelimli tasarlanmış; her kod bir sınıfın içinde yaşar
PythonNesne yönelimli ama zorunlu değil; fonksiyonel ve prosedürel stiller de kullanılabilir
C++Hem prosedürel hem nesne yönelimli programlamayı destekler
JavaScriptPrototip tabanlı ama <code>class</code> sözdizimiyle OOP tarzı yazılabilir

Örnek Senaryo: Bir E-Ticaret Uygulamasında OOP

Dört kavramın bir arada nasıl çalıştığını görmek için bir e-ticaret uygulamasını ele alalım. Urun sınıfı ad ve fiyatı kapsüller; IndirimliUrun sınıfı Urun'dan kalıtım alıp fiyat hesaplamasını override ederek çok biçimlilik sağlar; Sepet sınıfı ürünleri kompozisyonla içinde tutar:

class Urun:
    def __init__(self, ad, fiyat):
        self.ad = ad
        self.fiyat = fiyat

    def toplam_fiyat(self):
        return self.fiyat

class IndirimliUrun(Urun):
    def __init__(self, ad, fiyat, indirim_orani):
        super().__init__(ad, fiyat)
        self.indirim_orani = indirim_orani

    def toplam_fiyat(self):
        return self.fiyat * (1 - self.indirim_orani)

class Sepet:
    def __init__(self):
        self.urunler = []  # kompozisyon

    def urun_ekle(self, urun):
        self.urunler.append(urun)

    def toplam(self):
        return sum(u.toplam_fiyat() for u in self.urunler)  # polimorfizm

Sepet.toplam() metodu, listedeki her ürünün Urun mu IndirimliUrun mu olduğunu bilmeden toplam_fiyat() çağırır — her nesne kendi hesaplamasını yapar. Yeni bir ürün türü (örneğin KampanyaliUrun) eklendiğinde Sepet sınıfının kodunu değiştirmeye gerek kalmaz; bu, çok biçimliliğin pratikte sağladığı asıl esnekliktir.

OOP'yi öğrenmeye nereden başlamalı?

OOP'ye geçmeden önce seçtiğiniz dilin temel sözdizimini – değişken, döngü, fonksiyon – oturtmuş olmanız gerekir; aksi hâlde sınıf ve nesne kavramları havada kalır. Python'a özel bir zaman çizelgesini Python dili kaç ayda öğrenilir yazımızda, genel süreci yazılım öğrenmek ne kadar sürer yazımızda anlattık.

En iyi pratik yöntemi, gerçek bir şeyi modellemek: bir kütüphane ödünç verme sistemi (Kitap, Üye, Ödünç Alma sınıfları), ya da bir görev listesi uygulamasını nesnelerle yeniden yazmak (her görev kendi durumunu taşıyan bir nesne). Yoğun bir başlangıç planı arıyorsanız 3 ayda yazılım öğrenilir mi yazımızdaki 12 haftalık plana bakabilirsiniz.

Ekibinizin ihtiyaç duyduğu bir sistemi baştan tasarlamak istiyorsanız tamamladığımız projelere göz atabilir ya da bize yazabilirsiniz.

Sıkça Sorulan Sorular

Sık sorulan sorular

Nesne Yönelimli Programlama nedir?

Bir programı, veri ve davranışı birlikte taşıyan nesneler etrafında kurma yaklaşımıdır. Java, C#, Python ve C++ gibi diller bu yaklaşımı destekler.

Sınıf ile nesne arasındaki fark nedir?

Sınıf, bir nesnenin nasıl olacağını tanımlayan şablondur. Nesne, bu şablondan üretilmiş somut bir örnektir. Aynı sınıftan birden fazla nesne üretilebilir, her biri kendi verisini taşır.

Kalıtım neden kullanılır?

Ortak özellik ve davranışları tek bir üst sınıfta tanımlayıp alt sınıflara aktarmak için kullanılır. Bu, aynı kodu birden fazla sınıfta tekrar yazmayı önler.

Kapsülleme ne işe yarar?

Bir nesnenin verisini, o veriyi kontrollü şekilde değiştiren metotların arkasına gizler. Böylece veri, geçersiz veya tutarsız bir duruma doğrudan dışarıdan getirilemez.

Her projede OOP kullanmak gerekir mi?

Hayır. Basit betikler ve durumu olmayan hesaplamalar için sade fonksiyonlar yeterli olur. OOP, birden fazla nesne türünün etkileşime girdiği ve zamanla büyüyen projelerde değer katar.

Python'da OOP kullanmak zorunlu mu?

Hayır. Python fonksiyonel ve prosedürel stilleri de destekler. Küçük bir script için sınıf tanımlamak şart değil; ihtiyaç birden fazla ilişkili veri ve davranışı bir arada tutmaya geldiğinde OOP devreye girer.

Paylaş

Bu konuyu projenize uygulayalım.

Yazıda anlatılanları kendi ürününüzde nasıl hayata geçireceğinizi ücretsiz keşif görüşmesinde konuşalım.