Tek Sorumluluk Prensibi (Single Responsibility Principle — SRP), SOLID prensiplerinin ilkidir ve Nesne Yönelimli Programlama’da (OOP) temiz, sürdürülebilir kod yazmanın temelini oluşturur. Kısaca hatırlatmak gerekirse, SRP şunu söyler: Bir sınıfın değişmesi için sadece tek bir nedeni olmalıdır. Yani, her sınıfın sadece tek bir ana görevi veya sorumluluğu olmalıdır.
Teoride basit gibi görünse de, pratikte SRP’yi ihlal etmek oldukça kolaydır. Bazen farkında olmadan bir sınıfa birden fazla görev yükleyebiliriz. Bu durum başlangıçta sorun yaratmasa da, proje büyüdükçe ve değiştikçe kodun karmaşıklaşmasına, bakımının zorlaşmasına ve hatalara daha açık hale gelmesine neden olur.
Bu rehberde, sıkça karşılaşılan SRP ihlallerini somut Python örnekleri üzerinden inceleyeceğiz. Her örnekte önce ihlalin yapıldığı kodu gösterecek, neden SRP’ye aykırı olduğunu açıklayacak ve ardından bu prensibe uygun, daha iyi bir çözüm sunacağız. Amacımız, SRP ihlallerini tanımayı ve kodumuzu bu prensibe uygun hale getirmek için nasıl yeniden düzenleyebileceğimizi (refactoring) basit ve anlaşılır bir şekilde göstermektir.
Bölüm 1: SRP İhlallerini Neden Düzeltmeliyiz? (Kısa Hatırlatma)
Kodumuzda SRP ihlallerini bulup düzeltmek neden önemlidir?
Daha Anlaşılır Kod: Tek iş yapan sınıfları okumak ve anlamak daha kolaydır.
Daha Kolay Bakım: Bir değişiklik gerektiğinde, sadece ilgili sınıfı değiştirirsiniz, diğerleri etkilenmez.
Daha Kolay Test: Küçük, odaklanmış sınıfları test etmek daha basittir.
Daha Az Hata: Değişikliklerin yan etkileri azalır.
Daha İyi Yeniden Kullanım: Tek görevli sınıfları başka yerlerde kullanmak daha olasıdır.
Bölüm 2: Örnek 1 — Kullanıcı Verisi ve Veritabanı İşlemleri
Bu, en klasik ve sık görülen SRP ihlallerinden biridir: Bir sınıfın hem veriyi temsil etmesi (model olması) hem de o veriyi nasıl saklayacağını (kalıcılık — persistence) bilmesi.
İhlal Örneği:
SRP İhlali
import sqlite3 # Örnek için veritabanı kütüphanesi
class Kullanici:
def init(self, id, ad, email):
self.id = id
self.ad = ad
self.email = email
print(f"Kullanıcı nesnesi oluşturuldu: {self.ad}")
def bilgileri_al(self):
"""Kullanıcı bilgilerini döndürür (Bu sınıfın sorumluluğu)."""
return {"id": self.id, "ad": self.ad, "email": self.email}
# --- BURASI SRP İHLALİ BAŞLATIYOR ---
def veritabanina_kaydet(self):
"""Kullanıcı bilgilerini SQLite veritabanına kaydeder."""
# Bu sınıfın veritabanı detaylarını bilmemesi gerekir!
try:
conn = sqlite3.connect('kullanicilar.db')
cursor = conn.cursor()
# Tablo yoksa oluştur (Bu bile ayrı bir sorumluluk olabilir)
cursor.execute('''CREATE TABLE IF NOT EXISTS kullanicilar
(id INTEGER PRIMARY KEY, ad TEXT, email TEXT)''')
# Kullanıcıyı ekle veya güncelle
cursor.execute("INSERT OR REPLACE INTO kullanicilar (id, ad, email) VALUES (?, ?, ?)",
(self.id, self.ad, self.email))
conn.commit()
print(f"'{self.ad}' veritabanına kaydedildi/güncellendi.")
except sqlite3.Error as e:
print(f"Veritabanı hatası: {e}")
finally:
if conn:
conn.close()
# --- SRP İHLALİ BİTİYOR ---
Kullanım
k1 = Kullanici(1, "Ahmet Yılmaz", "[email protected]")
k1.veritabanina_kaydet() # Kullanıcı nesnesi kendi kendini kaydediyor!
k2 = Kullanici(2, "Ayşe Kara", "[email protected]")
k2.veritabanina_kaydet()
Neden İhlal Ediliyor?
Birden Fazla Değişme Nedeni: Bu Kullanici
sınıfını değiştirmek için en az iki ana neden var:
Kullanıcı verisinin yapısı değişirse (yeni bir alan eklenirse, email formatı değişirse vb.).
Veritabanı kaydetme mantığı değişirse (farklı bir veritabanı kullanılacaksa — örn: PostgreSQL, tablo yapısı değişirse, SQL sorguları değişirse, hata yönetimi değişirse vb.).
Farklı Sorumluluklar:
Kullanıcı verisini temsil etmek (nitelikler ve belki basit getter’lar).
Veritabanı ile etkileşim kurmak (bağlanma, sorgu çalıştırma, kapatma — kalıcılık katmanı sorumluluğu).
Bu iki sorumluluk birbirinden farklıdır ve farklı uzmanlık alanlarına veya sistem katmanlarına aittir.
Test Zorluğu: Kullanici
sınıfını test etmek için bir veritabanı bağlantısına ihtiyaç duyarsınız. Sınıfı veritabanından bağımsız test etmek zorlaşır.
Tekrar Kullanım Zorluğu: Bu Kullanici
sınıfını, veritabanı kullanmayan başka bir projede kullanmak isterseniz, gereksiz veritabanı kodunu da beraberinde getirmiş olursunuz.
Çözüm: Sorumlulukları Ayırma
Çözüm, sorumlulukları farklı sınıflara ayırmaktır:
Kullanici sınıfı sadece kullanıcı verilerini tutmaktan sorumlu olmalı (bir veri modeli veya varlık - entity).
Veritabanı işlemlerinden sorumlu ayrı bir sınıf (genellikle “Repository” veya “Data Access Object — DAO” deseni kullanılır) oluşturulmalı.
SRP Uyumlu Çözüm
import sqlite3
Sorumluluk 1: Kullanıcı Veri Modeli
class Kullanici:
def init(self, id, ad, email):
self.id = id
self.ad = ad
self.email = email
print(f"Kullanıcı nesnesi oluşturuldu: {self.ad}")
# Belki sadece bilgileri almak için metotlar olabilir
# ama genellikle public nitelikler yeterlidir.
Sorumluluk 2: Kullanıcı Veritabanı İşlemleri (Repository)
class KullaniciRepository:
def init(self, db_dosyasi='kullanicilar.db'):
self.db_dosyasi = db_dosyasi
self._tablo_olustur() # Başlangıçta tabloyu kontrol et/oluştur
def _baglan(self):
"""Veritabanı bağlantısını açar."""
return sqlite3.connect(self.db_dosyasi)
def _tablo_olustur(self):
"""Kullanıcılar tablosunu oluşturur (varsa dokunmaz)."""
conn = None
try:
conn = self._baglan()
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS kullanicilar
(id INTEGER PRIMARY KEY, ad TEXT, email TEXT UNIQUE)''') # Email unique olsun
conn.commit()
except sqlite3.Error as e:
print(f"Tablo oluşturma hatası: {e}")
finally:
if conn:
conn.close()
def kaydet(self, kullanici: Kullanici):
"""Verilen Kullanici nesnesini veritabanına kaydeder/günceller."""
conn = None
try:
conn = self._baglan()
cursor = conn.cursor()
cursor.execute("INSERT OR REPLACE INTO kullanicilar (id, ad, email) VALUES (?, ?, ?)",
(kullanici.id, kullanici.ad, kullanici.email))
conn.commit()
print(f"'{kullanici.ad}' veritabanına kaydedildi/güncellendi.")
return True
except sqlite3.Error as e:
print(f"Veritabanına kaydetme hatası: {e}")
return False
finally:
if conn:
conn.close()
def getir(self, id: int) -> Kullanici | None:
"""ID'ye göre kullanıcıyı getirir."""
conn = None
try:
conn = self._baglan()
cursor = conn.cursor()
cursor.execute("SELECT id, ad, email FROM kullanicilar WHERE id=?", (id,))
sonuc = cursor.fetchone()
if sonuc:
return Kullanici(sonuc[0], sonuc[1], sonuc[2]) # Yeni Kullanici nesnesi oluştur
return None
except sqlite3.Error as e:
print(f"Veritabanından getirme hatası: {e}")
return None
finally:
if conn:
conn.close()
Kullanım
k1_yeni = Kullanici(1, "Ahmet Yılmaz", "[email protected]")
k2_yeni = Kullanici(2, "Ayşe Kara", "[email protected]")
Kaydetme işlemi için Repository nesnesini kullan
repo = KullaniciRepository()
repo.kaydet(k1_yeni)
repo.kaydet(k2_yeni)
Veritabanından kullanıcı getirme
getirilen_kullanici = repo.getir(1)
if getirilen_kullanici:
print(f"\nGetirilen Kullanıcı: ID={getirilen_kullanici.id}, Ad={getirilen_kullanici.ad}")
Çözümün Faydaları:
Kullanici sınıfı artık veritabanı detaylarından tamamen habersiz. Sadece veriyi tutuyor.
KullaniciRepository sınıfı sadece veritabanı işlemleriyle ilgileniyor. Veritabanı değişirse sadece bu sınıfı değiştirmek yeterli.
Her iki sınıf da ayrı ayrı daha kolay test edilebilir. KullaniciRepository'yi test etmek için sahte (mock) Kullanici nesneleri kullanılabilir. Kullanici sınıfı ise veritabanı olmadan test edilebilir.
Kod daha modüler ve organize.
Bölüm 3: Örnek 2 — Raporlama: Formatlama ve Gönderme
Bir sınıfın hem rapor içeriğini oluşturması (formatlaması) hem de bu raporu farklı kanallardan (e-posta, dosya vb.) göndermesi/kaydetmesi de yaygın bir SRP ihlalidir.
İhlal Örneği:
SRP İhlali
import smtplib # E-posta gönderme için (sadece örnek)
from email.mime.text import MIMEText
class Raporlayici:
def init(self, veri):
self.veri = veri # Liste veya sözlük olduğunu varsayalım
self.rapor_icerigi = ""
# --- SORUMLULUK 1: FORMATLAMA ---
def html_formatla(self):
print("Veri HTML olarak formatlanıyor...")
# Basit bir HTML formatlama
icerik = "
Rapor
"
self.rapor_icerigi = icerik
print("HTML formatlama tamamlandı.")
def metin_formatla(self):
print("Veri düz metin olarak formatlanıyor...")
icerik = "--- Rapor ---\n"
for anahtar, deger in self.veri.items():
icerik += f"- {anahtar}: {deger}\n"
self.rapor_icerigi = icerik
print("Metin formatlama tamamlandı.")
# --- SORUMLULUK 2: GÖNDERME / KAYDETME ---
def email_gonder(self, gonderen, alici, konu):
if not self.rapor_icerigi:
print("Hata: Önce rapor formatlanmalı.")
return
print(f"Rapor '{alici}' adresine e-posta ile gönderiliyor...")
# Gerçek e-posta gönderme kodu (basitleştirilmiş)
try:
msg = MIMEText(self.rapor_icerigi, 'html' if "
Neden İhlal Ediliyor?
Birden Fazla Değişme Nedeni:
Rapor formatlama mantığı değişirse (yeni format eklenirse — CSV, XML vb. — veya mevcut formatların yapısı değişirse).
Raporu gönderme/kaydetme şekli değişirse (e-posta yerine Slack’e gönderme, dosya yerine veritabanına kaydetme, e-posta sunucu ayarları değişirse vb.).
Farklı Sorumluluklar: Veriyi belirli bir yapıya dönüştürmek (formatlama) ile bu dönüştürülmüş veriyi bir hedefe iletmek (gönderme/kaydetme) birbirinden bağımsız işlerdir.
Test Zorluğu: Raporlayici
sınıfını test etmek için hem formatlama hem de gönderme/kaydetme senaryolarını düşünmek gerekir. E-posta gönderme gibi dış sistemlere bağımlılıklar testi karmaşıklaştırır.
Çözüm: Formatlayıcı ve Gönderici/Yazıcı Sınıfları
Çözüm, formatlama işini yapan ayrı sınıflar (veya bir sınıf ailesi) ve raporu hedefe ileten ayrı sınıflar oluşturmaktır.
SRP Uyumlu Çözüm
import abc
import json # Önceki örnekten kalmış olabilir, bu örnekte gerek yok
import smtplib
from email.mime.text import MIMEText
--- Arayüzler (Kontratlar) ---
class IRaporFormatlayici(abc.ABC):
@abc.abstractmethod
def formatla(self, veri: dict) -> str: pass
class IRaporGonderici(abc.ABC):
@abc.abstractmethod
def gonder(self, icerik: str, hedef_bilgisi: str): pass
--- Formatlama Implementasyonları ---
class HtmlFormatlayici(IRaporFormatlayici):
def formatla(self, veri: dict) -> str:
print("(HTML Formatlama)")
icerik = "
Rapor
class MetinFormatlayici(IRaporFormatlayici):
def formatla(self, veri: dict) -> str:
print("(Metin Formatlama)")
icerik = "--- Rapor ---\n"
for anahtar, deger in veri.items():
icerik += f"- {anahtar}: {deger}\n"
return icerik
--- Gönderme/Kaydetme Implementasyonları ---
class EmailGonderici(IRaporGonderici):
def init(self, gonderen_adres, smtp_sunucu="smtp.example.com"):
self.gonderen_adres = gonderen_adres
self.smtp_sunucu = smtp_sunucu
def gonder(self, icerik: str, hedef_bilgisi: str): # hedef_bilgisi = alici_email
alici_email = hedef_bilgisi
print(f"'{alici_email}' adresine e-posta ile gönderiliyor...")
# Gerçek e-posta gönderme kodu
try:
# HTML mi metin mi olduğunu belirle (basit kontrol)
msg_type = 'html' if icerik.strip().startswith('
Çözümün Faydaları:
Her sınıfın (HtmlFormatlayici, EmailGonderici vb.) tek bir sorumluluğu vardır.
Yeni bir format eklemek (örn: CsvFormatlayici) veya yeni bir gönderme yöntemi eklemek (örn: SlackGonderici) sadece yeni bir sınıf yazmayı gerektirir, mevcutları değiştirmez (OCP).
Formatlama mantığı, gönderme mantığından tamamen ayrılmıştır.
Her bir bileşen (formatlayıcı, gönderici) ayrı ayrı test edilebilir.
RaporlamaServisi, hangi formatlayıcı ve göndericinin kullanılacağını bilmez, sadece arayüzlere (ABCs) bağımlıdır (DIP).
Bölüm 5: SRP ve Fonksiyonlar/Modüller
SRP sadece sınıflar için değil, fonksiyonlar ve modüller için de geçerlidir:
Fonksiyonlar: İdeal bir fonksiyon tek bir iş yapmalıdır. Bir fonksiyon hem kullanıcıdan girdi alıyor, hem hesaplama yapıyor, hem de sonucu ekrana basıyorsa, bu görevler muhtemelen ayrı fonksiyonlara bölünmelidir.
Modüller: Bir Python dosyası (modül) genellikle birbiriyle yakından ilişkili işlevleri barındırmalıdır. Eğer bir modül hem matematiksel hesaplamalar, hem dosya işlemleri, hem de ağ bağlantısı kodları içeriyorsa, bu sorumluluklar farklı modüllere ayrılabilir.
Bölüm 6: Sonuç: Odaklanmış Kodun Gücü
Tek Sorumluluk Prensibi (SRP), temiz, sürdürülebilir ve anlaşılır kod yazmanın temel taşlarından biridir. Bir sınıfın veya modülün değişmesi için tek bir neden olması gerektiği fikri, kodumuzu daha küçük, daha odaklanmış ve daha yönetilebilir parçalara ayırmamızı teşvik eder.
Bu rehberdeki örneklerde gördüğümüz gibi, SRP ihlalleri genellikle bir sınıfın birden fazla, birbirinden bağımsız işlevi üstlenmesiyle ortaya çıkar. Bu durum, kodun bakımını zorlaştırır, test edilmesini karmaşıklaştırır ve değişikliklerin beklenmedik yan etkilere yol açma riskini artırır.
Çözüm, bu farklı sorumlulukları tespit etmek ve onları kendi sınıflarına veya modüllerine ayırmaktır. Bu ayrıştırma işlemi sonucunda:
Her bileşenin amacı netleşir.
Kodun okunabilirliği artar.
Değişikliklerin etki alanı sınırlanır.
Testler daha kolay yazılır ve yönetilir.
Bileşenlerin yeniden kullanılabilirliği artar.
SRP'yi uygulamak, bazen başlangıçta daha fazla sınıf veya dosya oluşturmayı gerektirse de, projenin yaşam döngüsü boyunca sağladığı faydalar (daha kolay bakım, daha az hata, daha yüksek esneklik) bu başlangıç maliyetini fazlasıyla karşılar. Kodunuzu tasarlarken veya yeniden düzenlerken "Bu sınıfın/fonksiyonun değişmesi için kaç neden var?" sorusunu sormak, SRP'ye giden yolda iyi bir başlangıç noktasıdır.
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Özgeçmiş
Github
Github
Linkedin