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?
Temiz kod estetik bir tercih değil, bakım maliyeti ve değişiklik riski problemidir. 2015 dönemindeki kaynaklar ile 2026'daki araç ve Java pratikleri farklı tarihsel bağlamlarda değerlendirilmelidir. Ö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ı
Bir kod değişikliğinin kalitesi yalnız satır sayısındaki azalmayla değerlendirilmez. Önce korunması gereken mevcut davranış ve bu davranışı sınayan testler belirlenir. Değişiklikten sonra işlevsel eşdeğerlik, yürütme maliyeti ve hata ayıklama kolaylığı karşılaştırılır; yeni soyutlama yalnız değişiklik maliyetini gerçekten düşürüyorsa gerekçelendirilir. Bir fonksiyonun kısalması fakat davranışının başka dosyalara dağılması, bakım yükünü azaltmış olmak zorunda değildir.
1.1. Kalite bir Sıfat Değil, Ölçüt Kümesidir
"İyi kod" tek boyutlu bir yargı değildir. 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 terminolojisi ile 2026'da geçerli 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.
1.2. 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.
1.3. 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.
1.4. Kod Kalitesini Değişiklik Maliyeti ve Doğrulanabilirlik Üzerinden Okumak
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.
- iş parçacığı-safe primitive iş parçacığı-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 iş parçacığı yarış durumunu veya backpressure problemini çözmez.
1.5. Bakım Yapılabilirliği Değişiklik Üzerinden Ölçmek
Kod kalitesi yalnız isimlendirme veya biçim kurallarıyla değerlendirilemez. Küçük bir iş kuralı değişikliğinin kaç modüle yayıldığı, kaç farklı sözleşmeyi etkilediği ve hata durumunda davranışın ne kadar öngörülebilir olduğu daha somut sinyallerdir.
Yeniden düzenleme öncesinde davranışı koruyan testlerin bulunması riski azaltır. Değişiklik sonrasında yalnız test geçişi değil, bağımlılık yönü, performans ve hata mesajı gibi dışarıdan gözlenebilir sözleşmeler de kontrol edilmelidir.
Soyutlama gerçek varyasyonu sakladığında değerlidir. Yeni katmanın hangi tekrarı veya değişim eksenini azalttığı açıklanamıyorsa ek karmaşıklık bakım maliyetini artırabilir.
1.6. Değişiklik Yerelliği ve Davranış Eşdeğerliği
Bakımı kolay kodun önemli bir özelliği, küçük bir gereksinim değişikliğinin küçük ve öngörülebilir bir yüzeyde kalabilmesidir. Aynı davranışı düzeltmek için birbirinden uzak çok sayıda dosyaya dokunmak gerekiyorsa problem yalnız satır sayısı değil, davranışın sistemde dağılmış olmasıdır.
Bu nedenle yeniden düzenleme sırasında hedef “daha fazla soyutlama” değil, daha küçük davranış yayılımı olmalıdır. Yeni yardımcı sınıf, katman veya ortak fonksiyon ancak değişim eksenini gerçekten tek yerde topluyorsa değerlidir. Aksi halde çağrı zincirini uzatıp okuma maliyetini artırabilir.
Davranış eşdeğerliği de yalnız başarılı yolun aynı görünmesi değildir. Hata kodu, klavye davranışı, sıra kararlılığı, zaman aşımı, veri yazma sayısı ve yetkilendirme sonucu gibi gözlenebilir özellikler de kontrata dahil olabilir. Güvenli refactoring bu özellikleri önce tanımlar, sonra yapısal değişikliği uygular.
önce: davranış + kanıt
|
v
yapısal değişiklik
|
v
sonra: aynı davranış + aynı/iyileşmiş kanıtBir değişiklik sırasında ilgisiz temizliği de aynı pakete eklemek diff'i büyütür ve hata kaynağını belirsizleştirir. Gereksiz kod gerçekten davranış taşımıyorsa kaldırılabilir; ancak kaldırma kararı “eski görünüyor” gerekçesine değil çağrı, veri ve test kanıtına dayanmalıdır. Bu yaklaşım Yazılım Mühendisliği içindeki aşamalı modernizasyon ile doğrudan ilişkilidir.
1.7. Anlaşılabilirlik bir Güvenilirlik Özelliğidir
Güvenlik ve güvenilirlik ayrı sonradan-eklenen özellikler değil, ortak hata ve kötüye kullanım yüzeyini yöneten tasarım özellikleridir. Bakım mühendisliğinde anlaşılabilir kod tek başına yeterli değildir; değişikliğin kapsadığı bileşen sayısı, yetki sınırları, kaynak tüketimi, güvenli varsayılanlar ve geri alma mekanizması da küçültülmelidir ki rutin bir refactoring geniş bir blast radius üretmesin. Özellikle DoS sınıfı problemlerde güvenlik kontrolünün kendisinin kullanılabilirliği tüketmemesi gerekir.
- Kod okunabilirliğini yalnız stil değil güvenli değişiklik yapabilme kapasitesi olarak değerlendirmek.
- Kritik invariantların isimlendirme ve modül sınırlarında görünür tutulması.
- Hata sonrası incelemede davranışın kaynak koddan yeniden kurulabilmesinin hedeflenmesi.
Ünite 2: Okuma Maliyeti ve İsimlendirme
2.1. 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.
2.2. 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.
2.3. 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
Fonksiyon sınırı belirlenirken birlikte değişen iş kuralları ile farklı nedenlerle değişen ayrıntılar ayrılır. Bir bölümün dışarı alınması, çağıran kodu daha anlaşılır kılarken parametre ve ara durum aktarımını aşırı artırıyorsa bölme noktası yeniden düşünülmelidir. Arayüzün önkoşulları, yan etkileri ve hata durumları görünür olmalıdır. İyi ayrıştırma, yalnız daha çok fonksiyon üretmek değil, bir değişiklik yapıldığında etkilenen davranışların öngörülebilir kalmasını sağlamaktır.
3.1. 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.
3.2. Koruma Koşulları ve İç İç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.
3.3. 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.
3.4. 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
4.1. 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);
}4.2. 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.
4.3. 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
Bu ünite, değişmesi olası ayrıntıları kararlı çekirdekten sözleşme sınırlarıyla ayırarak değişiklik yayılımını azaltma ilkesini ele alır.
kararlı çekirdek
|
v
soyutlama / sözleşme
|
v
değişken ayrıntı
veritabanı / protokol / dosya biçimi / dış API
değişirse çekirdek mümkün olduğunca değişmez5.1. 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.
5.2. 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.
5.3. 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.
5.4. Bilişsel Yük ve Değişiklik Yayılımı
Kodun "temiz" görünmesi öznel olabilir; bakım maliyetini daha somut ölçmek için değişiklik yayılımına bakılabilir. Basit bir iş kuralı değişikliği kaç modüle, teste ve yapılandırma dosyasına dokunuyor? Aynı bilgi birden çok yerde temsil ediliyorsa değişiklik koordinasyon maliyeti büyür.
tek gereksinim değişikliği
|
v
modül A
/ \
v v
modül B modül C
| / \
v v v
test B test C entegrasyon
fan-out büyüdükçe değişiklik maliyeti ve regresyon yüzeyi genişler.Soyutlama sayısını artırmak her zaman temizliği artırmaz. Bir davranışı anlamak için beş interface, üç factory ve iki decorator arasında dolaşmak gerekiyorsa yerel okunabilirlik bozulabilir. Soyutlama, gerçek varyasyonu sakladığında değerlidir; henüz var olmayan olasılıklar için eklenen katmanlar borç oluşturabilir.
Bakım yapılabilirlikte güçlü sinyal, küçük bir değişikliğin küçük kalabilmesidir. Modül sınırı, test sözleşmesi ve hata modeli bu hedefi desteklemiyorsa isimlendirme düzeltmesi tek başına yeterli değildir.
5.5. SOLID, YAGNI ve Tasarım Kalıplarını Değişiklik Maliyeti Üzerinden Okumak
Temiz kod ilkeleri ezberlenmiş sloganlara dönüştüğünde amaçlarının tersine çalışabilir. "Her sınıf tek iş yapsın", "her şey interface olsun", "kalıp kullanmak profesyonelliktir" gibi mekanik yorumlar sınıf sayısını artırırken bakım maliyetini de yükseltebilir.
Daha yararlı soru şudur:
Bu ilke hangi değişiklik türünün hangi koda yayılmasını önlüyor?
5.6. Tek Sorumluluk İlkesi: Sınıfın Küçük Olması Değil Değişim Ekseninin Ayrılması
Tek Sorumluluk İlkesi (Single Responsibility Principle, SRP) "bir sınıfta yalnız bir metot olsun" demek değildir.
Bir modül:
fiyat hesaplama
PDF oluşturma
e-posta gönderme
veritabanına yazmaişlerini birlikte taşıyorsa dört farklı değişim nedeni aynı yerde birleşebilir.
Daha güçlü ayrım:
domain policy
presentation
delivery
persistencegibi farklı değişim eksenlerini ayrı sözleşmelere taşımaktır.
Buna karşılık sürekli birlikte değişen beş küçük sınıfı sırf SRP adına ayırmak gerçek bakım yerelliğini artırmayabilir.
5.7. Açık/Kapalı İlkesi: Hiçbir Zaman Kod Değiştirmemek Değildir
Açık/Kapalı İlkesi (Open/Closed Principle, OCP), sistemin her geleceği önceden tahmin eden soyutlamalarla doldurulmasını istemez.
Amaç, kanıtlanmış bir değişim ekseni varsa yeni varyant eklerken kararlı çekirdeğin gereksiz yere yeniden yazılmasını azaltmaktır.
Örneğin gerçekten çoğalan fiyat politikalarında:
interface PricingPolicy {
Money price(Order order);
}gibi strateji sınırı doğal olabilir.
Ancak sistemde tek ve sabit fiyat politikası varken gelecekte belki gerekir diye interface + abstract factory + registry üretmek OCP değil, erken genellemedir.
5.8. Liskov Yerine Geçme İlkesi Davranış Sözleşmesidir
Liskov Yerine Geçme İlkesi (LSP), alt sınıfın yalnız aynı metot imzasına sahip olmasını değil, üst türün beklenen davranış sözleşmesini korumasını ister.
Alt tür:
- kabul edilen girdiyi gereksiz daraltmamalı,
- başarılı sonucun anlamını değiştirmemeli,
- üst türün garanti ettiği invariantları bozmamalı,
- beklenmeyen yeni yan etkiler eklememeli.
Klasik problem:
üst tür: değiştirilebilir Rectangle
alt tür: width ve height'i zorla eşitleyen Squaregibi modelde değildir yalnız. Asıl ders, kalıtımın "is-a" cümlesiyle değil davranış substitutability ile doğrulanmasıdır.
Composition çoğu zaman davranış sözleşmesini daha görünür kılar.
5.9. Arayüz Ayırma İlkesi: Küçük Değil Tüketiciye Özgü Sözleşme
Arayüz Ayırma İlkesi (Interface Segregation Principle, ISP), her interface'in iki metottan küçük olmasını söylemez.
Sorun, tüketicinin kullanmadığı operasyonlara bağımlı bırakılmasıdır.
Worker
├─ work()
├─ eat()
├─ sleep()
├─ approvePayroll()
└─ configureNetwork()gibi tek interface farklı aktörleri gereksiz yere bağlar.
Daha doğru sınır, birlikte kullanılan ve birlikte değişen davranışları aynı sözleşmede tutmaktır.
5.10. Bağımlılıkların Tersine Çevrilmesi: İnterface Sayısını Artırmak Değildir
Dependency Inversion Principle (DIP) yüksek seviyeli politikanın düşük seviyeli ayrıntının fiziksel API'sine doğrudan bağlanmasını azaltır.
iş kuralı
↓
işin ihtiyaç duyduğu port / sözleşme
↑
veritabanı, HTTP, dosya adaptörüBu, her sınıf için IXxxService üretmek anlamına gelmez.
Soyutlama:
- bir test sınırı,
- bir teknoloji değişim sınırı,
- bir bağımsız politika sınırı
yaratmıyorsa yalnız çağrı zincirini uzatabilir.
5.11. SOLID İlkeleri Birbirinden Bağımsız Kutular Değildir
Örneğin:
SRP
-> değişim eksenini ayırır
ISP
-> tüketicinin gereksiz bağımlılığını azaltır
DIP
-> yüksek seviyeli politikayı altyapıdan ayırır
LSP
-> soyutlamanın davranış bakımından güvenilir olmasını ister
OCP
-> kanıtlanmış varyasyon ekseninde yeni davranış eklemeyi kolaylaştırırBu ilkeler ortak olarak değişiklik yayılımını ve yanlış coupling'i azaltmaya çalışır.
5.12. YAGNI: Gereksinimi Reddetmek Değil Kanıtsız Genellemeyi Ertelemek
YAGNI ("You Aren't Gonna Need It"), geleceği hiç düşünmemek değildir.
Şu ayrımı yapar:
gerekli esneklik:
mevcut gereksinim veya güçlü değişim kanıtı var
spekülatif esneklik:
"belki bir gün" dışında kanıt yokSpekülatif katman:
- daha fazla sınıf,
- daha fazla yapılandırma,
- daha büyük test matrisi,
- daha fazla hata yolu
demektir.
Ancak güvenlik, veri migrasyonu, geriye uyumluluk veya düzenleyici gereksinim gibi önceden tasarlanması gereken konuları YAGNI diyerek ertelemek de hatalıdır.
5.13. Tasarım Kalıbı Çözüm Adı Değil Problem Dili Olmalıdır
Factory, Strategy, Observer, Decorator, Adapter veya Command gibi kalıplar, ekiplerin tekrar eden tasarım kuvvetlerini adlandırmasına yardım eder.
Bir kalıp için şu dört soru sorulmalıdır:
problem ne?
hangi kuvvetler çatışıyor?
kalıp hangi trade-off'u seçiyor?
hangi yeni maliyeti getiriyor?Örneğin Strategy:
yarar:
değişen algoritmayı açık sınırda değiştirir
maliyet:
daha fazla nesne ve dolaylı çağrıDecorator:
yarar:
davranışı bileşimle katmanlar
maliyet:
çağrı zincirini ve hata izini uzatabilirSingleton:
yarar:
kontrollü tek sahiplik gereken bazı kaynaklarda yararlı olabilir
risk:
global mutable state ve test coupling'i üretebilirKalıbın adı gerekçe değildir.
5.14. Composition Over İnheritance
Kalıtım güçlüdür; ancak alt sınıf üst sınıfın iç uygulamasına ve yaşam döngüsüne sıkı bağlanabilir.
Bileşim:
Service
has-a Policymodelinde davranış açık bir işbirlikçi üzerinden değiştirilebilir.
Ancak composition da sınırsız interface zinciri kurmak anlamına gelmez. Bir davranış gerçekten değişmiyorsa doğrudan kod daha anlaşılır olabilir.
5.15. Kalıp Kötüye Kullanım Belirtileri
Aşağıdaki durumlar tasarımın kendisinden daha büyük soyutlama ürettiğini gösterebilir:
tek implementation için yıllarca yaşayan interface
bir nesne üretmek için gereksiz factory zinciri
global state'i saklayan singleton
iki satırlık varyasyon için strategy hiyerarşisi
veritabanındaki her tablo için mekanik repository/service katmanı
sırf "clean architecture" diyagramına uymak için passthrough sınıflarıBunlar otomatik hata değildir; fakat "bu katman hangi değişiklik maliyetini düşürüyor?" sorusunu gerektirir.
5.16. Refactoring ile Redesign Ayrımı
Refactoring, gözlenebilir davranışı koruyarak iç yapıyı iyileştirir.
Bir değişiklik:
- API sözleşmesini,
- hata kodunu,
- transaction sınırını,
- sıra davranışını,
- kullanıcı etkileşimini
değiştiriyorsa yalnız refactoring değildir; davranışsal tasarım değişikliği de içerir.
Bu ayrım code review ve regresyon testinin kapsamını belirler.
5.17. Temiz Kod için Pratik Karar Sırası
Yeni soyutlama eklemeden önce:
1. Tekrar eden bilgi gerçekten var mı?
2. Bu kod parçaları aynı nedenle mi değişiyor?
3. Tüketiciler aynı sözleşmeye mi ihtiyaç duyuyor?
4. Soyutlama test veya teknoloji sınırı oluşturuyor mu?
5. Kalıtım davranış sözleşmesini güvenle koruyor mu?
6. Yeni katman çağrı zincirini ne kadar uzatıyor?
7. Bugün kanıtlanmamış hangi geleceği modellemeye çalışıyorum?
8. Daha basit çözüm aynı değişiklik yerelliğini sağlayabilir mi?soruları sorulabilir.
Temiz kodun hedefi "en fazla prensip uygulanan" kod değil; niyeti açık, değişikliği yerel, davranışı doğrulanabilir ve gereksiz yapı yükü düşük kod üretmektir.
Ünite 6: Hata Yönetimi ve Hata Sözleşmesi
Bu ünite, altyapı hatalarının sınır katmanında anlamlı bağlama dönüştürülmesini ve uygulamaya kararlı bir hata sözleşmesiyle taşınmasını ele alır.
altyapı hatası
|
v
sınır katmanı
|
+--> bağlam ekle
+--> kararlı hata sözleşmesine dönüştür
|
v
uygulama politikası
|
+--> retry / kullanıcı mesajı / telafi / sonlandırma6.1. 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.
6.2. 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.
6.3. 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.
6.4. 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.
6.5. Hata Ailesi, Fiziksel Kaynak ve Çözüm Sözleşmesi
Exception sınıfının adı ile hatanın fiziksel kaynağı aynı şey değildir. Bir uygulama DataException adında bir istisnayı veri bütünlüğü, dış servis cevabı, ORM güvenlik kontrolü veya gerçekten veritabanından gelen bir hata için kullanabilir. Yalnız sınıf adına bakarak bütün bu durumları “veritabanı hatası” saymak, hem kullanıcı mesajını hem de teknik teşhisi bozar.
Daha kararlı bir hata modeli birkaç ayrı ekseni birlikte tutar:
Exception ailesi -> uygulamanın hataya nasıl davranacağı
Hata kodu -> somut ve kararlı problem türü
Kaynak -> problemin çıktığı katman
Neden -> eksik, geçersiz, zaman aşımı, çakışma gibi biçim
Çözüm -> bir sonraki güvenli adım
Sorumlu -> kullanıcı, yerel yönetici, kaynak sistem veya teknik ekipBu ayrımın önemli sonucu, hata sınıflandırmasının controller veya ekran bazında tekrar edilmemesidir. Aynı JDBC bağlantı hatası farklı uç noktalarde ortaya çıkabilir; her uç noktanın ayrı yorum üretmesi zamanla mesaj ve davranış ayrışmasına yol açar. Fiziksel kaynak belirleme bilgisi birlikte değişiyorsa merkezi bir classifier veya hata sözleşmesi doğal değişim sınırıdır.
Merkezi sınıflandırma her exception için yeni sınıf üretmek anlamına gelmez. Tersine, yüzlerce küçük exception türü yerine az sayıda davranış ailesi ve merkezi, kararlı hata kodları bakım maliyetini düşürebilir. Explicit olarak tanımlanmış domain hatası, generic cause-chain tahmininden önce gelmelidir; çünkü uygulama hatanın bağlamını altyapı katmanından daha iyi biliyor olabilir.
Cause chain incelemesinde de sınırsız gezinmek gerekmez. Döngüye dayanıklı ve sınırlı bir traversal, yalnız hata yolunda çalıştığı için normal hot path'i etkilemeden JDBC, dosya veya ağ kök nedenini bulabilir. Aynı isimli domain ve altyapı exception'ları ayrıca ayrılmalıdır; uygulama düzeyindeki bir “kilit” çatışması ile veritabanı deadkilidi aynı çözüm yoluna sahip değildir.
Kullanıcıya gösterilen hata sözleşmesi ile teknik log sözleşmesi de ayrı kalmalıdır. Kullanıcı güvenli kaynak, neden ve çözümü görürken teknik günlük gerçek exception zincirini tutabilir. Bu ayrımın güvenlik yönü Güvenli Yazılım Mühendisliği, doğrulama yönü ise Yazılım Test Mühendisliği ile birlikte değerlendirilmelidir.
Ünite 7: Sınır Kodu ve Dış Bağımlılıklar
7.1. 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.
7.2. 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.
7.3. Öğ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.
7.4. Build Bağımlılık Grafiği de Yazılım Mimarisinin Parçasıdır
Kaynak kod içindeki sınıf bağımlılıkları ile build sistemindeki kütüphane bağımlılıkları farklı grafiklerdir. İkisi de sistem davranışını ve değişiklik riskini etkiler.
Bir uygulamanın gerçek bağımlılık yüzeyi yalnız doğrudan yazılan dependency satırlarından oluşmaz:
uygulama
↓
doğrudan bağımlılık
↓
transitif bağımlılık
↓
başka transitif bağımlılıklarBu grafikteki bir düğüm değiştiğinde kaynak kod hiç değişmese bile derleme davranışı, çalışma zamanı classpath, serialization, HTTP istemcisi, logging altyapısı, bytecode instrumentation, security davranışı veya native entegrasyon değişebilir.
Bu nedenle bağımlılık yönetimi yalnız build aracının çözeceği paket problemi değildir. Bakım mühendisliğinin bir parçasıdır.
7.5. Doğrudan Bağımlılık ile Transitif Bağımlılık Aynı Sözleşme Değildir
Bir sınıfı uygulama kodunda doğrudan kullanıyorsak o kütüphane fiilen uygulamanın sözleşmesinin parçasıdır.
Buna karşılık yalnız başka bir kütüphanenin transitif olarak getirdiği API üzerine uygulama kodu yazmak daha kırılgan bir bağımlılık oluşturabilir:
uygulama
↓
A kütüphanesi
↓
B kütüphanesi
uygulama ---------> B APIBurada uygulama gerçekte B kütüphanesine de bağımlıdır; fakat build tanımı bu sahipliği açıkça göstermiyor olabilir.
A sonraki sürümde B bağımlılığını kaldırdığında uygulama davranışı beklenmedik biçimde kırılabilir.
Genel ilke şudur:
kodu doğrudan kullandığın bağımlılığı
mimari olarak da doğrudan sahiplenBu, her transitif dependency için ayrıca deklarasyon yapılması gerektiği anlamına gelmez. Yalnız uygulamanın gerçekten API sözleşmesi kurduğu bağımlılığın görünür olması gerekir.
7.6. Dependency Graph Küçük bir Tedarik Zinciridir
Bir production artifact incelenirken yalnız repository içindeki Java dosyalarına bakmak yeterli değildir.
Şu sorular da cevaplanabilmelidir:
- Hangi artifact neden var?
- Doğrudan mı transitif mi geliyor?
- Hangi bileşen tarafından getiriliyor?
- Hangi sürüm gerçekten resolve edildi?
- Aynı kütüphanenin farklı sürüm talepleri varsa hangisi kazandı?
- Bir exclusion neden eklendi?
- Elle yapılan version override hangi uyumluluk varsayımına dayanıyor?
Bu bilgiler yalnız güvenlik açısından değil, bakım ve hata teşhisi açısından da değerlidir.
7.7. BOM bir Sürüm Listesinden Daha Fazlasıdır
Spring Boot gibi platformlar birbiriyle birlikte yönetilen dependency sürümlerini bir BOM üzerinden sağlayabilir.
Bu modelin mühendislik değeri, çok sayıda bağımsız sürüm kararını bir platform baseline altında toplamaktır.
Uygulamanın her alt dependency için bağımsız sürüm seçmesi yerine bir uyumluluk kümesi esas alınır.
Bu, bütün uyumluluk problemlerini ortadan kaldırmaz. Uygulama yine farklı sürüm gerektirebilir. Fakat override artık sıradan bir sürüm satırı değil, bilinçli bir uyumluluk istisnasıdır.
Version override işlemi mümkünse gerekçesi ve doğrulama sınırıyla birlikte tutulmalıdır.
7.8. Version Convergence Doğruluk Kanıtı Değildir
Build sisteminin tek bir dependency sürümü seçebilmesi yalnız çözümleme işleminin tamamlandığını gösterir.
Classpath teknik olarak oluşturulmuş olsa da semantik uyumluluk bozuk olabilir.
Hata compile time, startup, ilk ilgili kod yolu, belirli veri biçimi veya belirli production yükü altında ortaya çıkabilir.
Bu nedenle dependency convergence, integration test ve gerçek kullanım yolunun doğrulanmasının yerine geçmez.
7.9. En Küçük Açıklanabilir Dependency Yüzeyi
Kullanılmayan dependency yalnız disk alanı değildir. Classpath, startup taraması, auto-configuration olasılıkları, native-image reachability, CVE izleme yüzeyi, lisans yüzeyi, upgrade grafiği ve çakışma olasılığı genişleyebilir.
Bununla birlikte mekanik olarak bütün transitif bağımlılıkları exclude etmek de doğru değildir.
Amaç minimum dependency sayısı değil, gereksinimi karşılayan en küçük açıklanabilir dependency yüzeyidir.
7.10. Starter Kullanmak Görünmez Dependency Kullanmak Anlamına Gelmez
Bir starter, ortak bir kullanım senaryosu için dependency grubunu anlamlı bir paket altında toplayabilir.
Bu geliştirme hızını artırır; fakat hangi capabilitylerin classpath içine girdiği yine bilinmelidir.
Özellikle Spring Boot ortamında classpath yalnız derleme girdisi değildir. Aynı zamanda bazı auto-configuration kararlarının girdisidir.
Dolayısıyla dependency eklemek hem yeni bir API ekleyebilir hem de çalışma zamanı configuration grafiğini değiştirebilir.
7.11. Exclusion bir Temizlik İşlemi Değil, Mimari Karardır
Bir transitif dependency dışlandığında neden gereksiz olduğu, hangi parent üzerinden geldiği, parent bileşenin onu çalışma zamanı için varsayıp varsaymadığı, yerine başka implementation gelip gelmediği ve gelecekteki bir upgrade'in bu varsayımı değiştirip değiştiremeyeceği açıklanmalıdır.
Exclusion listesi zaman içinde açıklaması unutulan bir katmana dönüşürse build dosyası da legacy kod gibi bakım borcu üretir.
7.12. Dependency Upgrade Bağımsız bir Kod Değişikliğidir
Kaynak kod diffi sıfır olsa bile bir kütüphane sürüm değişikliği davranış değişikliğidir.
Varsayılan yapılandırma (configuration), serileştirme (serialization), zaman aşımı (timeout), yeniden deneme (yeniden deneme), SQL üretimi, günlükleme (logging), iş parçacığı (iş parçacığı) kullanımı, yansıma (reflection), doğrulama (validation), güvenlik varsayımları veya performans karakteristiği değişebilir.
Bu nedenle büyük dependency upgrade işlemlerini yalnız build başarılı ölçütüyle değerlendirmek zayıftır.
Daha güçlü doğrulama zinciri:
dependency diff
↓
release notes / migration notes
↓
etkilenen kullanım yolları
↓
integration / regression test
↓
gerekiyorsa performans ölçümüşeklindedir.
7.13. Dependency Diff Kod İncelemesinin Parçasıdır
Bir değişiklik yalnız bir build dosyasında birkaç satır değiştirebilir; fakat resolve edilen graph farkı çok daha büyük olabilir.
Maven tarafındaki dependency:tree veya Gradle dependency raporları burada teşhis aracıdır. Araç çıktısı kararın kendisi değildir; hangi dependency yolunun neden oluştuğunu görünür kılar.
7.14. Build Reproducibility ile Dependency Yönetimi Birleşir
Bugün oluşturulan artifact ile altı ay sonra aynı source revision üzerinden oluşturulan artifact farklı dependency sürümlerine çözülüyorsa davranış da değişebilir.
İstenen ilişki şudur:
source revision
+ build configuration
+ dependency resolution
+ toolchain
=
yeniden üretilebilir artifactBu nedenle dependency yönetimi konfigürasyon yönetiminin de parçasıdır.
7.15. Bakım Açısından Pratik İnceleme Modeli
Bir bağımlılık değişikliğinde şu sıra kullanılabilir:
1. Bu dependency hangi gereksinimi karşılıyor?
2. Doğrudan mı transitif mi?
3. Gerçekte hangi sürüm resolve ediliyor?
4. Platform/BOM sürümünü override ediyor muyuz?
5. Neden?
6. Classpath değişikliği auto-configuration davranışını değiştiriyor mu?
7. Başka hangi artifactler değişiyor?
8. Hangi API veya runtime davranışı etkilenebilir?
9. Hangi test bu varsayımı doğruluyor?
10. Dependency kaldırıldığında hangi capability kayboluyor?Hedef, build graph içinde de değişiklik nedenini ve maliyetini görünür tutmaktır.
7.16. İlgili Teknik Kaynaklar
- Spring Boot, Build Systems and Dependency Management: https://docs.spring.io/spring-boot/reference/using/build-systems.html
- Spring Boot, Managing Dependencies: https://docs.spring.io/spring-boot/gradle-plugin/managing-dependencies.html
- Apache Maven Dependency Plugin, dependency:tree: https://maven.apache.org/plugins/maven-dependency-plugin/tree-mojo.html
Ünite 8: Test Edilebilirliğin Tasarım Üzerindeki Baskısı
8.1. 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.
8.2. 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
9.1. 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.
9.2. 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.
9.3. Tekrarlanabilir Kuralı İnsana 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.
9.4. Kod İncelemesi
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?
Bir iyileştirmenin hız kazandırdığı iddiası, yalnız tek çalıştırmadaki süreye dayandırılmamalıdır. Hazırlık maliyeti, JIT derleme, dosya sistemi önbelleği, işlemci frekansı ve eşzamanlı arka plan işi ölçümleri etkiler. Karşılaştırılacak sürümlerde aynı iş yükü, sabit doğruluk kontrolü ve birden fazla bağımsız ölçüm uygulanmalıdır. Ortalama hız artışı raporlanırken dağılım, uç değer ve en kötü kabul edilebilir gecikme de değerlendirilmelidir. Kod daha kısa olsa bile doğruluk veya gecikme kararlılığı bozuluyorsa bu değişiklik gerçek bir mühendislik iyileştirmesi değildir.
Ünite 10: Eşzamanlı Kodda Okunabilirliğin Sınırı
10.1. İş Parçacığı (Thread)-Safe Primitive, İş parçacığı-safe İş 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 iş parçacığı-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.
10.2. 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.
10.3. 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ığı
11.1. 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.
11.2. 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 işlem hacminden (throughput) 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.
11.3. 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
12.1. 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.
12.2. 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
13.1. 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.
13.2. 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.
13.3. 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
14.1. 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.
14.2. 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 çalışma zamanı (runtime) davranışı da üretir. Kritik sistemlerde yan etkilerin izlenebilirliği, tekrar azaltma kadar önemlidir.
14.3. Spring Uygulamalarında Bağımlılık ve Yapılandırma Mühendisliği
Temiz kod ilkeleri çerçeve (framework) kullanılan bir uygulamada yalnız sınıf ve metotların kaynak koduna bakılarak değerlendirilemez. Spring gibi bir çalışma ortamında nesne oluşturma, bağımlılık çözümleme, koşullu bean üretimi, proxy zinciri ve dış yapılandırma da programın gerçek davranışının parçasıdır.
Kaynak kodda doğrudan görünmeyen bu katman büyüdükçe bakım problemi yalnız kod okuma problemi olmaktan çıkar:
kaynak kod
+ bean grafiği
+ yapılandırma
+ koşullu etkinleşme
+ proxy davranışı
+ çalışma ortamı
=
gerçek uygulama davranışıBu nedenle çerçeve kullanan sistemlerde okunabilirlik, çalıştırılan nesne grafiğinin ve davranış sınırlarının da anlaşılabilir olmasını gerektirir.
14.4. Bean Sayısı bir Kalite Metriği Değildir
Çok sayıda bean bulunması tek başına kötü tasarım değildir. Büyük bir sistem doğal olarak çok sayıda bileşen içerebilir. Asıl soru, bu bileşenlerin neden var olduğu ve aralarındaki bağımlılık yönünün açıklanabilir olup olmadığıdır.
Bir bağımlılık grafiği ideal olarak iş akışını takip eden yönlü bir yapı oluşturur:
Controller
↓
Application Service
↓
Domain / Repository / GatewaySorun sınıf sayısının büyümesi değil, sorumlulukların birbirine geri dönmeye başlamasıdır:
A
↓
B
↓
C
↓
ABu çevrim, nesnelerin hangi sırayla üretileceğinden daha önemli bir tasarım sorusunu ortaya çıkarır: A, B olmadan; B, C olmadan; C de A olmadan anlamlı değilse sorumluluk sınırı gerçekten doğru yerde midir?
Circular dependency bu nedenle yalnız Spring container tarafından çözülmesi gereken bir başlatma problemi değildir. Çoğu durumda karşılıklı bilgi sahipliği, yanlış yerleştirilmiş orkestrasyon veya iki bileşenin aynı değişim eksenine sahip olması araştırılmalıdır.
Çözüm otomatik olarak yeni bir interface oluşturmak da değildir. Bazen iki servisin üzerinde açık bir iş akışı/orkestratör (workflow/orchestrator) katmanı kurmak, bazen ortak kararın tek bir bileşende sahiplenilmesi, bazen de veri akışının tek yönlü hale getirilmesi daha doğrudur.
14.5. Constructor İnjection Bağımlılığı Görünür Kılar
Zorunlu bir bağımlılığın nesne oluşturulurken sağlanması, sınıfın geçerli durumda oluşmasını kolaylaştırır:
@Service
public final class ReportService {
private final ReportRepository repository;
private final Clock clock;
public ReportService(final ReportRepository repository, final Clock clock) {
this.repository = repository;
this.clock = clock;
}
}Buradaki mühendislik değeri anotasyon tercihinden daha geniştir. Sınıfın çalışabilmesi için hangi bileşenlerin zorunlu olduğu doğrudan constructor sözleşmesinden okunabilir.
Bağımlılıklar gizli alan enjeksiyonu, global erişim veya çalışma sırasında yapılan service lookup ile edinildiğinde nesne geçerli görünmesine rağmen aslında eksik durumda olabilir. Bu da test kurulumunu, statik incelemeyi ve dependency graph analizini zorlaştırır.
Bununla birlikte constructor parametresi sayısı da mekanik bir kalite eşiği değildir. Çok sayıda zorunlu bağımlılık, sınıfın fazla sorumluluk taşıdığına dair bir inceleme sinyalidir; tek başına parçalama emri değildir.
14.6. Yapılandırma da Durum Uzayı Oluşturur
Kod sabit kalırken yapılandırma değiştiğinde sistem davranışı değişebiliyorsa yapılandırma, çalıştırılabilir sistemin girdilerinden biridir.
Aşağıdaki değişkenler birbirinden bağımsız görünse bile kombinasyon halinde farklı çalışma durumları oluşturabilir:
Spring profile
environment variable
feature flag
conditional bean
database target
timeout
queue capacity
security policyÜç bağımsız boolean özellik yalnızca sekiz teorik kombinasyon üretir. On bağımsız özellik 1024 kombinasyona çıkar. Gerçek sistemlerde değerler boolean da olmadığı için yapılandırma durum uzayı çok daha hızlı büyür.
Bu nedenle her yeni profile veya özellik flag bedelsiz değildir. Yapılandırma çeşitliliği arttıkça hangi kombinasyonların desteklendiği, geçersiz kombinasyonların başlangıçta reddedilip reddedilmediği, varsayılanların güvenli olup olmadığı ve aynı kararın birden fazla yerde tekrar tanımlanıp tanımlanmadığı açık olmalıdır.
Birbirine ait ayarların tipli bir yapı altında toplanması bu sözleşmeyi görünür kılabilir:
@ConfigurationProperties(prefix = "app.media")
public record MediaProperties(
Duration requestTimeout,
int batchSize,
int queueCapacity
) {
}Amaç her değeri ayrı bir sınıfa taşımak değildir. Aynı yaşam döngüsüne ve aynı doğrulama sınırına sahip ayarların tek bir anlamlı sözleşmede temsil edilmesidir.
14.7. Auto-configuration ile Sahiplik Sınırı
Spring Boot auto-configuration uygulamanın classpath, mevcut bean ve property durumuna göre uygun altyapıyı kurabilir. Bunun avantajı tekrar eden wiring kodunun azaltılmasıdır.
Ancak otomatik yapılandırma, görünmeyen davranışın sorgulanmaması anlamına gelmez.
Bir uygulama varsayılan bean davranışını değiştirdiğinde hangi varsayılanın değiştirildiği, neden değiştirildiği, yeni bean'in hangi koşulda etkin olduğu, eski davranışın hangi koşulda geri geldiği ve çerçeve sürümü değiştiğinde bu varsayımın korunup korunmadığı açıklanabilmelidir.
Her şeyi manuel tanımlamak ile her şeyi çerçeve varsayımına bırakmak iki uç yaklaşımdır. Daha sürdürülebilir sınır, uygulamanın gerçekten sahip olduğu kararları açıkça tanımlamak ve çerçeve tarafından güvenilir biçimde sağlanan mekanizmayı gereksiz yere tekrar etmemektir.
14.8. Proxy Sınırı Kaynak Kodda Görünmeyen bir Davranış Sınırıdır
Spring AOP tabanlı bazı özelliklerde çağrı doğrudan hedef nesneye değil proxy üzerinden gerçekleşir:
çağıran
↓
Spring proxy
↓
transaction / authorization / telemetry advice
↓
hedef nesneHedef nesnenin kendi içinden yaptığı doğrudan self-invocation ise aynı proxy yolundan geçmeyebilir.
Bu ayrıntı özellikle @Transactional gibi proxy tabanlı davranışlarda önemlidir. Kaynak kodda anotasyonun bulunması tek başına beklenen advice zincirinin çalıştığını kanıtlamaz.
Bakım açısından problem yalnız transaction açılmaması değildir. Metodu okuyan mühendis kaynak koddan farklı bir çalışma zamanı modeli çıkarabilir.
Bu nedenle transaction, yeniden deneme (retry), authorization, önbellek veya telemetry gibi cross-cutting davranışlar kritik bir metodun sözleşmesini değiştiriyorsa proxy sınırı ve çağrı yolu gözden geçirilebilir olmalıdır.
14.9. Çerçeve Soyutlaması Maliyeti Ortadan Kaldırmaz
Bir repository metodu tek satır görünebilir:
repository.findAllByStatus(status);Fakat altında SQL üretimi, bağlantı havuzu beklemesi, ağ gidiş-dönüşü, veri tabanı planı, entity oluşturma ve serialization bulunabilir.
Benzer biçimde bir anotasyon tek satır olabilir fakat uygulama başlatılırken bean graph, proxy veya çalışma zamanı metadata üretimi oluşturabilir.
Temiz kod bu nedenle yalnız kaynak kod satırını küçültmek değildir. Soyutlamanın gizlediği maliyet gerektiğinde yeniden görülebilmelidir.
Performans tarafındaki fiziksel maliyet modeli Java Tabanlı Veri Sistemlerinde Yüksek Başarım notunda ayrıntılı ele alınmaktadır.
14.10. Spring Tabanlı Kod İncelemesinde Karar Sınırları
Bir Spring değişikliğini değerlendirirken yalnız biçim ve sınıf boyutuna değil şu sınırlara bakmak daha değerlidir:
- Dependency yönü tek taraflı ve açıklanabilir mi?
- Nesnenin zorunlu bağımlılıkları oluşturulurken görünür mü?
- Yeni ayar gerçekten yeni bir davranış ekseni mi?
- Varsayılan davranış güvenli ve belgelenebilir mi?
- Bean hangi koşullarda var veya yok?
- Çağrı beklenen advice zincirinden geçiyor mu?
- Transaction sınırı gerçek iş birimiyle uyumlu mu?
- Çalışma zamanındaki gerçek karar gözlenebilir mi?
- çerçeve sürümü değiştiğinde hangi varsayım kırılabilir?
Amaç daha fazla çerçeve kodu üretmek değildir. Uygulamanın görünmeyen davranış yüzeyini mümkün olduğunca küçük ve açıklanabilir tutmaktır.
Ünite 15: Basit Tasarım ve Ardışık İyileştirme
15.1. İ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.
15.2. 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.
15.3. 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
16.1. Koku Hata Hükmü Değildir
Kod kokusu inceleme sinyalidir. Uzun parametre listesi, bayrak argümanı (flag argument), özellik (feature) envy, ölü fonksiyon, tekrar eden koşul veya aşırı büyük sınıf bağlama göre meşru olabilir.
16.2. Özellik 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.
16.3. 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.
16.4. 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ış
Bu ünite, üretici-tüketici akışında bounded queue, backpressure ve kontrollü kapanışın aynı yürütme modelinin parçaları olduğunu ele alır.
producer ---> bounded queue ---> consumer
| | |
| +--> backpressure|
| |
stop request -----------------------+
|
v
yeni işi kes -> kuyruk politikasını uygula -> çalışanları bitir -> kaynakları kapat17.1. 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 işlem hacmi artırabilir; CPU ağırlıklı (CPU-bound) işlerde görev sayısını artırmak contention, bağlam değiştirme (context switch) ve önbellek (cache) baskısını büyütebilir.
Modern Java'da virtual iş parçacığı I/O ağırlıklı çok sayıda görevin iş parçacığı maliyetini azaltabilir:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (final Task task : tasks) {
executor.submit(() -> process(task));
}
}Bu örnek yarış durumunu çözmez. Virtual iş parçacığı çalışma biriminin maliyet modelini değiştirir; paylaşılan mutable state, backpressure ve invariant problemleri aynen kalır.
17.2. 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.
17.3. 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.
17.4. Eşzamanlı Test İspat 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.
Kaynakça
- 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.
- David L. Parnas. On the Criteria To Be Used in Decomposing Systems into Modules. Communications of the ACM, 15(12), 1972. DOI
- Donald E. Knuth. Literate Programming. The Computer Journal, 27(2), 1984. DOI
- Edsger W. Dijkstra. Go To Statement Considered Harmful. Communications of the ACM, 11(3), 1968. DOI
- Erich Gamma; Richard Helm; Ralph Johnson; John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994.
- Frederick P. Brooks. The Mythical Man-Month. Addison-Wesley, 1975.
- Heather Adkins; Betsy Beyer; Paul Blankinship; Piotr Lewandowski; Ana Oprea; Adam Stubblefield. Building Secure & Reliable Systems: Best Practices for Designing, Implementing, and Maintaining Systems. O’Reilly Media, 2020.
- 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
- Kent Beck. Test-Driven Development: By Example. Addison-Wesley, 2002.
- Martin Fowler. Refactoring: Improving the Design of Existing Code, 2. baskı. Addison-Wesley, 2018.
- Meir M. Lehman. Programs, Life Cycles, and Laws of Software Evolution. Proceedings of the IEEE, 68(9), 1980. DOI
- Michael C. Feathers. Working Effectively with Legacy Code. Prentice Hall, 2004.
- 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.
- Spring Boot, Developing Auto-configuration: https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html
- Spring Boot, Properties and Configuration: https://docs.spring.io/spring-boot/how-to/properties-and-configuration.html
- Spring Framework, Declarative Transaction Implementation: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-decl-explained.html
- Spring Framework, Proxying Mechanisms: https://docs.spring.io/spring-framework/reference/core/aop/proxying.html
- Steve McConnell. Code Complete, 2. baskı. Microsoft Press, 2004.
- Thomas J. McCabe. A Complexity Measure. IEEE Transactions on Software Engineering, SE-2(4), 1976. DOI