Online Java kursunda OOP ile küçük proje geliştirmenin doğru başlangıcı, doğrudan sınıf yazmak değil; problemi, kuralları ve sorumlulukları görünür hâle getirmektir. Kütüphane ödünç alma gibi basit görünen bir senaryoda önce problem cümlesi kurulmalı, ardından kitap, üye ve ödünç kaydı gibi nesne adayları belirlenmelidir. Daha sonra her nesnenin hangi bilgiyi taşıyacağı, hangi davranıştan sorumlu olacağı ve diğer nesnelerle nasıl ilişki kuracağı kararlaştırılır.
Bu yaklaşım, Java sözdizimini bilen ancak gerçek projede sınıf sorumluluklarını ayırmakta zorlanan öğrenciler için özellikle önemlidir. Çünkü bütün mantığı tek bir sınıfa yazmak ilk birkaç dakikada hızlı görünse de sorumluluklar birbirine karıştığında yeni kural eklemek, hatayı bulmak ve test yazmak zorlaşır.
OOP Öğrenirken İlk Adım Kod Yazmak Değil, Problemi Parçalamaktır
Nesne yönelimli programlamayı öğrenirken sık yapılan hata, problemi anlamadan class tanımlamaya başlamaktır. Öğrenci hemen Library sınıfı oluşturur, içine kitap listesini ekler, üye kontrolünü yapar, ödünç verme işlemini başlatır ve iade mantığını aynı yerde yazar. Kod çalışsa bile bu yaklaşım, sınıfların neden var olduğunu açıklamaz.
OOP yaklaşımında ilk soru “Hangi sınıfları yazmalıyım?” değildir. Daha doğru soru şudur: “Bu sistemde hangi varlıklar var, hangi bilgileri taşıyorlar ve hangi davranışlardan sorumlular?” Bu sorular cevaplanmadan yazılan sınıflar çoğu zaman yalnızca veri depolayan yapılara veya her işi yapan büyük servis sınıflarına dönüşür.
Kütüphane ödünç alma senaryosunu ele alalım. Sistemin temel amacı şudur: Bir üye, uygun durumdaki bir kitabı ödünç alır. Sistem bu işlemi temsil eden bir ödünç kaydı oluşturur. Kitap iade edildiğinde kayıt kapanır ve kitap yeniden ödünç alınabilir duruma gelir.
Bu problem cümlesi henüz Java kodu değildir. Ancak iyi bir sınıf tasarımı için gerekli bilgiyi içerir:
- Sistemdeki temel varlıklar: üye, kitap ve ödünç kaydı.
- Gerçekleşen davranışlar: ödünç alma, kayıt oluşturma, iade etme ve uygunluk durumunu değiştirme.
- Durum değişimleri: kitap uygun durumdan ödünç verilmiş duruma geçer; iade sonrasında tekrar uygun hâle gelir.
- Kurallar: yalnızca uygun durumdaki bir kitap ödünç verilebilir ve iade edilen kayıt tekrar kapatılamaz.
Buradaki kuralların bazıları doğrudan ilgili nesneye aittir. Örneğin bir kitabın aynı anda iki farklı üyeye verilmesini engellemek, kitabın durumuyla ilgilidir. Buna karşılık bir ödünç kaydını oluşturmak, birden fazla nesneyi bir araya getiren bir işlem olduğu için kütüphane veya ödünç verme servisinin sorumluluğunda olabilir.
Bu ayrımı yapmadan bütün kontrolleri tek bir sınıfa koymak kısa vadede daha az kod yazdırabilir. Fakat zamanla şu sorunlar ortaya çıkar:
- Sorumluluk belirsizliği: Bir davranışın neden belirli bir sınıfta bulunduğu açıklanamaz hâle gelir.
- Değişiklik maliyeti: Ödünç verme kuralı değiştiğinde kitap, üye ve servis mantığının aynı sınıfta bulunması düzenlemeyi zorlaştırır.
- Test karmaşası: Basit bir kitap durumunu test etmek için bütün kütüphane akışını hazırlamak gerekebilir.
- Tekrarlanan koşullar: Kitabın uygunluk kontrolü farklı metotlarda tekrar tekrar yazılabilir.
Bu nedenle küçük bir Java projesinde bile OOP, “çok sayıda sınıf yazmak” anlamına gelmez. Amaç, sistemdeki anlamlı sorumlulukları birbirinden ayırmak ve sınıflar arasındaki ilişkileri anlaşılır kılmaktır. İyi tasarım, en fazla sınıfa sahip tasarım değil; değişmesi muhtemel kuralları doğru yerde tutan tasarımdır.
Problem Cümlesinden Nesne Adaylarına ve Sınıf Sorumluluklarına Geçiş

