Java OOP’ta bir davranışın hangi sınıfa ait olduğuna karar verirken yalnızca “Bu metot hangi veriyi kullanıyor?” sorusu yeterli değildir. Veri sahipliği, değişim nedeni ve bağımlılıklar birlikte değerlendirilmelidir. Davranış, üzerinde işlem yaptığı veriyi en iyi bilen ve bu davranışın değişmesi gerektiğinde en doğal biçimde güncellenecek sınıfa yerleştirilmelidir.
Pratik bir karar için şu soruyu sorun: “Bu davranışın kuralları kimin sorumluluğunda ve bu kural hangi nesnenin iç durumunu koruyor?” Cevap, çoğu zaman davranışın veriye en yakın sınıfta bulunması gerektiğini gösterir; ancak bu yaklaşım her işlemi tek bir sınıfa doldurmak anlamına gelmez.
Bir Davranış Hangi Sınıfa Aittir?
Bir sınıfa sorumluluk verirken üç ölçütü aynı anda incelemek gerekir: davranışın kullandığı verinin sahibi kim, davranış hangi nedenle değişebilir ve davranış hangi sınıflara bağımlı? Bu üç soru, “Metodu nereye yazmalıyım?” kararını ezberlenmiş kurallardan çıkarıp görünür bir tasarım değerlendirmesine dönüştürür.
1. Davranışın kullandığı verinin sahibi kim?
Bir davranış doğrudan belirli bir nesnenin durumunu değiştiriyor veya o durum hakkında karar veriyorsa, ilk aday genellikle o nesnenin sınıfıdır. Örneğin bir hesabın para çekme işlemi, hesabın bakiyesini ve işlem kısıtlarını bilmelidir. Bu nedenle para çekme kuralını dışarıdaki bir servis yerine BankAccount sınıfına yerleştirmek daha anlamlıdır.
Buradaki “sahiplik” yalnızca alanın sınıf içinde bulunması değildir. Sınıf, o verinin hangi koşullarda değişebileceğini de bilmelidir. balance alanı dışarıya açık bırakılır ve herkes tarafından değiştirilebilirse, alan sınıfın içinde durduğu hâlde davranış üzerindeki kontrol kaybolur.
2. Davranış hangi nedenle değişebilir?
İki sınıfın farklı değişim nedenleri varsa, bu sınıflara ait kuralları gereksiz yere bir araya getirmemek gerekir. Örneğin bir kütüphanede kitabın başlığının değiştirilmesiyle gecikme cezası hesaplama kuralının değişmesi aynı nedene bağlı değildir. Bu iki davranışı tek bir “genel kütüphane yöneticisi” sınıfında toplamak, zamanla ilgisiz kuralların aynı yerde birikmesine yol açabilir.
Bu ölçüt, sınıfın yalnızca kaç satır kod içerdiğinden daha değerlidir. Küçük görünen bir sınıf bile farklı değişim nedenlerini bir araya getiriyorsa tasarım açısından dağınık olabilir. Tersine, belirli bir iş kuralını tutarlı biçimde yöneten daha büyük bir sınıf, sorumlulukları arasında güçlü bir ilişki varsa anlaşılır kalabilir.
3. Davranış hangi bağımlılıklara ihtiyaç duyuyor?
Bir metot çok sayıda sınıftan veri topluyor, bu sınıfların iç ayrıntılarını biliyor ve onların durumunu ayrı ayrı değiştiriyorsa yanlış yerde olma ihtimali artar. Özellikle bir sınıf başka nesnelerden sürekli olarak getter çağırıyor ve bu verileri kullanarak onların adına karar veriyorsa, davranışın gerçek sahibi başka bir nesne olabilir.
Örneğin aşağıdaki yaklaşımda ödeme kararı dışarıdaki bir sınıf tarafından veriliyor:
if (account.getBalance() >= amount && !account.isBlocked()) {
account.setBalance(account.getBalance() - amount);
}
Bu kod çalışabilir; ancak hesap bakiyesinin yeterli olup olmadığını ve hesabın bloke durumunu kontrol etme bilgisi dışarıya taşınmıştır. Daha sonra minimum bakiye, günlük limit veya işlem geçmişi gibi kurallar eklendiğinde bu kararları veren sınıf hesabın iç ayrıntılarına daha fazla bağımlı hâle gelir.
Daha dengeli yaklaşım, kararın hesabın kendi sorumluluğunda olmasıdır:
public final class BankAccount {
private double balance;
private boolean blocked;
public BankAccount(double balance) {
this.balance = balance;
}
public void withdraw(double amount) {
if (amount <= 0 || blocked || amount > balance) {
throw new IllegalArgumentException("Geçersiz para çekme işlemi");
}
balance -= amount;
}
public double getBalance() {
return balance;
}
}
Bu örnekte withdraw davranışı bakiyeye ve bloke durumuna doğrudan erişmektedir. Hesap, kendi durumunu koruduğu için dışarıdaki kodun “önce kontrol et, sonra değiştir” sırasını bilmesine gerek kalmaz. Böylece kurala ilişkin sorumluluk tek bir noktada toplanır.
Karar vermek için kısa çerçeve
- Davranış hangi alanları okuyor veya değiştiriyor?
- Bu alanların geçerli kalmasını sağlamak kimin görevi?
- Kural değiştiğinde hangi sınıfın değişmesi beklenir?
- Davranış başka bir sınıfın iç ayrıntılarını bilmeden çalışabilir mi?
- Metot, bir nesnenin işini yapmak yerine birçok nesneyi koordine mi ediyor?
İlk dört sorunun cevabı aynı sınıfı gösteriyorsa davranışın o sınıfa ait olma ihtimali yüksektir. Son sorunun cevabı “evet” ise davranış bir koordinasyon görevi olabilir. Bu durumda davranışı doğrudan veri sınıfına taşımak yerine, sınıflar arası iş birliğini yöneten ayrı bir nesne gerekebilir. Ancak yalnızca bir metodu başka yere taşımak için gereksiz bir yardımcı sınıf üretmek de tasarım kalitesini otomatik olarak artırmaz.
Önemli ayrım şudur: Bir sınıfın veriyi kullanması, davranışın mutlaka o sınıfa ait olduğu anlamına gelmez. Davranış verinin iç kuralını uyguluyorsa veri sahibi sınıfa; farklı nesneleri belirli bir akışta buluşturuyorsa koordinasyon yapan sınıfa daha yakın olabilir. Karar, metodun kaç nesneye dokunduğundan çok hangi iş kuralını temsil ettiğine göre verilmelidir.
Kapsülleme ve Veriye En Yakın Davranış

