Temiz Kod ve Bakım Mühendisliği
Kod kalitesini isimlendirme ve biçim kurallarının ötesinde; bakım maliyeti, modüler ayrıştırma, hata sözleşmeleri, test edilebilirlik, statik analiz, eşzamanlılık ve değişim riski üzerinden ele alan mühendislik notları.
Temiz kod tartışması çoğu zaman isimlendirme, girinti ve kısa fonksiyonlarla başlar. Bunlar önemlidir; fakat asıl mühendislik problemi daha derindedir: kodu yazmayan bir mühendis davranışı ne kadar hızlı ve doğru anlayabilir, bir değişikliğin etkisi ne kadar dar tutulabilir ve davranışın bozulmadığı nasıl gösterilebilir?
Bu ders notu temiz kodu estetik bir tercih olarak değil, bakım maliyeti ve değişiklik riski üzerinden ele alır. İlk sürümünün hazırlandığı 2015 dönemindeki kaynaklar ile 2026'da kullanılan araç ve Java pratikleri aynı tarihsel bağlamdaymış gibi sunulmaz. Örnek kodlarda modern Java sözdizimi kullanılır; ancak amaç Java öğretmek değil, tasarım kararını görünür kılmaktır.
Süreç modelleri, gereksinim yönetimi ve kalite planlaması için Yazılım Mühendisliği, doğrulama ve geçerleme yöntemleri için Yazılım Test Mühendisliği, güvenlik açığı sınıfları için Güvenli Yazılım Mühendisliği ayrı notlardır.
Ünite 1: Kod Kalitesinin Ölçülebilir Tanımı
Kalite bir sıfat değil, ölçüt kümesidir
"İyi kod" tek boyutlu bir yargı değildir. Bu notun ilk hazırlandığı dönemde yürürlükte olan ISO/IEC 25010:2011 ürün kalitesi modelini sekiz özellik altında tanımlıyordu. 2023'te yayımlanan ikinci baskı modelin kapsamını yeniden düzenledi ve ürün kalitesini dokuz ana özellik altında ele aldı. Bu nedenle 2015'te kullanılan terimler ile 2026'daki güncel standart aynı sürümün diliymiş gibi okunmamalıdır.
Temiz kod açısından merkezde bakım yapılabilirlik vardır. Bir modülün bugün doğru sonuç vermesi yeterli değildir; davranışın güvenli biçimde değiştirilebilmesi, çözümlenebilmesi ve sınanabilmesi de kalite özelliğidir.
"Kod kirli" ifadesi ölçülebilir değildir. "Bu iş kuralını değiştirmek için altı sınıf ve üç yapılandırma dosyası birlikte değişiyor" ifadesi ise incelenebilir bir mühendislik problemidir.
Bakım ekonomisi
Kod bir kez yazılır, çok kez okunur. İlk geliştirme sırasında birkaç dakika kazandıran bir kısaltma, bakım boyunca tekrar tekrar zihinsel maliyet üretebilir. Bunun tersi de geçerlidir: sırf okunabilirlik adına sıcak yola gereksiz tahsis, kopyalama veya dolaylı çağrı eklemek de maliyettir.
Bu nedenle hedef "en güzel kod" değil, gerekçelendirilebilir toplam maliyettir.
Metrikler karar vermez, dikkat çeker
Döngüsel karmaşıklık, bağımlılık sayısı, dosya boyutu ve değişiklik sıklığı yararlı sinyallerdir; ancak insanın bir tasarımı ne kadar hızlı kavradığını doğrudan ölçmezler.
Aynı karmaşıklık değerine sahip iki fonksiyondan biri alan probleminin doğal ifadesi, diğeri gereksiz bir durum makinesi olabilir. Metrik eşik aşımını gösterir; nedenini mühendis açıklar.
Ünite 2: Okuma Maliyeti ve İsimlendirme
Ad, kapsamın fonksiyonudur
Bir tanımlayıcının uzunluğu görünür olduğu kapsamla birlikte değerlendirilmelidir. Üç satırlık bir döngüde i anlaşılır olabilir; sınıf alanında d gibi bir isim aynı ölçüde ucuz değildir.
Sorunlu bir örnek:
public List<User> get(final List<User> u) {
final List<User> r = new ArrayList<>();
for (final User x : u) {
if (x.status() == 1) {
r.add(x);
}
}
return r;
}Aynı davranış daha açık bir alan diliyle ifade edilebilir:
public List<User> findActiveUsers(final List<User> users) {
final List<User> activeUsers = new ArrayList<>();
for (final User user : users) {
if (user.isActive()) {
activeUsers.add(user);
}
}
return activeUsers;
}İyileşme ArrayList veya döngü biçiminden gelmez. get, u, r, x ve status() == 1 ifadelerinin okuyucuya yüklediği geriye dönük arama maliyeti ortadan kalkmıştır.
Aranabilirlik
Kod tabanında anlamlı bir kavramın tek ve kararlı bir adı olması arama maliyetini düşürür. Ham sayılar bu özelliği taşımaz:
if (elapsedSeconds > 86400) {
archive();
}Burada 86400 değeri tek başına anlam taşımaz:
private static final long SECONDS_PER_DAY = 86_400;
if (elapsedSeconds > SECONDS_PER_DAY) {
archive();
}Buna karşılık her literal değer "magic number" değildir. index + 1 içindeki 1 doğal olarak "bir sonraki indeks" anlamına geliyorsa NEXT_INDEX_OFFSET gibi bir sabit okunabilirliği artırmaz.
Tek kavram, tek ad
Aynı işlem bir yerde load, başka yerde fetch, başka yerde read olarak adlandırılıyorsa okuyucu bu kelimelerin farklı sözleşmeler taşıdığını varsayar. Gerçekten farklı davranış varsa farklı ad doğrudur; aynı davranışsa ortak sözlük daha ucuzdur.
İsimlendirme kuralının amacı sözcük sayısını artırmak değil, okuyucunun zihninde yanlış dallanmayı azaltmaktır.
Ünite 3: Fonksiyon Sınırı ve Soyutlama Düzeyi
Tek sorumluluk satır sayısı değildir
Bir fonksiyonun kaç satır olduğu tek başına sorumluluğunu söylemez. Asıl soru, fonksiyonun tek ve tutarlı bir karar düzeyinde çalışıp çalışmadığıdır.
Karışık bir örnek:
public void process(final Order order) {
if (order == null) {
throw new IllegalArgumentException("order");
}
if (order.total().signum() <= 0) {
throw new IllegalStateException("Order total must be positive");
}
repository.save(order);
final String email = order.customer().email();
if (email != null) {
mailService.send(email, "Order saved");
}
}Burada doğrulama, kalıcılık ve bildirim aynı gövdededir. Sınırlar davranışa göre ayrıldığında üst düzey akış doğrudan okunur:
public void process(final Order order) {
validate(order);
repository.save(order);
notifyCustomer(order);
}
private void validate(final Order order) {
if (order == null) {
throw new IllegalArgumentException("order");
}
if (order.total().signum() <= 0) {
throw new IllegalStateException("Order total must be positive");
}
}
private void notifyCustomer(final Order order) {
final String email = order.customer().email();
if (email != null) {
mailService.send(email, "Order saved");
}
}Bu örnek "her fonksiyonu küçült" kuralı değildir. Yalnız bir yerden çağrılan ve adı gövdesinden daha az bilgi taşıyan onlarca mikro fonksiyon, okuyucuyu sürekli dosya içinde gezdirebilir.
Koruma koşulları ve iç içe geçme
Derin if yapısı okuyucunun aynı anda çok fazla koşulu aktif tutmasına neden olur:
public void publish(final Media media) {
if (media != null) {
if (media.isReady()) {
if (!media.isDeleted()) {
publisher.publish(media);
}
}
}
}Koruma koşulları aynı davranışı daha düşük bilişsel yükle ifade eder:
public void publish(final Media media) {
if (media == null || !media.isReady() || media.isDeleted()) {
return;
}
publisher.publish(media);
}Burada amaç satır sayısını azaltmak değil, geçerli koşulların zihinsel yığınını küçültmektir.
Bayrak argümanları
public void save(final User user, final boolean notify) {
repository.save(user);
if (notify) {
notificationService.send(user);
}
}Çağrı yerinde save(user, true) davranışı açıklamaz. İki farklı iş akışı varsa bunları adlandırmak daha açıktır:
public void save(final User user) {
repository.save(user);
}
public void saveAndNotify(final User user) {
repository.save(user);
notificationService.send(user);
}Ancak setEnabled(boolean enabled) gibi bir çağrıda boolean doğrudan bir durum değeridir ve problem oluşturmaz. Sorun boolean türünün kendisi değil, iki ayrı davranışı gizleyen selector olarak kullanılmasıdır.
Komut-sorgu ayrımı
Bir sorgu gibi görünen metodun durum değiştirmesi pahalı bir yan etkidir:
if (userRepository.existsAndMarkSeen(userId)) {
continueProcessing();
}Okuma ve yazma ayrı sözleşmeler olduğunda durum geçişi görünür olur:
if (userRepository.exists(userId)) {
userRepository.markSeen(userId);
continueProcessing();
}Her durumda iki veritabanı çağrısı yapmak doğru olmayabilir. Atomiklik veya gecikme gereksinimi tek çağrıyı zorunlu kılıyorsa metodun adı bu birleşik davranışı açıkça söylemelidir.
Ünite 4: Yorum ile Kod Arasındaki Sözleşme
Yorum derlenmez
Yorum kodla birlikte otomatik olarak doğrulanmaz. Bu nedenle kod değişip yorum eski kaldığında, yanlış açıklama hiç açıklama olmamasından daha tehlikeli hale gelir.
Zayıf yorum:
// Kullanıcı aktifse devam et
if (user.status() == 1) {
process(user);
}Kod zaten aynı şeyi söylüyor. Anlamı koda taşımak daha güvenlidir:
if (user.isActive()) {
process(user);
}Gerekçe yorumda yaşayabilir
Kod "ne" yapıldığını gösterebilir; ölçülmüş bir istisnanın "neden" korunduğunu her zaman gösteremez:
// 64 KiB blok bu cihaz ailesinde ölçülen en düşük p99 yazma gecikmesini verdi.
// Değeri değiştirmeden önce aynı depolama profiliyle yeniden benchmark alınmalıdır.
private static final int WRITE_BUFFER_SIZE = 64 * 1024;Bu yorum kodu tekrar etmez; kararın kaynağını ve değiştirme koşulunu taşır.
Yorum satırına alınmış kod ve ölü kod
Sürüm denetimi kullanılan bir projede uzun süre yorum satırında bekleyen kod bir belirsizlik üretir. Geri getirilmesi gerekiyorsa geçmişten alınabilir. Aynı gerekçe hiç çağrılmayan metodlar için de geçerlidir.
TODO da somut bir karar taşıyorsa değerlidir:
// TODO: Sağlayıcının 2.4 sürümündeki zaman aşımı hatası kaldırıldığında bu fallback silinecek."TODO: düzelt" gibi bir not ise karar veya çıkış koşulu taşımaz.
Ünite 5: Modüler Ayrıştırma ve Bilgi Gizleme
Değişmesi olası kararı gizlemek
Parnas'ın temel ölçütü modülleri işlem sırasına göre değil, değişmesi olası tasarım kararına göre ayırmaktır. Bir karar değiştiğinde tek bir yerde değişiklik yapmak ideal yerelleştirmedir.
Örneğin bir rapor sınıfı tarih hesaplamasını, SQL biçimini ve çıktı üretimini birlikte sahipleniyorsa üç farklı değişim nedeni taşır. Bunun yerine iş katmanı bir soyutlamaya bağlanabilir:
public interface ReportRepository {
List<ReportRow> load(final ReportRange range);
}
public final class ReportService {
private final ReportRepository repository;
public ReportService(final ReportRepository repository) {
this.repository = repository;
}
public List<ReportRow> generate(final ReportRange range) {
return repository.load(range);
}
}Buradaki interface tek başına kalite üretmez. Tek bir gerçekleme bulunan ve değişim ekseni görünmeyen her sınıf için interface üretmek yalnız yapısal gürültü olabilir.
Bağlaşım ve uyum
Bir sınıf birçok farklı nedenle değişiyorsa uyumu düşüktür. Çok sayıda modüle bağlanması da bunun doğal sonucu olabilir. Bağımlılık grafiği bu düğümleri gösterir; fakat "çok bağlantı = kötü tasarım" sonucu otomatik değildir. Merkezi ve kararlı bir alan soyutlaması da doğal olarak çok sayıda bağlantıya sahip olabilir.
Erken soyutlamanın maliyeti
İki kod parçası benziyor diye hemen ortak üst sınıfa taşınmamalıdır. Gerçek değişim ekseni görülmeden yapılan ortaklaştırma, gelecekte iki bağımsız davranışı yanlış bir sözleşmeye bağlayabilir.
Benzer satırları değil, birlikte değişen bilgiyi soyutlamak gerekir.
Ünite 6: Hata Yönetimi ve Hata Sözleşmesi
Hata yolu normal akışı boğmamalıdır
Java'da istisnalar hata akışını normal sonuçtan ayırabilir; fakat kontrolü yerel olmayan biçimde aktarırlar. Hangi hatanın hangi katmanda anlam kazandığı açık olmalıdır.
null ile hata taşımak çağıranları dağıtılmış denetime zorlar:
public Media load(final long id) {
return repository.findById(id);
}Çağıran her yer null ihtimalini tekrar çözmek zorunda kalır. "Bulunamadı" bu katmanda istisnai bir durumsa sözleşme açık hale getirilebilir:
public Media load(final long id) {
final Media media = repository.findById(id);
if (media == null) {
throw new MediaNotFoundException(id);
}
return media;
}Bunun tersi de mümkündür. "Sonuç yok" normal iş akışının parçasıysa Optional, boş koleksiyon veya açık bir sonuç tipi daha doğru olabilir. Kural, her null değerini istisnaya çevirmek değildir.
Soyutlama değiştiği yerde bağlam eklemek
Düşük seviyeli hatayı tüm sisteme sızdırmak sınırı bozar:
try {
return jdbcRepository.load(id);
} catch (final SQLException e) {
throw e;
}Repository sınırı altyapı ayrıntısını kendi hata modeline çevirebilir:
try {
return jdbcRepository.load(id);
} catch (final SQLException e) {
throw new UserRepositoryException("Cannot load user " + id, e);
}Her istisnayı mekanik olarak yeniden sarmalamak da değersizdir. Yeni bağlam, soyutlama düzeyi değiştiğinde eklenmelidir.
Hata yutmak
try {
write(record);
} catch (final IOException ignored) {
}Bu kod başarısızlığı ortadan kaldırmaz; yalnız görünürlüğünü yok eder. Log yazıp devam etmek de her zaman doğru değildir. Çağıranın sonucu başarısız kabul edip etmeyeceği sözleşmenin parçasıdır.
Kısmi durum
İşlem yarıda kaldığında geride kalan durum tanımlı olmalıdır. Veritabanında transaction, dosyada atomik yayınlama, mesajlaşmada idempotency veya telafi mekanizması farklı çözümlerdir. Hata yönetimi yalnız exception sınıfı seçimi değildir; sistemin başarısızlıktan sonra hangi durumda kalacağını tanımlar.
Ünite 7: Sınır Kodu ve Dış Bağımlılıklar
Dış modelin çekirdeğe sızmasını önlemek
Bir harici servisin DTO'su bütün uygulamada dolaşırsa sağlayıcının alan değişikliği doğrudan çekirdek değişikliğine dönüşür.
Sınır bu bağımlılığı yerelleştirebilir:
public interface PersonnelGateway {
User findById(final String id);
}public final class RestPersonnelGateway implements PersonnelGateway {
private final RestClient restClient;
public RestPersonnelGateway(final RestClient restClient) {
this.restClient = restClient;
}
@Override
public User findById(final String id) {
final ExternalUser response = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(ExternalUser.class);
return new User(response.id(), response.name(), response.department());
}
}İş katmanı ExternalUser tipini bilmez. Sağlayıcı değişirse esas değişim sınır sınıfında kalır.
Her bağımlılık sarmalanmamalıdır
Kararlı, küçük ve uygulamanın her yerinde doğal biçimde kullanılan bir standart kütüphaneyi sırf "boundary" ilkesi adına yapay bir arayüzün arkasına koymak ek maliyet üretir. Sınırın değeri volatiliteyi ve değişim yüzeyini yerelleştirmesidir.
Öğrenme testleri
Bir kütüphanenin belgesinde yazan davranış ile belirli sürümde gözlenen davranış aynı kabul edilmemelidir. Küçük öğrenme testleri bağımlılık yükseltildiğinde uygulamanın varsayımlarının hâlâ geçerli olup olmadığını gösterebilir.
Legacy kodda Feathers'ın "seam" yaklaşımı da benzer bir hedef taşır: davranışı geniş çaplı yeniden yazım yapmadan değiştirilebilecek bir nokta bulmak.
Ünite 8: Test Edilebilirliğin Tasarım Üzerindeki Baskısı
Test zorluğu çoğu zaman tasarım sinyalidir
Bir sınıfı sınamak için gerçek veritabanı, dosya sistemi ve ağ bağlantısı kurmak gerekiyorsa sorun yalnız test altyapısında değildir; bağımlılıklar davranışın içine gömülmüştür.
Sorunlu kurulum:
public final class ReportService {
private final Repository repository = new OracleRepository();
private final Clock clock = Clock.systemUTC();
public Report create() {
return repository.load(LocalDate.now(clock));
}
}Bağımlılıklar dışarıdan verildiğinde davranış kontrol edilebilir hale gelir:
public final class ReportService {
private final Repository repository;
private final Clock clock;
public ReportService(final Repository repository, final Clock clock) {
this.repository = repository;
this.clock = clock;
}
public Report create() {
return repository.load(LocalDate.now(clock));
}
}Testte saat sabitlenebilir:
final Clock clock = Clock.fixed(
Instant.parse("2026-09-08T09:00:00Z"),
ZoneOffset.UTC
);
final ReportService service = new ReportService(repository, clock);Bu örnek "her şeyi enjekte et" anlamına gelmez. Yan etkisiz ve kararlı yardımcıları soyutlamak testten daha fazla karmaşıklık üretebilir.
Test davranışı korumalıdır
İç implementasyona aşırı bağlanan test, refactoring sırasında davranış değişmese bile kırılır. Testin asıl değeri yeniden düzenleme sırasında dış davranışın aynı kaldığını göstermesidir.
TDD'nin kalıcı katkılarından biri de burada görülür: arayüzü yazan sınıfın içinden değil, onu kullanan tarafın gözünden tasarlamaya zorlar.
Ünite 9: Ölçüm, Statik Analiz ve Otomasyon
Araçların kapsadığı alan
Statik analiz programı çalıştırmadan kodu inceler. Kullanılmayan değerler, belirli kaynak sızıntıları, şüpheli kontrol akışı, tip sorunları ve dilin izin verdiği riskli kalıplar araç tarafından bulunabilir. İş kuralının doğru olup olmadığı veya p99 gecikme hedefinin karşılanıp karşılanmadığı ise başka doğrulama yöntemleri gerektirir.
Döngüsel karmaşıklık
McCabe'in ölçütü tek bileşenli kontrol akış grafiğinde şu biçimde ifade edilir:
V(G) = E - N + 2Ölçütün değeri, bağımsız yolların sayısı hakkında fikir vermesidir. Yüksek değer inceleme sinyalidir; otomatik kalite hükmü değildir.
Aynı iş kuralı çok sayıda dala yayılmışsa koşul nesneye taşınabilir:
public boolean isProcessable(final Media media) {
return media.status() == MediaStatus.ACTIVE
&& media.durationSeconds() > 0
&& !media.deleted();
}Bu metodun varlığı karmaşıklığı sihirli biçimde yok etmez. Değerli olan, alan kararının tek isimli yerde toplanmasıdır.
Tekrarlanabilir kuralı insana bırakmamak
Baykar stajım sırasında programlama standartları üzerine çalışırken kuralların bir bölümünü otomatik denetleyen bir kaynak kod analiz programı geliştirdim. Çözüm basit metin araması yapmıyor; kaynağı karakter karakter işlerken yorum, string, önişlemci ve normal kod durumlarını ayırmaya çalışıyordu.
Aynı karakter dizisinin bulunduğu bağlama göre farklı anlam taşıması bu ayrımı zorunlu kılıyordu. String içindeki yorum başlangıcı yorum değildir; koşul içindeki atama operatörü de bağlam gerektirir. Daha sonra cppcheck ve splint çıktıları aynı sonuç modeline dönüştürüldü. Bu çalışmadan kalan ilke nettir: deterministik olarak denetlenebilen bir kural insan hafızasına bırakılmamalıdır. Dönemin ayrıntıları Baykar yazılım stajı kaydında yer alır.
Kod incelemesi
Biçim ve mekanik kurallar otomasyona bırakıldığında inceleme zamanı "neden" sorusuna ayrılır: değişiklik niçin yapıldı, hangi alternatif elendi, hangi sınır durum düşünüldü, bağımlılık yönü doğru mu, yeni risk nerede oluşuyor?
Ünite 10: Eşzamanlı Kodda Okunabilirliğin Sınırı
Thread-safe primitive, thread-safe iş akışı değildir
Paylaşılan değiştirilebilir durum eşzamanlı kodun temel maliyetidir. Basit bir sayaç bile veri yarışı üretir:
private long processed;
public void markProcessed() {
processed++;
}AtomicLong tekil artışı güvenli hale getirir:
private final AtomicLong processed = new AtomicLong();
public void markProcessed() {
processed.incrementAndGet();
}Fakat iki atomik işlemden oluşan bileşik karar yine yarışabilir:
if (processed.get() < limit) {
processed.incrementAndGet();
processItem();
}Burada her çağrı tek başına thread-safe olsa da "limit aşılmayacak" invariantı iki çağrının arasındadır. Senkronizasyon sınırı primitive'e değil invarianta göre seçilmelidir.
Paylaşımı azaltmak
En ucuz kilit, ihtiyaç duyulmayan kilittir. Durum görev içinde yerel tutulabiliyorsa senkronizasyon problemi baştan ortadan kalkar. Değişmez nesneler de yayımlandıktan sonra mutasyon yarışlarını azaltır.
Kilit kapsamı
Büyük kritik bölüm doğruluğu anlamayı kolaylaştırabilir fakat contention üretir. Çok küçük kritik bölüm performansı iyileştirebilir fakat tek invariantı iki parçaya bölebilir. Bu karar profil ve yük testiyle desteklenmelidir.
Temiz eşzamanlı kodun hedefi "akıcı görünmek" değil; sahiplik, invariant ve senkronizasyon sınırlarını görünür kılmaktır.
Ünite 11: Kuralların Bağlama Bağlılığı
Başarım duyarlı kod
Soyutlama katmanı, ara nesne ve koleksiyon dönüşümleri sıcak yolda gerçek maliyet oluşturabilir. Bu nedenle performans kritik kodda "daha temiz" görünen biçim otomatik olarak daha iyi değildir.
Örneğin yalnızca bir filtreleme için yeni liste üretmek:
public List<Sample> positiveSamples(final List<Sample> samples) {
return samples.stream()
.filter(sample -> sample.value() > 0)
.toList();
}genel uygulama kodunda yeterince açık olabilir. Milyonlarca örneğin işlendiği tahsis duyarlı sıcak yolda ise yerinde işleme veya önceden ayrılmış tampon daha doğru olabilir.
Kural şudur: optimizasyon tahminle değil ölçümle yapılır; okunabilirlikten verilen ödün yalnız ölçülen sıcak bölgede tutulur.
Gerçek zamanlı ve gömülü kısıtlar
En kötü durum yürütme süresi, kuyruk sınırı, bloklama davranışı ve bellek tahsisi gerçek zamanlı sistemlerde ortalama throughput'tan daha önemli olabilir. Bu tür kodda temizliğin bir parçası da kaynak sınırlarının görünür olmasıdır.
Tarihsel bağlam
Eski bir kod tabanındaki sıra dışı kararın bugünkü araçlarla gereksiz görünmesi, kararın yazıldığı dönemde de gereksiz olduğu anlamına gelmez. Eski optimizasyonu silmeden önce o dönemin derleyici, donanım ve çalışma zamanı kısıtları araştırılmalıdır.
Ünite 12: Biçimlendirme Bir Görsel Sözleşmedir
Dikey ve yatay düzen
Biçimlendirme salt estetik değildir. İlişkili ifadeler birbirine yakın, ayrı kavramlar görsel olarak ayrılmış olmalıdır. Dosya üstten alta doğru genel davranıştan ayrıntıya ilerleyebiliyorsa okuyucunun gezinme maliyeti azalır.
Yoğun bir ifade:
if(user!=null&&user.isActive()&&rights.contains(user.id())&&!blocked.contains(user.id()))process(user);aynı semantiği daha görünür sınırlarla taşıyabilir:
if (user != null
&& user.isActive()
&& rights.contains(user.id())
&& !blocked.contains(user.id())) {
process(user);
}Burada çok satır her zaman üstün değildir. İfade kısa ve doğal olduğunda tek satır daha iyi olabilir. Amaç formatter çıktısını kutsallaştırmak değil, ilişkilerin gözle seçilebilmesini sağlamaktır.
Takım kuralı
Kod tabanında tek ve otomatik uygulanabilen biçim standardı, birden çok "iyi" kişisel stilden daha ucuzdur. Formatter ve linter tarafından çözülebilen konular kod incelemesinin ana tartışması olmamalıdır.
Ünite 13: Nesne, Veri Yapısı ve Davranışın Yeri
Private alan tek başına kapsülleme değildir
Getter ve setter ile bütün alanlarını doğrudan açan bir sınıf, alanları private olsa bile temsil kararını dışarı sızdırabilir.
Anemik hesap:
public final class Account {
private BigDecimal balance;
public BigDecimal getBalance() {
return balance;
}
public void setBalance(final BigDecimal balance) {
this.balance = balance;
}
}Davranışın nesnede olduğu biçim:
public final class Account {
private BigDecimal balance;
public Account(final BigDecimal balance) {
this.balance = balance;
}
public void withdraw(final BigDecimal amount) {
if (amount.signum() <= 0) {
throw new IllegalArgumentException("amount");
}
if (balance.compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
balance = balance.subtract(amount);
}
public BigDecimal balance() {
return balance;
}
}İkinci tasarımda "bakiyeden para eksiltmenin" invariantları sınıfın içinde kalır.
DTO davranışsız olabilir
Bu durum DTO'lara mekanik olarak uygulanmamalıdır:
public record PersonnelResponse(
String id,
String name,
String department
) {
}Bu tipin görevi veri taşımaktır. Her veri yapısını "zengin domain nesnesi" haline getirmek doğru değildir. Veri yapısı ile davranış nesnesinin değişim maliyetleri farklıdır.
Demeter yasası
Aşağıdaki zincir istemciye iç topolojiyi öğretir:
final String city = order.customer().address().city().name();Alan modeli gerçekten bu davranışı sahipleniyorsa daha dar sözleşme mümkündür:
final String city = order.deliveryCity();Buna karşılık fluent API çağrıları veya saf DTO gezinmesi aynı nedenle otomatik ihlal sayılmaz:
request.header("Accept", "application/json")
.timeout(timeout)
.send();Sorun nokta sayısı değil, istemcinin bilmek zorunda kaldığı iç temsil bilgisidir.
Ünite 14: Sistem Kurulumu ile Sistem Kullanımını Ayırmak
Construction ile use farklı sorumluluklardır
İş mantığı kendi altyapı bağımlılıklarını oluşturursa ne yapılacağı ile nasıl bağlanacağı aynı sınıfta kilitlenir:
public final class ReportService {
private final Repository repository = new OracleRepository();
private final Clock clock = Clock.systemUTC();
}Bağımlılıkları dışarıdan almak iş davranışını kurulum politikasından ayırır:
public final class ReportService {
private final Repository repository;
private final Clock clock;
public ReportService(final Repository repository, final Clock clock) {
this.repository = repository;
this.clock = clock;
}
}Composition root somut bileşenlerin bir araya getirildiği sınırlı alandır:
final Repository repository = new OracleRepository(dataSource);
final Clock clock = Clock.systemUTC();
final ReportService reportService = new ReportService(repository, clock);Spring veya başka bir DI container bu kurulumu otomatikleştirebilir. Fakat Dependency Injection ile Dependency Inversion aynı kavram değildir. Container kullanmak yanlış bağımlılık yönünü kendiliğinden düzeltmez.
Cross-cutting concern
Transaction, yetkilendirme, loglama ve telemetry birçok sınıfı kesebilir. Interceptor, proxy veya aspect bu politikayı merkezileştirebilir; ancak görünmeyen runtime davranışı da üretir. Kritik sistemlerde yan etkilerin izlenebilirliği, tekrar azaltma kadar önemlidir.
Ünite 15: Basit Tasarım ve Ardışık İyileştirme
İlk doğru çözüm son tasarım olmak zorunda değildir
Davranış önce çalışır ve doğrulanabilir hale getirilebilir; ardından yapı küçük adımlarla iyileştirilebilir. Büyük yeniden yazımda davranış hatası ile tasarım değişikliği aynı diff içinde karışır.
Tekrar edilen satır ile tekrar edilen bilgi aynı değildir
İki hesaplama farklı sözcüklerle aynı iş kuralını uyguluyorsa gerçek tekrar vardır:
final BigDecimal invoiceTax = invoiceTotal.multiply(new BigDecimal("0.20"));
final BigDecimal refundTax = refundTotal.multiply(new BigDecimal("0.20"));Ortak karar adlandırılabilir:
private static final BigDecimal VAT_RATE = new BigDecimal("0.20");
public BigDecimal calculateVat(final BigDecimal amount) {
return amount.multiply(VAT_RATE);
}Ancak iki benzer formül farklı mevzuat veya farklı ürün politikaları nedeniyle bağımsız değişecekse sırf satırlar benziyor diye ortak metoda taşınması yanlış soyutlama olabilir.
Dört pratik öncelik
Basit tasarımın yararlı bir sıralaması şudur: davranış doğrulanabilir olmalı, aynı bilgi gereksiz yere tekrarlanmamalı, niyet açık olmalı ve gereksiz yapı düşük tutulmalıdır.
Son madde diğerlerini ezmemelidir. "Az sınıf" uğruna sorumlulukları birleştirmek de, "temiz mimari" uğruna her sınıf için bir interface ve factory üretmek de mekanik tasarımdır.
Ünite 16: Kod Kokularını Tanı Aracı Olarak Kullanmak
Koku hata hükmü değildir
Kod kokusu inceleme sinyalidir. Uzun parametre listesi, flag argument, feature envy, ölü fonksiyon, tekrar eden koşul veya aşırı büyük sınıf bağlama göre meşru olabilir.
Feature envy
Bir metod sürekli başka nesnenin verilerini çekip karar veriyorsa davranışın yanlış yerde olma ihtimali vardır:
public BigDecimal discount(final Customer customer) {
if (customer.orders().size() > 20
&& customer.totalSpend().compareTo(VIP_LIMIT) > 0) {
return VIP_DISCOUNT;
}
return BigDecimal.ZERO;
}Bu karar gerçekten Customer yaşam döngüsüne aitse:
public BigDecimal discount() {
if (orders.size() > 20 && totalSpend.compareTo(VIP_LIMIT) > 0) {
return VIP_DISCOUNT;
}
return BigDecimal.ZERO;
}daha yerel olabilir. Ancak indirim politikası ayrı bir pazarlama kuralı olarak sık değişiyorsa davranışı Customer içine taşımak tersine bağlaşımı artırır. Kokunun kendisi taşıma kararını vermez.
Sınır koşullarını merkezileştirmek
Aynı sınır üç biçimde yazıldığında hata ihtimali artar:
if (index < items.size() - 1) {
index++;
}if (selected + 1 < items.size()) {
selected++;
}Ortak davranış gerçekten aynıysa adlandırılabilir:
private boolean hasNext(final int index, final int size) {
return index + 1 < size;
}Tarih, indeks, kapasite, paket boyutu ve timeout hataları çoğu zaman ortada değil sınırlarda ortaya çıkar.
Gizli zaman bağımlılığı
initialize() çağrılmadan start() çalışmaması gerekiyorsa bu yalnız dokümantasyonda bırakılmamalıdır. Kurucu, factory veya durum modeli geçersiz çağrı sırasını mümkünse yapısal olarak engellemelidir.
Ünite 17: Eşzamanlılıkta Yürütme Modeli ve Kapanış
Eşzamanlılık hız ile eş anlamlı değildir
Eşzamanlılık "ne yapılacağı" ile "ne zaman yapılacağı" kararlarını ayırır. I/O ağırlıklı işlerde beklemeleri örtüştürerek throughput artırabilir; CPU-bound işlerde görev sayısını artırmak contention, context switch ve cache baskısını büyütebilir.
Modern Java'da virtual thread I/O ağırlıklı çok sayıda görevin thread maliyetini azaltabilir:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (final Task task : tasks) {
executor.submit(() -> process(task));
}
}Bu örnek yarış durumunu çözmez. Virtual thread çalışma biriminin maliyet modelini değiştirir; paylaşılan mutable state, backpressure ve invariant problemleri aynen kalır.
Producer-consumer ve backpressure
Üretici tüketiciden hızlıysa sınırsız kuyruk yalnız gecikmeyi belleğe taşır. Kapasitesi belli bir kuyruk, reddetme veya yavaşlatma politikasıyla birlikte tasarlanmalıdır. "Asenkron" olmak sınırsız kaynak anlamına gelmez.
Kapanış bir protokoldür
Başlatılabilen bir servis düzgün kapatılamıyorsa yaşam döngüsü eksiktir:
public final class Worker implements AutoCloseable {
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
public void submit(final Runnable task) {
executor.submit(task);
}
@Override
public void close() {
executor.shutdown();
}
}Gerçek sistemde yalnız shutdown() çağrısı yetmeyebilir. Yeni görev kabulünün kesilmesi, çalışan işlerin tamamlanması veya iptal edilmesi, interrupt sinyalinin korunması, kuyrukların boşaltılması ve kaynakların belirli sürede kapanması ayrı kararlardır.
Eşzamanlı test ispat değildir
Concurrency testi bir kez geçerek doğruluk kanıtlamaz. Farklı işçi sayıları, yoğun tekrar, zamanlama baskısı ve farklı platformlar daha çok interleaving görme olasılığı sağlar. Tekrarlanması zor hata otomatik olarak "flaky test" diye silinmemelidir; yarış durumunun belirtisi olabilir.
Stres testi de formal ispat değildir. Yalnız hatayı görünür kılma olasılığını artırır.
Genel Kavramsal Çerçeve
Kod kalitesi bir üslup tercihi değil, bakım maliyetinin ve değişiklik riskinin bileşenidir. Ayrıştırma, değişim eksenine göre yapılır; doğru sınır bir karar değiştiğinde değişiklik yüzeyini daraltır. Otomasyon tekrarlanabilir kuralı, inceleme ise gerekçeyi denetler.
Temiz kod ilkeleri mutlak kurallar değildir. Başarım duyarlı, gerçek zamanlı, gömülü ve güvenlik kritik sistemlerde aynı kural farklı bir maliyet üretir. Bu nedenle bilinçli istisna, ölçülen gerekçesiyle birlikte yerel tutulmalıdır.
Ayırt edilmesi gereken noktalar:
- Biçimlendirme okunabilirliğin tamamı değildir.
- Kısa fonksiyon tek sorumluluk garantisi değildir.
- Yüksek test kapsamı doğru sınama garantisi değildir.
- Düşük döngüsel karmaşıklık anlaşılır tasarım garantisi değildir.
- Statik analiz uyarısı olmaması hata olmadığı anlamına gelmez.
- Çok yorum kararların iyi belgelendiği anlamına gelmez.
- Refactoring davranış değişikliği değildir.
- Thread-safe primitive thread-safe iş akışı değildir.
- Dependency injection doğru bağımlılık yönünü kendiliğinden oluşturmaz.
- Benzer kod her zaman aynı bilginin tekrarı değildir.
- İmmutability bazı hata sınıflarını azaltır; tahsis maliyetini yok etmez.
- Virtual thread yarış durumunu veya backpressure problemini çözmez.
Kaynaklar
- David L. Parnas. On the Criteria To Be Used in Decomposing Systems into Modules. Communications of the ACM, 15(12), 1972. DOI
- Edsger W. Dijkstra. Go To Statement Considered Harmful. Communications of the ACM, 11(3), 1968. DOI
- Thomas J. McCabe. A Complexity Measure. IEEE Transactions on Software Engineering, SE-2(4), 1976. DOI
- Donald E. Knuth. Literate Programming. The Computer Journal, 27(2), 1984. DOI
- Meir M. Lehman. Programs, Life Cycles, and Laws of Software Evolution. Proceedings of the IEEE, 68(9), 1980. DOI
- Barbara H. Liskov; Jeannette M. Wing. A Behavioral Notion of Subtyping. ACM TOPLAS, 16(6), 1994. DOI
- Brian W. Kernighan; P. J. Plauger. The Elements of Programming Style, 2. baskı. McGraw-Hill, 1978.
- Frederick P. Brooks. The Mythical Man-Month. Addison-Wesley, 1975.
- Steve McConnell. Code Complete, 2. baskı. Microsoft Press, 2004.
- Kent Beck. Test-Driven Development: By Example. Addison-Wesley, 2002.
- Michael C. Feathers. Working Effectively with Legacy Code. Prentice Hall, 2004.
- Martin Fowler. Refactoring: Improving the Design of Existing Code, 2. baskı. Addison-Wesley, 2018.
- Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2008.
- Robert C. Martin. The Clean Coder: A Code of Conduct for Professional Programmers. Prentice Hall, 2011.
- International Organization for Standardization. ISO/IEC 25010:2011 — Systems and software Quality Requirements and Evaluation (SQuaRE). ISO, 2011. URL
- International Organization for Standardization. ISO/IEC 25010:2023 — Systems and software engineering — Systems and software Quality Requirements and Evaluation (SQuaRE) — Product quality model. ISO, 2023. URL