Problem cümlesinden sınıf tasarımına geçerken kullanılabilecek pratik yöntem, cümledeki isimleri ve fiilleri ayrı ayrı incelemektir. İsimler genellikle nesne adayıdır; fiiller ise davranış veya işlem adayı olarak değerlendirilir. Ancak cümlede geçen her isim otomatik olarak sınıf yapılmamalı, her fiil de doğrudan bir metoda dönüştürülmemelidir.
Örneğimizde “Bir üye uygun durumdaki kitabı ödünç alır, sistem ödünç kaydını oluşturur; kitap iade edildiğinde kayıt kapanır ve kitap yeniden uygun hâle gelir.” cümlesindeki isimler şunlardır:
- Üye
- Kitap
- Ödünç kaydı
- Sistem veya kütüphane
Fiiller ise şunlardır:
- Ödünç almak
- Kayıt oluşturmak
- İade etmek
- Kaydı kapatmak
- Kitabı uygun hâle getirmek
Bir sonraki adım, her davranışı onu en iyi açıklayan sınıfa yerleştirmektir. Örneğin returnBook() işlemi ödünç kaydıyla yakından ilgilidir; çünkü kaydın kapanması bu işlem sırasında gerçekleşir. Kitabın ödünç verilip verilemeyeceğini belirlemek ise kitabın kendi durumuyla ilgilidir. Kütüphane ya da ödünç verme servisi ise kitap ve üye nesnelerini kullanarak işlemin koordinasyonunu üstlenebilir.
| Aday nesne | Temsil ettiği bilgi | Sorumluluk | Başka nesnelerle ilişkisi |
|---|---|---|---|
| Kitap | Kitap numarası, başlık, yazar, uygunluk durumu | Ödünç alınabilir durumda olup olmadığını yönetmek | Ödünç kaydıyla ilişkilidir |
| Üye | Üye numarası, ad ve iletişim bilgileri | Ödünç alan kişiyi temsil etmek | Bir veya daha fazla ödünç kaydında yer alabilir |
| Ödünç kaydı | Kitap, üye, ödünç alma durumu ve iade bilgisi | Ödünç işleminin yaşam döngüsünü takip etmek | Bir kitap ve bir üyeyle ilişkilidir |
| Kütüphane veya ödünç verme servisi | Kitap ve üye koleksiyonları | Ödünç alma ve iade işlemlerini koordine etmek | Kitap, üye ve ödünç kaydı nesnelerini kullanır |
Kitap sınıfının sorumlulukları
- Kitabın kimlik ve tanımlayıcı bilgilerini taşımak.
- Kitabın uygun veya ödünç verilmiş durumunu korumak.
- Ödünç verme işlemi öncesinde durum değişikliğine izin vermek.
- İade sonrasında kitabı tekrar uygun duruma geçirmek.
Üye sınıfının sorumlulukları
- Üyeyi tanımlayan bilgileri taşımak.
- Ödünç işleminin hangi kişiye ait olduğunu temsil etmek.
- Üyeye ait aktif ödünç kayıtlarının takip edilmesine yardımcı olmak.
Ödünç kaydı sınıfının sorumlulukları
- Hangi kitabın hangi üyeye verildiğini saklamak.
- Ödünç alma zamanını veya durumunu temsil etmek.
- İade işlemi gerçekleştiğinde kaydı kapatmak.
- Kapanmış bir kaydın ikinci kez kapatılmasını engellemek.
Kütüphane veya ödünç verme servisinin sorumlulukları
- İşleme gönderilen kitap ve üye nesnelerini bir araya getirmek.
- Kitabın ödünç almaya uygun olup olmadığını kontrol etmek.
- Başarılı işlem sonrasında ödünç kaydı oluşturmak.
- İade sürecinde ilgili kaydı bulup işlemi koordine etmek.
Burada önemli bir sınır vardır: Kütüphane sınıfı kitabın iç durumunu doğrudan değiştirmek yerine kitabın kendi davranışını çağırmalıdır. Örneğin servis içinde book.available = false; gibi doğrudan alan değişiklikleri yapmak, kitabın kendi kurallarını devre dışı bırakabilir. Bunun yerine kitabın durum değişimini yöneten anlamlı bir metot tasarlanabilir. Böylece kitap, kendi geçerli durumunu koruyabilir.
Bu tasarım mantığını uygulamadan önce temel Java kavramlarını ölçmek isteyen bir öğrenci, Java bilgi testi üzerinden sınıf, metot ve nesne bilgilerini kontrol edebilir. Böyle bir ön kontrol, sorunun OOP tasarımından mı yoksa Java sözdizimindeki eksiklerden mi kaynaklandığını ayırt etmeyi kolaylaştırır.
Kod yazmadan önce uygulanabilecek dört adım
- Problem cümlesini sadeleştir: Sistemin ne yapacağını tek veya birkaç açık cümleyle ifade et. Gereksiz ayrıntıları bu aşamada ekleme.
- Nesne adaylarını çıkar: Cümledeki isimleri listele; ancak her ismi otomatik olarak sınıf kabul etmeden anlamını ve sistemdeki rolünü incele.
- Sorumlulukları tekilleştir: Her davranışın öncelikle hangi sınıfa ait olduğunu belirle. Aynı kuralı birden fazla sınıfta tekrar etmemeye çalış.
- İşlemleri test senaryolarına dönüştür: Başarılı ödünç alma, uygun olmayan kitabı ödünç alma ve iade sonrası kitabın tekrar uygun hâle gelmesi gibi durumları yazılı hâle getir.
Bu dört adım tamamlandığında sınıf tasarımı daha somut bir hâle gelir. Henüz kalıtım, interface veya karmaşık tasarım kalıpları eklenmemiş olsa bile hangi nesnelerin gerekli olduğu ve aralarındaki temel iş akışı görünür olur. Sonraki aşamada karar verilmesi gereken konu, bu nesnelerin tek bir sınıfta toplanmasının ne zaman kabul edilebilir, ne zaman ise tasarımı zorlaştıran bir tercihe dönüştüğüdür.
Sınıf Tasarımında Denge: Tek Sınıf mı, Gereksiz Çok Sınıf mı?
İyi sınıf tasarımı, mümkün olan en az veya en fazla sayıda sınıfı oluşturmak değildir. Amaç, birlikte değişen veri ve davranışları aynı sınıfta toplamak, farklı nedenlerle değişen sorumlulukları ise birbirinden ayırmaktır. Kütüphane projesinde bu denge kurulmadığında kod ya tek bir dev sınıfa dönüşür ya da gereksiz soyutlamalar arasında kaybolur.
Birinci hata: Her şeyi tek sınıfa doldurmak
Başlangıçta yalnızca kitap ekleyen ve ödünç veren bir program için tek sınıf yeterli görünebilir. Ancak aynı sınıf kitap listesini tutuyor, üyeyi kontrol ediyor, ödünç kaydı oluşturuyor, iadeyi gerçekleştiriyor ve kullanıcıya gösterilecek metni hazırlıyorsa artık tek bir iş yapmıyordur.
Örneğin LibrarySystem adlı sınıfın aşağıdaki görevlerin tamamını üstlendiğini düşünelim:
- Kitapların başlık ve kimlik bilgilerini saklamak
- Üyelerin aktif olup olmadığını kontrol etmek
- Bir kitabın ödünç verilip verilemeyeceğine karar vermek
- Ödünç alma ve iade tarihlerini kaydetmek
- Ekrana sonuç mesajı yazdırmak
Bu tasarımın sorunu yalnızca sınıfın uzun olması değildir. Asıl sorun, değişiklik nedenlerinin birbirine karışmasıdır. Ödünç verme kuralı değiştiğinde kitap listesini yöneten kodun bulunduğu sınıf düzenlenir. Çıktı biçimi değiştiğinde yine aynı sınıfa dokunulur. Yeni bir üye kontrolü eklendiğinde, ödünç kayıtlarını yöneten metotlar da yanlışlıkla etkilenebilir.
Küçük bir senaryo düşünelim: Başlangıçta aktif olan her üye kitap alabiliyor. Daha sonra aynı kitabın ikinci kez ödünç verilememesi gerektiğini fark ediyoruz. Eğer kitabın ödünç durumu LibrarySystem içindeki farklı listeler ve koşullarla takip ediliyorsa bu kuralı değiştirmek için birden fazla metodu incelemek gerekir. Kitabın kendi durumunu koruduğu tasarımda ise ilgili değişiklik büyük ölçüde Book sınıfında kalır.
Testlerde de benzer bir sorun ortaya çıkar. Yalnızca “ödünç alınmış kitap yeniden verilemez” kuralını sınamak isterken üye listesi, kitap listesi ve çıktı mekanizması da hazırlanmak zorunda kalabilir. Böylece basit bir davranış testi, bütün sistemi kurmayı gerektiren ağır bir teste dönüşür.
İkinci hata: Her işlem için ayrı sınıf açmak
Tek sınıflı tasarımın sorunlarını gören bir öğrenci bu kez ters yöne fazla ilerleyebilir. BookTitleValidator, MemberStatusChecker, BorrowMessageCreator, BookFinder ve ReturnOperationExecutor gibi çok sayıda küçük sınıf oluşturmak, her zaman daha iyi OOP anlamına gelmez.
Bir sınıf yalnızca başka bir nesnenin tek metodunu çağırıyor ve kendine ait anlamlı bir veri ya da karar taşımıyorsa yeni bir soyutlama katmanı ekliyor olabilir. Bu durumda proje büyümese bile dosya sayısı, nesneler arasındaki bağlantılar ve takip edilmesi gereken isimler artar. Öğrenci, gerçek problemi çözmek yerine hangi sınıfın hangi sınıfı çağırdığını anlamaya çalışır.
Gereksiz sınıflandırma özellikle başlangıç seviyesinde iki kavramsal yük oluşturur:
- Basit bir iş akışını takip etmek için çok sayıda dosya arasında geçiş yapılır.
- Her sınıfa bir görev verme çabası, problem alanında karşılığı olmayan yapay nesneler üretir.
Dolayısıyla “her sınıf yalnızca bir metot içermelidir” gibi mekanik bir kural kullanılmamalıdır. Bir sınıf birbiriyle ilişkili birkaç davranışı yönetebilir. Önemli olan bu davranışların aynı sorumluluk etrafında birleşmesidir.
Yeni bir sınıf açmadan önce sorulacak dört soru
Bir nesne adayının gerçekten sınıfa dönüşüp dönüşmemesi gerektiğini değerlendirmek için şu karar ölçütleri kullanılabilir:
- Kendi verisi var mı? Kitabın kimliği, başlığı ve ödünç durumu vardır. Bu bilgiler birlikte anlam taşır ve kitabın yaşamı boyunca korunur.
- Anlamlı bir davranışı var mı? Kitap ödünç alınabilir veya iade edilebilir. Üye aktiflik durumunu koruyabilir. Ödünç kaydı, işlemin kapanıp kapanmadığını belirleyebilir.
- Bağımsız değişme ihtimali var mı? Üyelik kuralları değişebilirken kitabın temel bilgileri aynı kalabilir. Bu iki alanın ayrı değişebilmesi, farklı sınıfları destekler.
- Başka sınıfın iç detaylarını bilmeden görevini yapabilir mi? Kütüphane yöneticisi kitabın özel alanlarını doğrudan değiştirmek yerine
borrow()gibi bir davranışı çağırabiliyorsa sınırlar daha nettir.
Bu soruların tamamına aynı ağırlık verilmek zorunda değildir. Örneğin geçici bir sonuç mesajının kendine ait verisi ve bağımsız davranışı bulunmuyorsa onu sınıfa dönüştürmek gereksiz olabilir. Buna karşılık ödünç kaydı; kitabı, üyeyi ve işlemin durumunu bir arada tuttuğu için ayrı bir nesne olmayı hak edebilir.
Kütüphane projesinde dengeli sorumluluk dağılımı
Dengeli bir başlangıç tasarımında Book, Member ve Loan nesneleri kendi durumlarını korur. Library ya da LibraryManager ise bu nesneler arasındaki işlem akışını koordine eder.
Book: Kitabın kimliğini, başlığını ve ödünç durumunu korur.Member: Üyenin kimliğini ve üyelik durumunu korur.Loan: Hangi kitabın hangi üyeye verildiğini ve işlemin kapanıp kapanmadığını temsil eder.LibraryManager: Kitabı ve üyeyi bulur, gerekli kuralları kontrol eder ve uygun nesnelerin davranışlarını çağırır.
Burada yönetici sınıfın görevi her ayrıntıyı kendisi yapmak değil, akışı yönetmektir. Örneğin bir kitabın ödünç durumunu doğrudan true yapmak yerine kitabın borrow() metodunu çağırır. Böylece kitabın geçerlilik kuralları kitabın içinde kalır.
Sınıf sınırlarını belirlerken kod incelemesi ve bireysel geri bildirim ihtiyacı duyan okuyucular, kişiye özel planlanan Java özel ders seçeneği üzerinden OOP ve mini proje çalışmalarını kendi seviyelerine göre ele alabilir. Buradaki temel hedef, hazır sınıf şemalarını ezberlemek değil, her sınıfın neden var olduğunu açıklayabilmektir.
İlk tasarımın kusursuz olması beklenmemelidir. Önce anlaşılır bir sorumluluk dağılımı kurulur; tekrarlar, karmaşık koşullar ve birlikte değişen kodlar görüldükçe tasarım yeniden düzenlenir. OOP tasarımı tek seferlik bir çizim değil, problem hakkındaki anlayış geliştikçe ilerleyen bir karar sürecidir.
Composition, Kalıtım, Interface ve Constructor Kararları Nasıl Verilir?