Kapsülleme, alanları private yapmakla başlayıp biten bir işlem değildir. Asıl amaç, bir nesnenin geçerli durumunu koruyan kuralları o nesnenin kontrolünde tutmaktır. Bunun için veriye doğrudan erişimi sınırlamak ve verinin nasıl okunup değiştirileceğini anlamlı davranışlarla belirlemek gerekir.
Veriye en yakın davranış yaklaşımı, bir davranışın ihtiyaç duyduğu bilgiyi en iyi bilen nesnede bulunmasını önerir. Bu yaklaşımın iki önemli sonucu vardır: sınıfın iç tutarlılığı yükselir ve sınıflar arası gereksiz bağımlılık azalır.
Yüksek iç tutarlılık ne sağlar?
İç tutarlılık, bir sınıfın içindeki alanların ve metotların aynı amaca hizmet etmesiyle ilgilidir. Bir BankAccount sınıfında bakiye artırma, azaltma ve işlem sınırlarını kontrol etme davranışlarının bulunması doğaldır; çünkü bu davranışların tamamı hesabın finansal durumunu korumaya yöneliktir.
Buna karşılık aynı sınıfa e-posta gönderme, PDF raporu oluşturma ve dosya adlandırma gibi farklı nedenlerle değişecek işlemleri eklemek iç tutarlılığı zayıflatır. Sınıfın adı hâlâ anlamlı görünse bile, değişmesi gereken her yeni kural başka bir sorumluluğu da etkileyebilir.
Düşük bağımlılık neden önemlidir?
Düşük bağımlılık, bir sınıfın başka sınıfların ayrıntılarına mümkün olduğunca az ihtiyaç duymasıdır. Dışarıdaki bir sınıf bir nesnenin alanlarına, getter zincirlerine veya değişiklik sırasına ne kadar çok bağımlı olursa, küçük bir iç değişiklik daha geniş bir kod alanını etkileyebilir.
Örneğin şu kullanım, siparişin toplamını hesaplayan kodun sipariş içindeki her ürünün fiyat ve miktar ayrıntısını bilmesini gerektirir:
double total = 0;
for (OrderItem item : order.getItems()) {
total += item.getProduct().getPrice() * item.getQuantity();
}
Bu kod her zaman yanlış değildir. Ancak toplam hesaplama, siparişin temel iş kuralıysa davranışın Order sınıfında bulunması daha korunaklı olabilir:
public double calculateTotal() {
double total = 0;
for (OrderItem item : items) {
total += item.calculateSubtotal();
}
return total;
}
Bu tasarımda Order, toplamın nasıl elde edildiğini yönetir; OrderItem ise kendi ara toplamını hesaplar. Ürün fiyatının nasıl temsil edildiği, indirim bilgisinin nerede tutulduğu veya miktarın hangi kurallara uyduğu değiştiğinde tüm kodun aynı ayrıntıları tekrar etmesi gerekmez.
Kapsülleme, veriyi her koşulda saklamak değil, veriye erişimin anlamını korumaktır. Örneğin getItems() metodunun doğrudan değiştirilebilir bir liste döndürmesi, siparişin iç durumunu dışarıya açabilir. Böyle bir durumda dış kod, siparişin kurallarını atlayarak listeye geçersiz bir öğe ekleyebilir. Daha güvenli bir arayüz, “ürün ekle” veya “öğeyi kaldır” gibi geçerli işlemleri temsil eden davranışlar sunar.
Ödünç alma örneğiyle sorumluluğu görmek
Bir kütüphane uygulamasında kitabın ödünç verilebilir olup olmadığını kontrol eden kuralı düşünelim. Bu kural yalnızca kitabın durumuna bakıyorsa davranış Book sınıfına yakın olabilir. Üyenin aktif ödünç sayısını, üyelik durumunu ve gecikmiş kayıtlarını da hesaba katıyorsa karar iki nesnenin bilgisini birleştirir.
Bu noktada bütün kontrolleri tek bir sınıfa yığmak yerine kuralı parçalara ayırmak daha sağlıklıdır:
Book, kitabın şu anda ödünçte olup olmadığını bilir.Member, üyenin yeni bir ödünç alıp alamayacağını bilir.- Ödünç verme işlemini başlatan koordinasyon kodu, bu iki kararın sonucunu bir araya getirir.
Bu ayrım, “her sınıf yalnızca tek bir metot içersin” anlamına gelmez. Ama her sınıfın kendi durumunu geçerli tutan kuralları koruması beklenir. Java OOP öğrenirken Java bilgi testi gibi kısa alıştırmalarla sınıf, metot, erişim belirleyicisi ve nesne ilişkileri konusundaki eksikleri görmek, bu sorumluluk kararlarını daha bilinçli değerlendirmeye yardımcı olabilir.
Veriye yakınlık tek başına yeterli değildir
Her veriye yakın davranışı doğrudan veri sınıfına eklemek, zamanla büyük ve karmaşık sınıflar oluşturabilir. Örneğin Book sınıfına kitabı veritabanına kaydetme, CSV’ye aktarma, HTML olarak biçimlendirme ve bildirim gönderme davranışlarını eklemek veriye yakınlık ilkesini yanlış yorumlamaktır.
Burada kitap nesnesi kendi başlık, yazar veya durum bilgilerini yönetebilir; fakat kalıcı depolama ve çıktı üretimi farklı değişim nedenlerine sahiptir. Gerektiğinde bu işlemler başka bileşenlerle yürütülebilir. Yine de yalnızca “bu sorumluluk ayrı sınıf olmalı” diyerek her küçük işlem için yeni bir yardımcı sınıf oluşturulmamalıdır.
Yeni bir sınıf eklemeden önce şu maliyetleri değerlendirin:
- Yeni sınıf gerçekten bağımsız bir kavramı mı temsil ediyor?
- Bu sınıfın kendi durumu veya anlamlı bir davranışı var mı?
- Eklenen soyutlama kodu okumayı kolaylaştırıyor mu?
- Yoksa yalnızca birkaç satırı başka bir dosyaya mı taşıyor?
- Yeni sınıf, mevcut sınıflar arasındaki bağımlılığı azaltıyor mu?
İyi tasarım, sorumlulukları rastgele dağıtmak değil; değişim nedenlerini, veri sahipliğini ve nesneler arası bağımlılığı dengeli biçimde düzenlemektir. Veriye en yakın davranış çoğu zaman iyi bir başlangıç noktasıdır, fakat son karar davranışın anlamı ve sınıfın bütün sorumlulukları birlikte incelenerek verilmelidir.
Constructor ve Nesneler Arası İş Birliği
Bir nesnenin çalışabilmesi için ihtiyaç duyduğu bilgileri constructor üzerinden almak, sınıf sorumluluklarını görünür hâle getirir. Ancak constructor’a her şeyi doldurmak da doğru değildir. İyi bir tasarımda nesne, görevini yerine getirmek için gerçekten ihtiyaç duyduğu verileri alır; başka bir nesnenin iç ayrıntılarını yönetmeye başlamaz.
Örneğin bir ödünç verme işlemi için Loan nesnesinin hangi kitabın, hangi üyenin ve hangi tarihte ödünç verildiğini bilmesi gerekebilir. Bu bilgiler constructor ile geçirilebilir:
Loan loan = new Loan(book, member, today);
Buradaki amaç yalnızca alanları doldurmak değildir. Constructor, nesnenin geçerli bir başlangıç durumuna sahip olmasını sağlar. Bir ödünç kaydı kitapsız veya üyesiz oluşturulamıyorsa, bu kuralı constructor seviyesinde korumak daha güvenlidir.
Veri geçirmek ile işi başka sınıfa devretmek aynı şey değildir
Constructor’a başka bir nesne vermek, otomatik olarak kötü bir bağımlılık anlamına gelmez. Önemli olan, verilen nesnenin hangi amaçla kullanıldığıdır. Bir Loan nesnesinin Book nesnesini bilmesi anlamlı olabilir; çünkü ödünç alınan kitap, ödünç kaydının doğal bir parçasıdır.
Buna karşılık Loan sınıfının kütüphanedeki bütün kitapları araması, üyelerin listesini değiştirmesi ve gecikme ücretlerini hesaplaması sorumlulukların karıştığını gösterir. Böyle bir sınıf, kendisine verilen nesnelerle iş birliği yapmak yerine sistemin tamamını yönetmeye başlar.
Nesneler arası iş birliği şu soruya cevap vermelidir: “Bu işlem için hangi nesne hangi bilgiyi sağlamalı?” Örneğin:
Book, kitabın uygun durumda olup olmadığını bilir.Member, üyenin yeni bir kitap ödünç alıp alamayacağını kontrol edebilir.Loan, ödünç ilişkisinin kendisini ve tarih bilgilerini tutabilir.- İşlemi başlatan servis veya uygulama akışı, bu nesneleri doğru sırayla bir araya getirebilir.
Burada amaç her davranış için yeni bir sınıf oluşturmak değildir. Küçük bir işlem için hemen BookValidator, MemberPolicy, LoanCoordinator ve DateHelper gibi sınıflar üretmek, başlangıçta düzenli görünse de kodu anlamayı zorlaştırabilir.
Gereksiz yardımcı sınıfın maliyeti
Bir yardımcı sınıf ancak belirgin ve tekrar kullanılabilir bir sorumluluk taşıyorsa değerlidir. Örneğin tarih aralığı hesaplama işlemi birçok farklı yerde kullanılıyorsa ayrı bir bileşene ayrılabilir. Fakat yalnızca iki satırlık bir kontrol için yeni bir sınıf oluşturmak şu maliyetleri doğurabilir:
- Bir işlemi takip etmek için birden fazla dosya arasında dolaşma zorunluluğu,
- Constructor bağımlılıklarının gereksiz şekilde artması,
- Basit kuralların isimler ve katmanlar arasında kaybolması,
- Test edilmesi gereken küçük sınıf sayısının çoğalması.
Bu nedenle önce davranışın en doğal sahibini belirlemek, sonra gerçekten ihtiyaç varsa ayırmak daha sağlıklıdır. “Her sınıf küçük olmalı” kuralı, “her satır ayrı sınıfa taşınmalı” anlamına gelmez.
Pratik bir karar ölçütü olarak şu üç soruyu kullanabilirsiniz:
- Bu davranışın kullandığı veriler en çok hangi nesnede bulunuyor?
- Davranış başka bir sınıfa taşınırsa anlamı daha mı netleşecek, yoksa veri sürekli ileri geri mi taşınacak?
- Yeni sınıf bağımsız olarak tekrar kullanılacak mı, yoksa yalnızca mevcut sınıfın birkaç satırını mı saklayacak?
İlk sorunun cevabı çoğunlukla davranışın sahibi için güçlü bir ipucu verir. İkinci ve üçüncü sorular ise gereksiz soyutlamayı engeller.
Tek Sınıflı Kütüphane Taslağını İncelemek

Başlangıçta bütün işlemleri tek bir Library sınıfında toplamak hızlı bir prototip hazırlamak için anlaşılabilir bir tercihtir. Sorun, bu taslağın büyüdükçe fark edilmeden sistemin tamamını yöneten bir sınıfa dönüşmesidir.
Aşağıdaki örnekte sınıf kitap ekliyor, üye kaydediyor ve ödünç verme işlemini yürütüyor. Kod kısa olduğu için ilk bakışta sorunlu görünmeyebilir:
import java.util.ArrayList;
import java.util.List;
public class LibraryDemo {
static class Library {
List<String> books = new ArrayList<>();
List<String> members = new ArrayList<>();
void addBook(String title) { books.add(title); }
void addMember(String name) { members.add(name); }
void loan(String title, String member) {
if (books.contains(title) && members.contains(member))
System.out.println(member + " borrowed " + title);
}
}
public static void main(String[] args) {
Library library = new Library();
library.addBook("Clean Code");
library.addMember("Ayşe");
library.loan("Clean Code", "Ayşe");
}
}
Beklenen çıktı şöyledir:
Ayşe borrowed Clean Code
Bu örnek çalışır; fakat gerçek bir uygulamanın ihtiyaçları arttığında Library sınıfının sorumlulukları hızla genişleyebilir. Kitabın müsaitlik durumu, üyenin ödünç alma limiti, teslim tarihi, gecikme hesabı, kayıt silme, arama ve raporlama gibi davranışların tümü aynı sınıfa eklenirse sınıfın değişme nedenleri çoğalır.
Büyüme riski nerede başlar?
İlk uyarı işareti, sınıfın farklı türde verileri doğrudan yönetmeye başlamasıdır. Yukarıdaki örnekte List<String> kullanımı basitliği korur; ancak kitapla ilgili her bilgiyi tek bir metne sığdırmak mümkün değildir. Kısa süre sonra şu tür ihtiyaçlar ortaya çıkabilir:
- Kitabın yazarı, ISBN bilgisi ve mevcut olup olmadığı,
- Üyenin iletişim bilgileri ve aktif ödünç sayısı,
- Ödünç işleminin başlangıç ve teslim tarihleri,
- Aynı kitabın birden fazla kopyası,
- Geçersiz işlem durumlarında kullanıcıya anlamlı hata verilmesi.
Bu noktada sınıfı yalnızca satır sayısına bakarak bölmek doğru değildir. Asıl soru, hangi bilgilerin ve davranışların birlikte değiştiğidir. Kitabın müsaitlik durumunu kontrol eden davranış, kitap verisine yakın durabilir. Üyenin ödünç alma hakkını kontrol eden davranış, üye bilgisine daha yakındır. Ödünç işleminin tarihlerini ve ilişkisini tutan davranış ise ayrı bir kavram hâline gelebilir.
Örneğin Library sınıfı yalnızca kitap, üye ve ödünç kayıtlarını bir araya getiren bir koordinasyon noktası olarak kalabilir. Fakat kitabın iç durumunu değiştirmek için her seferinde Library içindeki alanlara doğrudan erişmek, kapsüllemeyi zayıflatır. Bu durumda nesneler iş birliği yapmıyor; tek bir sınıf diğer nesnelerin yerine karar veriyor demektir.
Refactoring kararı nasıl verilir?
Tek sınıflı taslağı hemen çok sayıda sınıfa bölmek yerine önce değişim noktalarını listelemek daha doğru olur. Aşağıdaki belirtilerden birkaçını görüyorsanız sorumlulukları yeniden değerlendirme zamanı gelmiş olabilir:
- Bir metot aynı anda kitap, üye ve ödünç bilgilerini değiştiriyorsa,
- Bir davranışın çalışması için sınıfın birçok alanına doğrudan erişiliyorsa,
- Yeni bir kural eklemek daha önce çalışan işlemleri değiştirmeyi gerektiriyorsa,
- Test yazmak için sınıfın bütün listelerini ve yardımcı metotlarını hazırlamak gerekiyorsa,
- Metot adları farklı olsa da çoğu işlem aynı verileri tekrar tekrar doğruluyorsa.
Bu belirtiler, “daha fazla sınıf ekle” emri değildir. Önce davranışın gerçek sahibini bulmayı gerektirir. Küçük bir uygulamada Library içinde birkaç işlem kalabilir; daha kapsamlı bir tasarımda ise Book, Member ve Loan gibi kavramların kendi durumlarını koruması daha anlaşılır olabilir.
Book, Member ve Loan Sorumluluklarını Ayırmak
Bir kütüphane taslağındaki her davranışı doğrudan Library sınıfına eklemek kısa vadede kolay görünür; ancak sınıf büyüdükçe kitap bilgisi, üye bilgisi, ödünç alma kuralları ve raporlama işlemleri aynı yerde toplanır. Daha sağlıklı ayrım şudur: Book kitapla ilgili verileri ve davranışları, Member üyeye ait bilgileri, Loan ise ödünç alma ilişkisini temsil eder. Library sınıfı ise bu nesneler arasındaki iş akışını koordine edebilir.
Buradaki amaç her birkaç satırda bir yeni sınıf oluşturmak değildir. Amaç, aynı nedenle değişen kodları bir arada tutmak ve bir davranışın gereksiz nesnelere bağımlı olmasını önlemektir. Java OOP çalışırken bu ayrımı yalnızca sınıf adlarına bakarak değil; verinin kime ait olduğunu, işlemin neyi değiştirdiğini ve hangi nesnelere ihtiyaç duyduğunu inceleyerek yapmak gerekir.
Book sınıfı hangi sorumlulukları taşımalıdır?
Book sınıfının temel konusu kitabın kendisidir. Kitabın başlığı, ISBN bilgisi, yazarı veya mevcut olup olmadığı gibi alanlar bu nesneye aittir. Kitabın ödünç verilebilir durumda olup olmadığını değiştiren davranışlar da çoğunlukla yine Book içinde bulunabilir.
- Kitabın kimliğini ve temel bilgilerini saklamak
- Kitabın ödünç verilebilir durumda olup olmadığını bilmek
- Kitabı ödünç verme veya iade alma sırasında kendi durumunu güncellemek
- Geçersiz bir durum değişikliğini engellemek
Örneğin bir kitap zaten ödünç verilmişse aynı kitabın ikinci kez ödünç verilmesini engellemek, kitabın durumunu en yakından bilen nesnenin sorumluluğudur. Bu kontrolü her çağıran sınıfa dağıtırsanız bir yerde available, başka bir yerde borrowed kontrolü yapılabilir. Zamanla aynı iş kuralının farklı biçimlerde yazılması mümkün olur.
Member sınıfı neyi bilmeli, neyi bilmemelidir?
Member sınıfı üyenin kimliğini ve üyeye ait kuralları temsil eder. Üyenin adı, üyelik numarası veya iletişim bilgileri bu sınıfta tutulabilir. Üyenin aynı anda kaç kitap ödünç alabileceği gibi bir kural da doğrudan üyeye ait kabul edilebilir; ancak bu kuralın uygulanması için aktif ödünç kayıtlarının nerede tutulduğu ayrıca düşünülmelidir.
Örneğin Member sınıfına bütün kütüphanedeki kitapları ve diğer üyeleri yüklemek doğru bir ayrım değildir. Bir üyenin hangi kitapları ödünç aldığını bilmek gerekiyorsa bunu doğrudan kitap listesini kopyalayarak değil, Loan nesneleri veya bu kayıtları yöneten bir servis üzerinden modellemek daha anlaşılır olabilir.
Member, üyenin temel kimlik bilgilerini taşımalıdır.- Üyeye özel basit kurallar, mümkünse üyenin kendi davranışları olarak tanımlanmalıdır.
- Üyenin tüm kütüphane verisine erişmesi engellenmelidir.
- Üyelik ile ödünç alma ilişkisi birbirine karıştırılmamalıdır.
Bu yaklaşım, canlı sınıfta yapılan kod incelemelerinde de önemli bir düşünme alışkanlığıdır: Bir nesnenin bir veriye erişebiliyor olması, o verinin sahibi olduğu anlamına gelmez. Öğrenciler bu ayrımı canlı sınıflı yazılım eğitimleri sırasında örnekleri küçük parçalara ayırarak daha görünür hâle getirebilir.
Loan neden ayrı bir sınıf olmalıdır?
Loan, yalnızca “kitap ödünç verildi” bilgisinden ibaret değildir. Bir kitabın belirli bir üyeye, belirli bir tarihte verilmesi iki nesne arasındaki ilişkiyi oluşturur. Bu ilişkinin başlangıç tarihi, son teslim tarihi, iade tarihi veya gecikme durumu gibi bilgileri vardır. Bu nedenle bu alanları hem Book hem de Member içine dağıtmak yerine ayrı bir nesneyle temsil etmek daha doğru olabilir.
Bir davranış birden fazla nesne arasındaki ilişkiye aitse, ilişkiyi temsil eden sınıf iyi bir adaydır. Örneğin “ödünç işleminin gecikmiş olup olmadığını hesaplama” doğrudan kitabın özelliği değildir. Kitap tek başına son teslim tarihini bilmez. Aynı şekilde üye de hangi ödünç işleminin hangi tarihte başladığını bilmek zorunda değildir. Bu bilgiler Loan içinde anlamlı hâle gelir.
- Hangi kitabın ödünç alındığını göstermek
- Hangi üyenin kitabı aldığını göstermek
- Ödünç alma ve son teslim tarihlerini saklamak
- İade edilip edilmediğini veya gecikme durumunu hesaplamak
- İade işlemi sırasında ödünç kaydının durumunu güncellemek
Aşağıdaki kısa örnekte üç sınıfın temel ayrımı görülür. Book kendi kullanılabilirlik durumunu yönetirken, Loan kitap ile üye arasındaki ilişkiyi taşır. Üç sınıf da aynı dosyada iç içe tanımlandığı için örnek doğrudan çalıştırılabilir:
import java.time.LocalDate;
public class LibraryDemo {
static class Book {
String title; boolean available = true;
Book(String title) { this.title = title; }
void lend() { if (!available) throw new IllegalStateException("Kitap ödünçte"); available = false; }
}
static class Member {
String name;
Member(String name) { this.name = name; }
}
static class Loan {
Book book; Member member; LocalDate date;
Loan(Book book, Member member) { this.book = book; this.member = member; this.date = LocalDate.now(); }
}
public static void main(String[] args) {
Book book = new Book("Clean Code");
Member member = new Member("Ayşe");
book.lend();
Loan loan = new Loan(book, member);
System.out.println(loan.member.name + " -> " + loan.book.title);
}
}
Beklenen çıktı, üye ile kitabın aynı ödünç kaydında buluştuğunu gösterir:
Ayşe -> Clean Code
Bu örnekte Library sınıfı bulunmuyor. Gerçek bir uygulamada kütüphane; kitapları, üyeleri ve ödünç kayıtlarını saklayabilir, uygun nesneleri bulabilir ve işlem sırasını yönetebilir. Ancak kitabın “ödünçte olup olmadığını” veya ödünç kaydının “hangi üyeye ait olduğunu” kendisinin hesaplaması gerekmez.
Library sınıfı ne yapmalıdır?
Library sınıfı, bütün iş kurallarının sahibi olmak yerine uygulama akışını koordine eden bir nesne olabilir. Örneğin “üyeye kitap ödünç ver” işlemi sırasında şu adımları yönetebilir:
- Üyenin sistemde bulunup bulunmadığını kontrol etmek
- Kitabın mevcut olup olmadığını bulmak
- Üyenin ödünç alma koşullarını değerlendirmek
- Kitabın durumunu güncellemek
- Yeni bir
Loankaydı oluşturmak - Ödünç kaydını ilgili koleksiyona eklemek
Burada Library işlemin koordinatörüdür; fakat her ayrıntılı kararın sahibi değildir. Kitabın durumunu Book, ödünç ilişkisinin tarihlerini Loan, üyeye ait sınırlamaları ise uygun ölçüde Member yönetebilir.
Bu ayrımın önemli bir sonucu vardır: Bir davranışın uygulanması için gereken bağımlılık sayısı azalır. Örneğin yalnızca kitabın kullanılabilirliğini değiştiren bir metot, bütün kütüphaneye, tüm üye listesine ve ödünç kayıtlarının tamamına ihtiyaç duymamalıdır. Gereksiz bağımlılık, sınıfın test edilmesini ve daha sonra değiştirilmesini zorlaştırır.
Ayrımı veri sahipliği, işlem ve bağımlılıkla test etmek
Bir davranışı hangi sınıfa koyacağınıza karar verirken aşağıdaki üç soruyu birlikte sorun:
| Soru | Ne aranır? | Örnek |
|---|---|---|
| Veri kimin? | Davranışın kullandığı temel bilgi hangi nesneye ait? | Kitabın kullanılabilirlik durumu Book nesnesine aittir. |
| İşlem neyi değiştiriyor? | Davranış hangi nesnenin durumunu güncelliyor? | İade işleminde kitap yeniden kullanılabilir olur, ödünç kaydı kapanır. |
| Kaç nesne gerekiyor? | Davranış tek nesnenin özelliği mi, yoksa nesneler arası ilişki mi? | Üye-kitap ilişkisi Loan için daha uygundur. |
Bir metot bu sorulara net cevap veremiyorsa, muhtemelen birden fazla sorumluluğu aynı anda taşıyordur. Örneğin Library.calculateLateFeeAndPrintMemberReport() gibi bir metot hem gecikme hesabı yapıyor hem rapor üretiyor hem de üyeyi buluyorsa, metot adı bile sınıfın fazla büyüdüğünü gösterir.
Sınıf Büyüdüğünde Yeniden Değerlendirme
Büyük bir Java sınıfını parçalarken ilk hedef, sınıfı dosya sayısı bakımından küçültmek değil; farklı nedenlerle değişen davranışları ayırmaktır. Bir sınıfın çok satırlı olması tek başına hata değildir. Asıl sorun; aynı sınıfta birbirinden bağımsız kararların, verilerin ve dış bağımlılıkların birikmesidir.
Bu nedenle gereksinimleri sınıf isimleriyle değil, davranışlarla ayırmak daha sağlıklıdır. “Kitap sınıfı ekleyelim, üye sınıfı ekleyelim” demek başlangıç için yararlı olabilir; fakat daha güçlü soru şudur: “Sistemde hangi davranışlar var ve bu davranışların her biri hangi veriye ihtiyaç duyuyor?”
1. Sınıfın davranışlarını listeleyin
Önce büyük sınıfın içindeki metotları ve metotların ne yaptığını listeleyin. Metot adlarını yalnızca teknik görevlerine göre değil, iş davranışına göre yazın.
- Kitap ekleme
- Kitap arama
- Üye kaydetme
- Kitap ödünç verme
- Kitap iade alma
- Gecikme durumunu hesaplama
- Üye raporu oluşturma
- Dosyaya kayıt yapma
Bu liste, sınıfın neden büyüdüğünü görünür hâle getirir. “Kitap arama” ile “dosyaya kayıt yapma” aynı iş akışında kullanılsa bile aynı sorumluluk değildir. İlki koleksiyon üzerinde arama yapar, ikincisi kalıcı depolama veya dosya formatıyla ilgilenir.
2. Davranışları kullandıkları veriye göre gruplayın
Her davranışın hangi alanları okuduğunu ve hangi alanları değiştirdiğini inceleyin. Birkaç metot sürekli olarak aynı veri grubunu kullanıyorsa bu veri grubu yeni bir sınıf için aday olabilir.
title,isbnveavailablealanlarını kullanan metotlar kitap davranışları olabilir.memberId,nameve üyelik durumunu kullanan metotlar üye davranışları olabilir.borrowDate,dueDatevereturnedalanlarını kullanan metotlar ödünç kaydına taşınabilir.- Dosya yolu, JSON biçimi veya veritabanı bağlantısı kullanan metotlar alan nesnelerinden ayrılmalıdır.
Burada “aynı alanı kullanan her metot aynı sınıfa taşınmalıdır” gibi mekanik bir kural yoktur. Bir alanın kullanılması, o alanın otomatik olarak ilgili sınıfın sahibi olduğu anlamına gelmez. Davranışın anlamı, değişiklik nedeni ve nesnenin yaşam döngüsü de değerlendirilmelidir.
3. Değişim nedenlerini ayırın
Bir sınıfın hangi nedenlerle değiştiğini düşünün. Kitap ödünç alma kuralları değiştiğinde dosyaya yazma biçiminin de değişmesi gerekmiyorsa, bu iki davranışın aynı sınıfta bulunması şüphe uyandırır. Aynı şekilde rapor formatı değiştiğinde Book nesnesinin alanlarını değiştirmek zorunda kalmamalısınız.
Şu sorular teşhis için kullanılabilir:
- Bu sınıfta farklı ekip veya farklı görev sahipleri aynı anda değişiklik yapabilir mi?
- Bir özelliği değiştirmek için ilgisiz metotlara dokunmak gerekiyor mu?
- Metotlar sınıfın alanlarının yalnızca küçük bir bölümünü mü kullanıyor?
- Bir testin çalışması için çok sayıda nesne veya dış sistem kurmak gerekiyor mu?
- Yeni bir kural eklendiğinde mevcut metotlar uzun koşul bloklarıyla mı büyüyor?
Bu soruların çoğuna “evet” yanıtı veriliyorsa sınıfı davranış kümelerine ayırmayı değerlendirebilirsiniz. Ancak her “evet” yeni bir yardımcı sınıf oluşturmak için yeterli değildir. Yeni sınıfın açık bir sorumluluğu, tutarlı verileri ve anlaşılır bir yaşam döngüsü olmalıdır.
4. Bağımlılık yönünü kontrol edin
Yeni sınıflara ayırma sırasında bir başka risk de dairesel ve gereksiz bağımlılıklar oluşturmaktır. Örneğin Book nesnesinin bütün Library nesnesini tutması ve Library nesnesinin de bütün kitapları yönetmesi, basit bir işlem için gereğinden fazla bağlantı oluşturabilir.
Tercih edilebilecek daha sade akış şöyledir:
Library, ilgiliBookveMembernesnelerini bulur.- Kitabın kendi durum değişikliğini yapmasına izin verir.
Loannesnesini oluşturur.- Ödünç kaydını kendi koleksiyonunda saklar.
Bu modelde Book, kütüphanedeki tüm üyeleri bilmek zorunda kalmaz. Member, bütün kitap koleksiyonunu yönetmez. Loan ise kütüphanenin tamamını tanımadan bir kitap ve üye arasındaki kaydı temsil eder. Gerekli bağımlılıkların constructor üzerinden açıkça verilmesi, nesnenin neye ihtiyaç duyduğunu anlamayı kolaylaştırır.
5. Gereksiz soyutlamayı engelleyin
Büyük sınıfı parçalama çabası bazen tersine sonuç verebilir. Örneğin yalnızca tek bir yerde kullanılan ve anlamlı bir davranış grubu oluşturmayan bir metodu sırf dosya kısalsın diye yeni bir yardımcı sınıfa taşımak, kodun izlenmesini zorlaştırabilir.
Yeni bir sınıf oluşturmadan önce şu kontrol listesini kullanın:
- Bu sınıfın tek cümleyle açıklanabilen bir sorumluluğu var mı?
- Taşınacak metotlar ortak ve anlamlı bir veri grubuyla mı çalışıyor?
- Yeni sınıfın constructor’ı gereksiz bağımlılıklarla dolacak mı?
- Taşıma sonrasında çağıran kod daha anlaşılır mı olacak?
- Bu ayrım test yazmayı veya hatayı izlemeyi kolaylaştıracak mı?
- Yeni sınıf yalnızca bir metodu saklayan yapay bir kutuya mı dönüşüyor?
Bu değerlendirme, özellikle Java proje tasarımında sınıf sayısını artırmak ile tasarımı iyileştirmek arasındaki farkı ortaya koyar. Sınıf sayısının fazla olması otomatik olarak iyi tasarım anlamına gelmez; önemli olan her sınıfın anlamlı bir davranış bütününü taşımasıdır.
6. Küçük bir parçayı taşıyıp davranışı doğrulayın
Refactoring sırasında bütün sınıfı tek seferde yeniden yazmak yerine, bir davranış grubunu seçip taşıyın. Örneğin önce gecikme hesabını Loan içine alın. Ardından mevcut testleri çalıştırın ve sonuçların değişmediğini kontrol edin. Sonra kitap durumunu yöneten davranışları Book sınıfına taşıyabilirsiniz.
Her adımda şu üç şeyi doğrulayın:
- Eski davranış korunuyor mu?
- Yeni sınıf yalnızca ihtiyaç duyduğu verilere mi erişiyor?
- Çağıran kod daha açık hâle geliyor mu?
Java sınıflarındaki sorumluluk ayrımını örnekler ve uygulama üzerinden pekiştirmek isteyenler, birebir Java özel ders içeriğini kendi proje sorularına göre değerlendirebilir. Buradaki temel ölçüt daha fazla sınıf yazmak değil, kararların ve bağımlılıkların görünür olmasıdır.
Sık Sorulan Sorular
Her metot ayrı bir sınıfa mı taşınmalıdır?
Hayır. Bir metodun ayrı bir sınıfa taşınması için yalnızca uzun olması yeterli değildir. Metot, anlamlı bir sorumluluk grubuna ait olmalı, belirli verilerle tutarlı biçimde çalışmalı ve ayrıldığında kodun anlaşılmasını veya test edilmesini kolaylaştırmalıdır. Tek bir yardımcı metot için yeni sınıf oluşturmak bazen gereksiz soyutlama maliyeti doğurur.
Bir davranışın veri sahibi hangi sınıf olduğu nasıl anlaşılır?
Önce davranışın kullandığı temel verinin hangi nesneye ait olduğunu ve işlemin hangi nesnenin durumunu değiştirdiğini inceleyin. Davranış birden fazla nesne arasındaki ilişkiyi temsil ediyorsa, ilişkiyi modelleyen ayrı bir sınıf daha uygun olabilir. Örneğin kitap ile üye arasındaki ödünç alma bilgisi Loan sınıfında tutulabilir.
Constructor’a çok fazla bağımlılık vermek ne zaman sorun olur?
Bir sınıfın constructor’ı çok sayıda servis, koleksiyon ve başka nesne almaya başladığında sınıfın birden fazla işi yönettiği düşünülebilir. Bu durum nesne oluşturmayı, test yazmayı ve bağımlılıkların anlamını zorlaştırır. Ancak her fazla parametre otomatik olarak hatalı değildir; önemli olan bağımlılıkların sınıfın gerçek sorumluluğuyla doğrudan ilgili olup olmadığıdır.
Büyük bir Java sınıfını parçalarken ilk adım ne olmalıdır?
İlk adım sınıfı hemen bölmek değil, mevcut davranışları listelemektir. Metotların hangi verileri kullandığını, hangi alanları değiştirdiğini ve hangi nedenlerle değişebileceğini inceleyin. Daha sonra davranışları anlamlı kümelere ayırıp küçük bir parçayı taşıyarak testlerle doğrulayın.
İyi bir sınıf tasarımı, daha fazla sınıf yazmaktan çok her davranışın yerini ve nedenini açıklayabilmektir.