Composition, kalıtım ve interface birbirinin yerine rastgele kullanılabilecek araçlar değildir. Karar verirken ilişkinin gerçek anlamına, parçaların bağımsız değişme ihtimaline ve davranışın hangi nesnede bulunması gerektiğine bakılmalıdır. Constructor ise oluşturulan nesnenin daha ilk anda geçerli durumda olmasını sağlamalıdır.
Composition ne zaman tercih edilir?
Composition, bir nesnenin görevini yerine getirmek için başka nesneleri içermesi veya kullanmasıdır. Kütüphane nesnesinin kitapları ve ödünç kayıtlarını koleksiyonlar içinde tutması buna örnektir. Kütüphane bir kitap değildir; kitaplara sahiptir ve onlar arasındaki işlemleri yönetir.
Composition ile kalıtım arasında karar verirken üç ölçüt kullanılabilir:
- İlişkinin anlamı: “A, B’dir” ilişkisi mi, yoksa “A, B’ye sahiptir” ilişkisi mi?
- Değişme ihtimali: Kullanılan nesnenin davranışı, ana nesneden bağımsız olarak değişebilir mi?
- Sorumluluğun sahibi: İlgili veri ve kural doğal olarak hangi nesnede bulunmalıdır?
Library ile Book arasındaki ilişki “kütüphane kitaplara sahiptir” biçimindedir. Bu nedenle kitapların bir koleksiyon içinde tutulması doğal bir composition örneğidir. Kitabın ödünç durumunu korumak ise kütüphanenin değil, kitabın sorumluluğudur.
Kalıtım neden zorlanmamalıdır?
Kalıtım, yalnızca kod tekrarını azaltmak için kullanılmamalıdır. Alt sınıfın üst sınıfın gerçek bir uzmanlaşması olması ve üst türün yerine anlamlı biçimde kullanılabilmesi gerekir.
Örneğin projede yalnızca kütüphane üyelerini yönetiyorsak Member extends Person yazmak otomatik olarak daha iyi bir tasarım oluşturmaz. Person sınıfının sistemde bağımsız bir anlamı, ortak davranışları ve gerçek alt türleri yoksa bu kalıtım yalnızca fazladan bir katman üretir. Üyenin adı ve kimliği doğrudan Member içinde tutulabilir.
Kalıtım kararı vermeden önce şu cümle tamamlanmalıdır: “Alt sınıf, gerçekten bir üst sınıf türüdür ve üst sınıfın beklenen davranışlarını bozmadan kullanılır.” Bu cümle problem alanında ikna edici değilse composition çoğu zaman daha açık bir başlangıç sağlar.
Interface hangi problemi çözer?
Interface, bir nesnenin hangi davranışı sunacağını belirleyen sözleşmedir. Temel amacı ortak veri taşımak değil, farklı sınıflardan beklenen davranışı ifade etmektir.
Kütüphane projesinde kitap dışında dergi veya başka materyaller de ödünç verilebilecekse Loanable adlı bir interface düşünülebilir. Bu sözleşme, nesnenin borrow() davranışını sunmasını ister. Yönetici sınıf böylece somut nesnenin kitap mı yoksa başka bir materyal mi olduğuna değil, ödünç verilebilir olup olmadığına odaklanabilir.
Ancak projede yalnızca tek bir Book türü varsa interface oluşturmak zorunlu değildir. Yakın gelecekte gerçekleşeceği belli olmayan her olasılık için soyutlama eklemek, bugünkü tasarımı gereksiz yere karmaşıklaştırabilir. Interface, gerçek bir davranış çeşitliliği veya bağımlılık sınırı oluştuğunda daha anlamlıdır.
Constructor nesneyi geçerli durumda başlatmalıdır
Constructor tasarımındaki temel soru şudur: “Bu nesne hangi bilgiler olmadan anlamlı biçimde var olamaz?” Bir kitabın başlığı zorunluysa constructor parametresi yapılmalıdır. Ödünç durumu ise yeni kitap için başlangıçta otomatik olarak uygun değere ayarlanabilir.
Zorunlu alanların constructor ile alınması, eksik nesnelerin sistem içinde dolaşmasını önler. Alanların dışarıdan doğrudan değiştirilmesini sınırlandırmak da nesnenin kendi kurallarını korumasına yardımcı olur. Örneğin borrowed alanını herkese açık yapmak yerine durum yalnızca borrow() ve returnBook() gibi davranışlarla değiştirilmelidir.
Kod örnekleriyle kendi hızında tekrar yapmak isteyen öğrenciler, online video yazılım eğitimlerini inceleyebilir. Bir örneği yalnızca izlemek yerine önce sonucu tahmin etmek, ardından kodu çalıştırıp sınıf sorumluluklarını değiştirmek OOP pratiğini daha verimli hâle getirir.
Çalışan Java örneği
Aşağıdaki kısa programda Library, kitap koleksiyonunu içerir; Book ise kendi ödünç durumunu yönetir. Loanable interface’i beklenen davranışı tanımlar ve constructor kitabın zorunlu başlığını alır.
public class Main {
interface Loanable { boolean borrow(); }
static class Book implements Loanable {
private final String title;
private boolean borrowed;
Book(String title) { this.title = title; }
public boolean borrow() {
if (borrowed) return false;
borrowed = true;
return true;
}
}
static class Library {
private final java.util.List<Book> books = new java.util.ArrayList<>();
void add(Book book) { books.add(book); }
boolean borrow(Book book) { return books.contains(book) && book.borrow(); }
}
public static void main(String[] args) {
Library library = new Library(); Book book = new Book("Temiz Kod");
library.add(book); System.out.println(library.borrow(book));
System.out.println(library.borrow(book));
}
}
Loanable interface’i, ödünç verilebilir bir nesnenin borrow() davranışına sahip olacağını söyler. Burada veri tutmaz; yalnızca davranış sözleşmesini belirtir.
Book sınıfı bu sözleşmeyi uygular. title alanı constructor ile zorunlu olarak alınır ve sonradan başka bir değere yönlendirilmez. borrowed alanı dışarıya kapalıdır; ödünç durumu yalnızca kitabın kendi metodu üzerinden değişir.
borrow() önce kitabın mevcut durumunu kontrol eder. Kitap daha önce alınmışsa false döndürür. Uygunsa durumu değiştirir ve işlemin başarılı olduğunu bildiren true sonucunu üretir.
Library, kitapları bir List içinde tutar. Bir kitabı ödünç verme isteği geldiğinde önce kitabın kendi koleksiyonunda bulunup bulunmadığını kontrol eder, ardından asıl durum değişikliğini Book nesnesine bırakır. Böylece koordinasyon ile durum yönetimi birbirinden ayrılır.
Programın beklenen çıktısı şöyledir:
true
false
İlk çağrıda kitap kütüphanede bulunduğu ve henüz ödünç alınmadığı için işlem başarılıdır. İkinci çağrıda aynı nesnenin ödünç durumu zaten değişmiştir; bu nedenle kitap yeniden verilemez ve sonuç false olur.
List, Map ve Set arasında nasıl seçim yapılır?
Koleksiyon seçimi, alışkanlığa göre değil yapılacak işleme göre belirlenmelidir. Her koleksiyon farklı bir erişim ve bütünlük ihtiyacını ifade eder.
List, kitapları eklenme sırasıyla tutmak veya sırayla dolaşmak istendiğinde uygundur. Aynı değerin birden fazla kez bulunmasına izin veren bir yapı gerektiğinde de kullanılabilir.Map, kitabı benzersiz kimliği üzerinden bulmak istendiğinde daha doğal bir model sunar. Örneğin kitap kimliği anahtar,Booknesnesi değer olabilir.Set, aynı üyeliğin veya aynı benzersiz kaydın tekrar eklenmesini engellemek istediğimiz durumlarda düşünülebilir.
Örneğin kullanıcı sürekli kitap kimliği girerek arama yapacaksa Map<String, Book> niyeti açık biçimde gösterir. Ama amaç yalnızca eklenen kitapları sırayla ekrana yazdırmaksa List<Book> daha sade olabilir. Benzersizlik ihtiyacı varsa Set değerlendirilebilir; ancak bir nesnenin neye göre aynı kabul edildiği de açıkça tasarlanmalıdır.
Sonuç olarak araç seçimi şu sırayla yapılmalıdır: Önce problemdeki ilişki belirlenir, sonra sorumluluğun sahibi seçilir, en son dil yapısı kararlaştırılır. Önce kalıtım, interface veya belirli bir koleksiyon seçip problemi buna uydurmak; anlaşılması güç ve gereğinden fazla bağlı sınıflar üretir.
Ödünç Verme ve İade Akışını Test Edilebilir Bir Tasarıma Dönüştürmek
Bir kütüphane projesinde kodun çalışması tek başına yeterli değildir. Asıl önemli olan, ödünç verme ve iade gibi kuralların kod içinde görünür olması ve bu kuralların testlerle doğrulanabilmesidir. Geçerli bir üyenin uygun kitabı ödünç alabilmesi kadar, aynı kitabın ikinci kez verilememesi, bulunmayan bir üye kimliğinin anlamlı bir hatayla karşılanması ve iade edilen kitabın yeniden kullanılabilir duruma gelmesi de tasarımın parçasıdır.
Bu nedenle test yazmaya “metot hata veriyor mu?” sorusuyla değil, “bu işlemden sonra sistemin gözlemlenebilir durumu ne olmalı?” sorusuyla başlamak daha sağlıklıdır. Kütüphane senaryosunda gözlemlenebilir sonuç; kitabın uygunluk durumu, ödünç kaydının oluşması, iade işleminin tamamlanması veya belirli bir hata türünün fırlatılması olabilir.
Önce temel başarılı akışı kurun
İlk senaryo mümkün olan en sade durumdan başlamalıdır:
- Sistemde bir kitap kayıtlıdır.
- Sistemde geçerli bir üye kayıtlıdır.
- Kitap henüz ödünç verilmemiştir.
- Üye, kitabı ödünç almak ister.
- İşlem başarılı olur ve kitap artık uygun değildir.
Burada dikkat edilmesi gereken nokta, ödünç verme kararının yalnızca ana metotta verilmemesidir. Örneğin LibraryService işlemi koordine edebilir; ancak kitabın uygun olup olmadığını kontrol etme sorumluluğu Book sınıfında bulunabilir. Böylece kitap kendi durumunu korur ve dışarıdaki kodun doğrudan available = false yazmasına gerek kalmaz.
public class LibraryDemo {
public static void main(String[] args) {
Book book = new Book("B1", "Clean Code");
Member member = new Member("M1", "Ayşe");
LibraryService library = new LibraryService();
library.addBook(book);
library.addMember(member);
library.borrow("B1", "M1");
System.out.println(book.isAvailable());
}
}
Bu örnekte beklenen çıktı false değeridir. Çünkü ödünç verme işleminden sonra kitabın uygunluk durumu değişmiştir. Gerçek projede Book, Member ve LibraryService sınıfları ayrı dosyalarda bulunur; örnek, akışın hangi sonucu üretmesi gerektiğini göstermek için kısa tutulmuştur.
Başarısız işlemler de tasarımın parçasıdır
Bir sistemin kalitesi çoğu zaman doğru işlemlerden çok, yanlış işlemleri nasıl yönettiğiyle anlaşılır. Kütüphane otomasyonunda en azından aşağıdaki durumlar açıkça tanımlanmalıdır:
- Uygun kitap ve geçerli üye ile ödünç verme başarılı olmalıdır.
- Daha önce ödünç verilmiş bir kitap tekrar ödünç verilmeye çalışıldığında işlem durmalıdır.
- Geçersiz üye kimliği gönderildiğinde sistem, hangi bilginin bulunamadığını açıklayan bir hata üretmelidir.
- Geçersiz kitap kimliği gönderildiğinde işlem sessizce yok sayılmamalıdır.
- Ödünç alınmış bir kitap iade edildiğinde tekrar uygun duruma gelmelidir.
- İade sonrasında aynı kitap başka bir üyeye yeniden verilebilmelidir.
Buradaki “anlamlı hata” ifadesi, yalnızca ekrana yazdırılmış bir mesaj anlamına gelmez. Örneğin IllegalArgumentException ile geçersiz kimlik bilgisini, IllegalStateException ile uygun olmayan durumda bulunan kitabı ayırmak daha okunabilir bir tasarım sağlar. Hatanın türü, test tarafından gözlemlenebilir olmalıdır.
Şu iki yaklaşım arasındaki fark önemlidir:
- Zayıf yaklaşım: Metot içinde hata mesajını
System.out.println()ile yazdırmak ve işlemi sessizce bitirmek. - Test edilebilir yaklaşım: Metodun belirli bir hata türü fırlatması veya açık bir sonuç nesnesi döndürmesi.
İkinci yaklaşımda test, konsol çıktısını yakalamaya çalışmak zorunda kalmaz. Bunun yerine kitabın durumunu, dönen sonucu veya fırlatılan istisnayı doğrudan kontrol eder.
İade işlemi bir “durumu tersine çevirme” değildir
İade metodunu yalnızca book.setAvailable(true) satırından ibaret düşünmek eksik bir model oluşturur. Önce gerçekten açık bir ödünç kaydı olup olmadığı kontrol edilmelidir. Kütüphane, iade edilen kitabın hangi üyede olduğunu ve aktif ödünç kaydının bulunup bulunmadığını bilmelidir.
Basit bir modelde Loan sınıfı; kitabı, üyeyi ve aktiflik durumunu temsil edebilir. Ödünç verme sırasında yeni bir Loan oluşturulur. İade sırasında bu kayıt kapatılır ve kitap tekrar uygun duruma gelir. Böyle bir tasarım, ileride teslim tarihi, gecikme bilgisi veya geçmiş ödünç hareketleri gibi ihtiyaçların eklenmesini kolaylaştırır.
Önemli olan, tüm bilgileri tek sınıfta toplamak değil; davranışı o davranışla ilgili veriyi en iyi bilen sınıfa yerleştirmektir. Book uygunluk durumunu, Loan ödünç ilişkisinin aktifliğini, LibraryService ise kitap ve üye kayıtları arasındaki işlem akışını yönetebilir.
Küçük bir JUnit test senaryosu
Aşağıdaki örnekte iki temel kural test edilir: başarılı ödünç verme sonrasında kitabın uygun olmaması ve uygun olmayan kitabın tekrar ödünç verilmesi durumunda anlamlı bir hata oluşması.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class LibraryServiceTest {
@Test
void bookBecomesUnavailableAfterBorrowing() {
LibraryService library = new LibraryService();
Book book = new Book("B1", "Clean Code");
Member member = new Member("M1", "Ayşe");
library.addBook(book);
library.addMember(member);
library.borrow("B1", "M1");
assertFalse(book.isAvailable());
}
@Test
void borrowingUnavailableBookThrowsMeaningfulError() {
LibraryService library = new LibraryService();
Book book = new Book("B1", "Clean Code");
Member member = new Member("M1", "Ayşe");
library.addBook(book);
library.addMember(member);
library.borrow("B1", "M1");
IllegalStateException error = assertThrows(
IllegalStateException.class,
() -> library.borrow("B1", "M1")
);
assertEquals("Kitap şu anda ödünçte.", error.getMessage());
}
}
İlk testte işlemden sonra kitabın uygunluk durumu kontrol edilir. İkinci testte ise yalnızca “bir hata oluştu” denmez; hata türü ve hata mesajı da doğrulanır. JUnit Jupiter içindeki assertThrows() kullanımı, belirli bir kod bloğunun beklenen istisnayı üretip üretmediğini kontrol etmek ve oluşan istisne nesnesini ayrıca incelemek için kullanılabilir. ([junit.org](https://junit.org/junit5/docs/current/user-guide/?utm_source=openai))
Bu testlerin çalışması için LibraryService sınıfının istisnayı gerçekten fırlatması gerekir. Örneğin ikinci ödünç verme denemesinde yalnızca ekrana mesaj yazdırılırsa test başarısız olur. Bu başarısızlık olumsuz bir sonuç değil, tasarımın test edilebilir bir davranış sunmadığını gösteren yararlı bir uyarıdır.
Testleri konsol çıktısına değil, duruma yazın
İş mantığı main metoduna yığıldığında test yazmak zorlaşır. main metodu kullanıcıdan veri alabilir, nesneleri oluşturabilir ve servisi çağırabilir; ancak “kitap uygun mu?”, “üye kayıtlı mı?” veya “iade sonrası ne değişti?” gibi kuralların sahibi olmamalıdır.
Daha temiz bir akış şu şekilde düşünülebilir:
mainveya kullanıcı arayüzü yalnızca girdileri alır.LibraryServiceödünç verme ve iade işlemini koordine eder.Bookkendi uygunluk durumunu korur.Memberüyeye ait temel bilgileri ve gerekiyorsa aktif ödünçleri temsil eder.Loankitap ile üye arasındaki ödünç ilişkisini taşır.- Testler bu sınıfların dışarıdan gözlemlenebilen davranışlarını doğrular.
Bu ayrım sayesinde veritabanı, kullanıcı arayüzü veya dosya sistemi olmadan da temel iş kuralları test edilebilir. Başlangıç seviyesindeki bir projede her şeyi hemen kalıcı veritabanına bağlamak yerine, önce bellekte çalışan küçük bir model kurmak daha anlaşılırdır. Daha sonra ihtiyaç ortaya çıktığında veri saklama katmanı ayrı bir sorumluluk olarak eklenebilir.
| Senaryo | Başlangıç durumu | İşlem | Beklenen sonuç |
|---|---|---|---|
| Uygun kitap ödünç verilir | Kitap uygun, üye kayıtlı | Ödünç verme çağrılır | Kitap uygun değildir, aktif kayıt oluşur |
| Aynı kitap tekrar istenir | Kitap başka bir ödünç kaydında | Tekrar ödünç verme denenir | Anlamlı bir durum hatası oluşur |
| Geçersiz üye | Kitap kayıtlı, üye bulunamıyor | Geçersiz üye kimliği gönderilir | Üyenin bulunamadığını belirten hata oluşur |
| Geçersiz kitap | Üye kayıtlı, kitap bulunamıyor | Geçersiz kitap kimliği gönderilir | Kitabın bulunamadığını belirten hata oluşur |
| Kitap iade edilir | Aktif ödünç kaydı var | İade işlemi yapılır | Kitap tekrar uygun olur, kayıt kapanır |
| İade sonrası yeniden ödünç verme | Kitap iade edilmiş, üye kayıtlı | Aynı kitap yeniden istenir | Yeni ödünç işlemi başarılı olur |
Küçük Projeyi Geliştirirken Kullanılacak OOP Karar Kontrol Listesi
Benzer bir Java projesini kendi başınıza geliştirirken aşağıdaki sıra, kod yazmaya başlamadan önce kararlarınızı görünür hâle getirir. Amaç en başta kusursuz bir mimari kurmak değil, çalışan en küçük modeli kurup gerçek ihtiyaçlara göre iyileştirmektir.
- Problem cümlesini yazın.
“Kütüphane kitapları kaydeder, kayıtlı üyelerin uygun kitapları ödünç almasını ve iade etmesini sağlar.” gibi tek cümlelik bir problem tanımı oluşturun. Cümlede henüz sınıf, interface veya framework adı bulunmasına gerek yoktur. Önce sistemin hangi davranışı gerçekleştireceğini netleştirin.
- Nesne adaylarını çıkarın.
Problem cümlesindeki isimleri işaretleyin: kitap, üye, ödünç kaydı, kütüphane. Bu kelimelerin hepsi otomatik olarak sınıf yapılmaz. Bazıları yalnızca özellik, bazıları ise bir işlem veya değer olabilir. Adayları çıkarmak, sınıf tasarımının başlangıç listesini oluşturur.
- Her adaya tekil bir sorumluluk verin.
Bir sınıfın hangi işi üstleneceğini tek cümleyle açıklayamıyorsanız sınıf henüz fazla geniş olabilir.
Bookkitabın kimliğini ve uygunluk durumunu;Memberüyeyi;Loanödünç ilişkisini;LibraryServiceise işlemlerin koordinasyonunu üstlenebilir. - İlişkileri composition, kalıtım ve interface ölçütleriyle değerlendirin.
Bir nesne diğerini gerçekten sahipleniyor mu, yoksa yalnızca onunla mı çalışıyor? “Bu bir tür müdür?” sorusuna net biçimde evet diyemiyorsanız kalıtım kullanmak için acele etmeyin. Kütüphane ile kitap arasındaki ilişki çoğu küçük modelde “kütüphane kitapları yönetir” biçiminde composition veya koleksiyon ilişkisine daha yakındır. Kalıtım ise ortak bir üst tür ve anlamlı bir davranış farklılığı olduğunda düşünülmelidir.
- Constructor ve koleksiyonları seçin.
Nesnenin geçerli olabilmesi için zorunlu alanları constructor ile alın. Örneğin kitabın kimliği ve başlığı olmadan
Bookoluşturulmasına izin vermemek, hatalı nesnelerin sisteme girmesini azaltır. Birden fazla kitap veya üye tutulacaksa uygun bir koleksiyon seçin; kimlik üzerinden hızlı erişim gerekiyorsaMap, yalnızca sıralı bir liste gerekiyorsaListtercih edilebilir. - Ana akışı küçük bir servisle koordine edin.
Ödünç verme sırasında kitap aranır, üye doğrulanır, kitap uygunluk kontrolünden geçer ve ödünç kaydı oluşturulur. Bu adımların koordinasyonu için küçük bir servis yeterlidir. Servisin her işi üstlenmesine izin vermeyin; veri ve davranışın sahibi olan nesnelerdeki kuralları servise kopyalamayın.
- Başarılı ve hatalı senaryoları test edin.
En az bir normal akış, bir sınır durumu ve bir hatalı işlem seçin. Ödünç verme, tekrar ödünç verme, geçersiz kimlik, iade ve iade sonrası yeniden ödünç verme bu proje için iyi bir başlangıç kümesidir. Testleri yazarken her senaryoda başlangıç durumu, işlem ve beklenen sonucu ayrı ayrı ifade edin.
Tasarım denetim listesi
Kodunuzu çalıştırmadan önce aşağıdaki soruları sınıf ve metot bazında sorun:
- Bu sınıfın tek bir değişme nedeni var mı?
- Bu davranış, veriyi en iyi bilen nesnede mi?
- İlişki gerçekten “bir tür” mü, yoksa “sahip olma” mı?
- Bu interface, gerçek bir davranış sözleşmesi mi sağlıyor, yoksa yalnızca soyut görünmek için mi eklendi?
- Constructor, nesnenin geçersiz bir durumda oluşturulmasını engelliyor mu?
- Koleksiyon seçimi, veriye nasıl erişeceğim sorusuyla uyumlu mu?
- Bu kuralı testte gözlemleyebilir miyim?
- Hata durumunda kullanıcıya veya çağıran koda yeterli bilgi aktarılıyor mu?
Özellikle son iki soru, OOP tasarımının pratik değerini ölçer. Bir kuralı test edemiyorsanız bunun nedeni test yazma beceriniz kadar, davranışın sınıflara kötü dağıtılmış olması da olabilir. Örneğin ödünç verme sonucu yalnızca ekrana yazdırılıyorsa, test kitabın gerçekten uygun olmadığını doğrudan inceleyemez.
Gereksiz abstraction eklemeden ilerleyin
Küçük bir projede henüz ihtiyaç yokken çok sayıda interface, factory, repository veya katman eklemek kodu daha profesyonel değil, daha zor okunur hâle getirebilir. Önce çalışan en küçük modeli kurun: kitap, üye, ödünç kaydı ve işlemleri koordine eden küçük bir servis.
Daha sonra gerçek bir değişim ihtiyacı ortaya çıkarsa refactor yapın. Örneğin farklı üyelik türlerinin farklı ödünç limitleri varsa bir strateji veya interface anlamlı hâle gelebilir. Fakat tek bir üyelik kuralı varken yalnızca ileride kullanılabilir düşüncesiyle abstraction eklemek, problemi çözmeden yapıyı büyütür.
Bu karar akışını uygularken mevcut eksiklerinizi görmek için ücretsiz kodlama seviyenizi ölçebileceğiniz bilgi testlerini kullanabilirsiniz. Testler; Python, Java ve algoritmik düşünme gibi alanlarda hangi konuları tekrar etmeniz gerektiğine dair başlangıç noktası sunar. ([berkakademi.com](ücretsiz seviye testleri?utm_source=openai))
Özellikle OOP, sınıf tasarımı ve Java sözdizimi konularını ayrı değerlendirmek isterseniz Java ve OOP odaklı bilgi testi üzerinden kendi seviyenizi kontrol edebilirsiniz. Böylece yalnızca kodu kopyalamak yerine hangi kararları neden verdiğinizi ölçmeye başlayabilirsiniz. ([berkakademi.com](ücretsiz seviye testleri?utm_source=openai))
Sık Sorulan Sorular
Kütüphane projesinde kitap, üye ve ödünç kaydı neden ayrı sınıflar olmalı?
Çünkü bu üç kavramın sorumlulukları farklıdır. Kitap kendi kimliğini ve uygunluk durumunu, üye kullanıcıya ait bilgileri, ödünç kaydı ise kitap ile üye arasındaki işlemsel ilişkiyi temsil eder. Hepsini tek sınıfta toplamak, sınıfın büyümesine ve bir değişikliğin ilgisiz davranışları etkilemesine neden olabilir.
Java projesinde composition ile kalıtım arasında karar verirken hangi sorular sorulmalı?
Önce ilişkinin gerçekten “bir tür” ilişkisi olup olmadığını sorun. Bir sınıf diğer sınıfın özel bir türü değilse kalıtım yerine composition veya başka bir nesneyle iş birliği daha uygun olabilir. Ayrıca değişen parçayı bağımsız değiştirmek, test etmek ve yeniden kullanmak isteyip istemediğinizi değerlendirin.
Bir interface hangi durumda somut bir sınıftan daha uygun olur?
Aynı davranışın birden fazla farklı sınıf tarafından uygulanması veya çağıran kodun somut sınıfa bağımlı olmaması gerektiğinde interface anlamlıdır. Ancak yalnızca sınıfa “daha profesyonel” bir görünüm vermek için interface eklenmemelidir. Önce gerçek bir değişim noktası veya davranış sözleşmesi bulunmalıdır.
OOP projesinde JUnit testlerine hangi başarılı ve hatalı senaryolarla başlanmalı?
Önce temel başarılı akışı test edin: uygun kitabın geçerli üyeye ödünç verilmesi ve kitabın uygunluk durumunun değişmesi. Ardından aynı kitabın tekrar istenmesi, geçersiz kitap veya üye kimliği, iade ve iade sonrası yeniden ödünç verme senaryolarını ekleyin. Her testte başlangıç durumu, işlem ve beklenen gözlemlenebilir sonuç açık olmalıdır.
İyi bir OOP projesi, yalnızca çalışan sınıflardan değil; neden o sınıflara ihtiyaç duyulduğunu açıklayan ve kuralları testlerle görünür kılan bir tasarımdan oluşur.