Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Coding Conventions ve Ekip Standartları — #01

Coding Convention Nedir, Neden İnsana Değil Araca Bırakılır? Kaynaklar, Seviyeler ve Ekip Kararı

Mert Özen 14 Kas 2026 12 dk 3 görüntülenme
Coding Convention Nedir, Neden İnsana Değil Araca Bırakılır? Kaynaklar, Seviyeler ve Ekip Kararı

Coding convention'ın değeri kuralın kendisinden değil, tartışılmaz ve araçla uygulanır olmasından gelir: Bookstore'da tek satırlık bir indirim değişikliği editör ayarları yüzünden 55 satırlık diff'e gömüldü, .editorconfig ve dotnet format onu tekrar tek satıra indirdi. Yazı formatting, stil, naming ve mimari kural seviyelerini, kaynak seçimini ve ekipte kararı bir kez verip kayda geçirmeyi anlatıyor.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 (SDK 10.0.402) ile derlenip denenmiştir.

Önce Tahmin Et: Bu PR'da Kaç Satır Değişti?

Bookstore'un sipariş modülünde küçük bir iş kuralı değişiyor: toplu alım indirimi üç kitapta değil, beş kitapta başlayacak. Grace değişikliği yapıyor ve PR açıyor. Commit mesajı tek cümle, değişiklik tek sabit. Reviewer'ın önüne gelen tablo ise şu:

$ git log --oneline
23e4c7b Raise bulk discount threshold to 5
bfa313e Add OrderService

$ git diff --stat HEAD~1 HEAD
 src/Bookstore.Ordering/OrderService.cs | 55 +++++++++++++++++-----------------
 1 file changed, 28 insertions(+), 27 deletions(-)

Dosya 33 satırlık bir OrderService. Grace'in editörü kendi kişisel ayarlarıyla dosyayı kaydederken yeniden biçimlendirmiş: girinti tab'a dönmüş, file-scoped namespace süslü parantezli bloğa çevrilmiş, açık tipli değişkenler var olmuş, field erişimlerinin başına this. eklenmiş. Reviewer olarak sana iki soru: bu 55 satırın kaçı programın davranışını değiştiriyor ve sen o satırı diff'in içinde ilk bakışta bulabilir misin? Bir de git diff -w ile boşlukları yok saydığında kaç satır kalır, tahmin et.

Coding Convention Neyi Çözer?

Coding convention, bir ekibin kodu nasıl yazacağına dair verdiği ve dilin kendisinin zorunlu tutmadığı kararlardır: girinti kaç boşluk, private field'ın adı alt çizgiyle mi başlar, tip açık mı yazılır, using'ler namespace'in içinde mi dışında mı durur. Derleyici bunların hiçbirine bakmaz. var subtotal = 0m; ile decimal subtotal = 0m; aynı IL (.NET derleyicisinin ürettiği ara kod) kodunu üretir.

O hâlde neden önemli? Çünkü kodu derleyici değil insanlar okur ve okuma maliyeti tutarsızlıkla artar. Bir dosyada _items, diğerinde m_items, üçüncüsünde this.items görürsen her seferinde beynin "bu farklı bir şey mi?" diye durur. Daha somut bedel review'da: açılıştaki PR'da tek bir davranış değişikliği 54 satırlık biçim değişikliğinin arasında kaybolmuş. Reviewer'ın dikkati sınırlı bir kaynak; bu dikkati girintiye harcarsan indirim eşiğinin 3'ten 5'e çıktığını kaçırırsın. Bu yazının asıl iddiası da buradan çıkıyor: convention'ın değeri kuralın kendisinden çok, kuralın tartışılmaz ve otomatik olmasından gelir. Hangi girinti seçildiği o kadar önemli değil; önemli olan herkesin aynısını kullanması ve bunu bir insanın değil bir aracın sağlaması.

Clean Code serisinin "Formatting ve Dosya Düzeni" yazısında biçimin okunabilirliğe etkisini, "Clean Code'da Naming" yazısında da isimlerin neden contract olduğunu konuştuk. Bu seri o fikirlerin ekip ölçeğindeki uygulamasıyla ilgileniyor: kuralları seçmek, bir dosyaya yazmak ve araçlara uygulatmak.

Dört Seviye, Dört Farklı Uygulayıcı

"Convention" kelimesi birbirinden çok farklı dört tür kuralı aynı torbaya koyuyor. Her birinin doğru uygulayıcısı başka; karıştırınca ya araç yanlış işe koşulur ya da insanlar aracın yapabileceği işi elle yapar.

SeviyeÖrnekDoğru uygulayıcıNerede yakalanır
FormattingGirinti, boşluk, satır kırma, satır sonuFormatter (CSharpier, dotnet format whitespace, Prettier)Editörde kayıtta, pre-commit, CI
Stil / dil tercihivar mı açık tip mi, file-scoped namespace, expression-bodied memberAnalyzer (IDE kuralları, ESLint)Build, CI
Naming_camelCase field, Async son eki, I ile başlayan interfaceAnalyzer (biçim) + review (anlam)Build ve PR review
Mimari kuralDomain katmanı Infrastructure'a referans vermezArchitecture testTest koşusu, CI

Formatting tamamen mekanik bir iştir: doğru cevap tektir ve bir program onu üretebilir. Bu yüzden formatting hakkında hiçbir insan yorum yazmamalı; formatter çalışır, tartışma biter. Stil tercihleri biraz daha anlamlıdır ama hâlâ kurala dökülebilir: Roslyn'in IDE ile başlayan code-style kuralları ve TypeScript tarafında ESLint bunları raporlar, çoğunu otomatik düzeltir.

Naming iki parçalı. Biçimi (alt çizgi, büyük harf, son ek) bir analyzer kontrol eder: .editorconfig'teki naming rule'lar IDE1006 kuralıyla build'de uyarı verir. Ama ismin doğru şeyi anlatıp anlatmadığını hiçbir araç bilemez; _data naming kuralına uyar ve yine de kötü bir isimdir. Bu kısım review'a kalır. Mimari kurallar ise stil değil yapı kurallarıdır; bir class'ın hangi namespace'e bağımlı olabileceğini NetArchTest ya da ArchUnitNET gibi kütüphanelerle yazılan testler korur. Bu seri ilk üç seviyeyle ilgileniyor; dördüncüsü "Clean, Onion ve Hexagonal Architecture" serisinin "Clean Architecture'ı Test Etmek ve Korumak" yazısında ve "Modular Monolith" serisinin "Module Sınırlarını Korumak" yazısında anlatıldı.

Bu ayrımın pratik sonucu şu: review checklist'inde "girinti doğru mu", "var kullanılmış mı" gibi maddeler varsa, o maddeler yanlış yerdedir. Review insanın bakması gereken yere, yani anlama, isimlere ve tasarıma ayrılır.

Araç Grace'in PR'ında Ne Gördü?

Önce tahminin cevabı. 55 satırın yalnız biri davranış değiştiriyor: BulkDiscountThreshold sabiti. git diff -w girinti farklarını yok sayınca tablo küçülüyor ama sıfırlanmıyor:

$ git diff -w --stat HEAD~1 HEAD
 src/Bookstore.Ordering/OrderService.cs | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

Kalan 17 satırın 16'sı hâlâ gereksiz değişiklik: namespace bloğu, üç var, iki this.. Bunlar boşluk değil, stil farkı; -w onları göremez. İşte seviyeler burada ayrışıyor. Ekip kurallarını repo köküne bir .editorconfig olarak koyup build'de zorladığımızda:

root = true

[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true

[*.cs]
indent_style = space
indent_size = 4

# Açık tip tercihi
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:warning
csharp_style_var_elsewhere = false:warning

# File-scoped namespace
csharp_style_namespace_declarations = file_scoped:warning

# this. kullanılmaz
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_method = false:warning

# Boşluk ve girinti (IDE0055)
dotnet_diagnostic.IDE0055.severity = warning
<Project>
  <PropertyGroup>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  </PropertyGroup>
</Project>

İkinci dosya repo kökündeki Directory.Build.props; code-style kurallarını komut satırı build'ine de taşıyan property bu. Grace'in dosyasıyla build (doküman linklerini ve proje yolunu kısalttık):

$ dotnet build src/Bookstore.Ordering --no-incremental
src/Bookstore.Ordering/OrderService.cs(1,11): warning IDE0161: Convert to file-scoped namespace
src/Bookstore.Ordering/OrderService.cs(3,1): warning IDE0055: Fix formatting
src/Bookstore.Ordering/OrderService.cs(5,1): warning IDE0055: Fix formatting
...
src/Bookstore.Ordering/OrderService.cs(20,4): warning IDE0008: Use explicit type instead of 'var'
src/Bookstore.Ordering/OrderService.cs(21,4): warning IDE0008: Use explicit type instead of 'var'
...
src/Bookstore.Ordering/OrderService.cs(28,4): warning IDE0008: Use explicit type instead of 'var'
...
Build succeeded.
    30 Warning(s)

30 uyarı: 26 tane IDE0055 (girinti), üç IDE0008 (var) ve bir IDE0161 (namespace). Dikkat edersen iki this. listede yok, oysa .editorconfig'te onlar için de :warning yazdık. Aynı kuralları dotnet format ile kontrol edince eksik parça görünüyor:

$ dotnet format src/Bookstore.Ordering --verify-no-changes
src/Bookstore.Ordering/OrderService.cs(3,1): error WHITESPACE: Fix whitespace formatting. Replace 1 characters with '\s\s\s\s'.
...
src/Bookstore.Ordering/OrderService.cs(1,11): warning IDE0161: Convert to file-scoped namespace
src/Bookstore.Ordering/OrderService.cs(15,13): warning IDE0003: Name can be simplified
src/Bookstore.Ordering/OrderService.cs(20,13): warning IDE0008: Use explicit type instead of 'var'
src/Bookstore.Ordering/OrderService.cs(21,13): warning IDE0008: Use explicit type instead of 'var'
src/Bookstore.Ordering/OrderService.cs(22,40): warning IDE0003: Name can be simplified
src/Bookstore.Ordering/OrderService.cs(28,13): warning IDE0008: Use explicit type instead of 'var'
$ echo $?
2

IDE0003 (gereksiz this.) build'de hiç çalışmayan bir avuç kuraldan biri; Roslyn kaynak kodunda performans gerekçesiyle "build'de asla" olarak işaretli. Ayrıntısını bir sonraki yazıda göreceğiz. Buradan çıkan sonuç daha genel: bir kuralı yazmak onun her katmanda uygulandığı anlamına gelmez, hangi aracın hangi kuralın sahibi olduğunu bilmek gerekir. Son adımda dotnet format'ı düzeltme modunda çalıştırıp dosyayı orijinal commit'le karşılaştırdık:

$ dotnet format src/Bookstore.Ordering
$ git diff --stat HEAD~1 -- src
 src/Bookstore.Ordering/OrderService.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

55 satırlık diff bir satıra indi: BulkDiscountThreshold = 3 yerine 5. Reviewer'ın görmesi gereken tek satır bu.

Kaynak Seçimi: Kimin Convention'ını Kopyalıyoruz?

Sıfırdan convention yazmak zaman kaybıdır. C# için birkaç ciddi başlangıç noktası var ve aralarındaki farkları bilmek, "Microsoft böyle diyor" tartışmalarını kısaltır.

Microsoft Learn'deki .NET Coding Conventions sayfası, dotnet/docs ve dotnet/samples repo'larındaki örnek kodların kuralları. Sayfa bu kuralları runtime ve Roslyn (C# derleyicisi) ekiplerinin stilinden uyarladığını açıkça yazıyor ve neden ayrıştıklarını da anlatıyor: doküman örneklerinin hedefi öğretmek ve yeni dil özelliklerini yaygınlaştırmak, runtime'ın hedefi ise hot path'lerde performans. Bu yüzden doküman kuralları yeni özelliklere daha hevesli; ayrıca satır uzunluğunu doküman sitesinde okunabilsin diye 65 karakterle sınırlıyor, ki bu bir uygulama kod tabanı için anlamsız derecede kısa. Sayfa kendini bağlayıcı bir liste olarak değil, örnek olarak sunuyor ve ayrıntılı API tasarımı için Framework Design Guidelines'a yönlendiriyor.

dotnet/runtime repo'sundaki C# Coding Style dosyası daha kısa ve daha katı. Private ve internal field'lar _camelCase, static field'lar s_, thread-static olanlar t_ önekiyle yazılıyor; var yalnız tip sağ taraftan açıkça görünüyorsa kullanılıyor; this. gerekmedikçe yazılmıyor; görünürlük her zaman açık yazılıyor. Kuralların başındaki genel ilke de öğretici: mevcut bir dosya bu kurallardan farklıysa, o dosyanın kendi stiline uy.

Framework Design Guidelines (Krzysztof Cwalina, Jeremy Barton ve Brad Abrams; üçüncü baskı 2020) ise farklı bir katman: kodun içini değil, dışarıya açılan API'yi anlatıyor. Public tiplerin, member'ların ve exception'ların nasıl adlandırılacağı ve tasarlanacağı burada. Kütüphane yazan bir ekip için naming kararlarının kaynağı bu olmalı. Microsoft Learn'deki Framework Design Guidelines bölümü kitabın 2008 tarihli ikinci baskısından alıntılanmış ve sayfanın kendisi bazı bilgilerin eskimiş olabileceğini söylüyor; güncel hâli üçüncü baskıda.

Angular tarafında kaynak tek: angular.dev'deki Angular Style Guide. Guide kendini zorunlu değil önerilen bir set olarak tanımlıyor, TypeScript'in genel kuralları için Google'ın TypeScript stil rehberine yönlendiriyor ve çatışmada dosyanın kendi tutarlılığını öne alıyor; kendi ifadesiyle "When in doubt, prefer consistency." (Angular Style Guide, angular.dev). Dördüncü yazıda bu guide'ın ana kararlarına bakacağız.

Pratik yol: birini temel al, kopyala, sonra ekibin gerçek ihtiyacına göre değiştir. Microsoft da dokümanında aynısını öneriyor ve kendi .editorconfig'ini başlangıç noktası olarak veriyor. Bookstore için runtime stilini temel aldık, çünkü bir uygulama kod tabanına doküman kurallarından daha yakın; tek sapmamız var konusunda, aşağıda.

Ekipte Karar: Bir Kez Tartış, Kaydet, Kapat

Convention'lar teknik olarak kolay, sosyal olarak zordur. Herkesin bir girinti tercihi vardır ve bu tercihler mantıktan çok alışkanlıktan gelir. Bu konunun bir adı var: bikeshedding. C. Northcote Parkinson 1957'de yayımlanan Parkinson's Law kitabında bir komitenin nükleer santral bütçesini dakikalar içinde onaylayıp bisiklet sundurmasının malzemesini saatlerce tartıştığı bir örnek anlatır; herkes sundurmayı anladığı için herkesin bir fikri vardır. Terim 1999'da Poul-Henning Kamp'ın FreeBSD e-posta listesine yazdığı bir mesajla yazılım dünyasında yaygınlaştı. Kod stili tam olarak bir bisiklet sundurmasıdır.

Bunu yönetmenin işe yarayan yolu üç adımlı. Birincisi, tartışmayı bir kez ve zaman kutusuyla yap: bir toplantı ya da bir PR, en fazla bir hafta. Karar çıkmazsa seçtiğin temel kaynağın varsayılanı kazanır; varsayılan zaten birinin kararı. İkincisi, kararı kayda geçir. Kısa bir convention dokümanı (bir sayfa) neyin seçildiğini ve neden seçildiğini söyler; tartışmalı kararların gerekçesi bir ADR (mimari bir kararı gerekçesiyle kaydeden kısa doküman) olarak yazılabilir. Nygard template'ini ve MADR (mimari kararlar için Markdown şablonu) formatını anlatan, "Architecture Decision'lar" serisinin "ADR Yazmak: Nygard Şablonu, MADR 4.0 ve Y-Statement ile Karar Kaydı" yazısındaki template burada da işler: bağlam, karar, sonuçlar. Altı ay sonra yeni gelen biri "neden var yasak?" diye sorduğunda cevap bir linktir, yeni bir tartışma değil. Üçüncüsü, dokümandaki her kuralın yanına onu uygulayan aracı yaz. Aracı olmayan kural bir dilektir.

Bookstore'un convention dokümanının başı şöyle:

# Bookstore Coding Conventions

Base: dotnet/runtime C# Coding Style, Angular Style Guide (v22).
Decision log: docs/adr/0007-coding-conventions.md

| Rule                                   | Enforced by                      |
|----------------------------------------|----------------------------------|
| Explicit types, no var                 | .editorconfig IDE0008            |
| File-scoped namespaces                 | .editorconfig IDE0161            |
| _camelCase private fields              | .editorconfig IDE1006            |
| Async suffix on async methods          | .editorconfig IDE1006            |
| No this. qualification                 | dotnet format (IDE0003, not on build) |
| Formatting (C#)                        | CSharpier                        |
| Formatting (TS, HTML, SCSS)            | Prettier                         |
| Angular selector prefix: bs            | angular-eslint                   |
| Domain must not reference Infrastructure | Architecture tests             |

Changing a rule: open a PR that edits .editorconfig and this table together.

Son satır önemli: kuralı değiştirmenin yolu dokümanı ve configuration'ı aynı PR'da değiştirmek. Doküman ile araç ayrışırsa her zaman araç kazanır, çünkü build onu uygular; doküman yalan söylemeye başlar.

Bookstore'un Kural Seti

Bu seri boyunca Bookstore'da şu kararlarla ilerleyeceğiz:

  • Açık tip: yerel değişkenlerde var yerine tipin adı. Microsoft dokümanı ve runtime var'a tip sağ taraftan belliyse izin veriyor; ekip, diff'leri ve review'ı IDE'siz (tarayıcıda) okuduğu için açık tipi seçti ve gerekçeyi ADR'a yazdı.
  • File-scoped namespace ve namespace dışında using.
  • Private field'lar _camelCase, static private field'lar da aynı biçimde (runtime'ın s_ önekini almadık; ekipte static field neredeyse yok).
  • Asenkron method'larda Async son eki, interface'lerde I öneki, sabitlerde PascalCase.
  • Primary constructor ve collection expression yerinde tercih edilir, zorlanmaz (suggestion).
  • C# biçimi CSharpier'ın, TypeScript, HTML (HyperText Markup Language) ve SCSS (Sass'ın CSS'e benzeyen syntax'ı) biçimi Prettier'ın; ikisinin ayarı neredeyse varsayılan.
  • Angular: Style Guide'ın v20 sonrası adlandırması (dosya adında tip son eki yok), selector öneki bs, inject().

Bu listenin neredeyse tamamı ikinci yazıda .editorconfig'e, üçüncüde formatter'a, dördüncüde ESLint ve Prettier'a, beşincide hook ve CI kapılarına dönüşecek.

Sık Düşülen Tuzaklar

Convention'ı wiki'de bırakmak. Confluence'ta güzel bir sayfa yazılır, iki ay sonra kimse açmaz, kod tabanı sayfadan uzaklaşır. Ölçüt basit: kuralın ihlali build'de, hook'ta ya da CI'da bir satır çıktı üretmiyorsa o kural fiilen yoktur.

Her kuralı ilk gün error yapmak. Mevcut bir kod tabanında yüzlerce ihlal varken bütün stil kurallarını error'a çekersen build kırılır, ekip ya kuralları kapatır ya da toplu #pragma ile susturur. Önce warning, önce yeni kod, sonra kademeli sıkılaştırma; beşinci yazıda legacy kod tabanına geçişi bu yüzden ayrı ele alıyoruz.

Formatting'i review'da tartışmak. "Burada bir boşluk fazla" yorumu, aracın yapması gereken işi en pahalı kaynağa, insanın dikkatine yükler. Formatter varsa bu yorum yazılmaz; yoksa önce formatter kurulur.

Naming'in tamamını araca bırakmak. Analyzer _bookCount ile _x'i aynı derecede doğru bulur. İsmin anlamı review'ın işi; araç yalnız biçimi garanti eder.

Kişisel IDE ayarlarını kural sanmak. Açılıştaki gereksiz değişikliklerin kaynağı Grace'in yanlış bir şey yapması değildi; repo'da kural olmadığı için editörü kendi varsayılanlarını uyguladı. Kural repo'da, .editorconfig ve configuration dosyalarında yaşamalı; kişisel ayarlar ancak repo bir şey söylemiyorsa devreye girer.

Bir dosyadaki her stili aynı anda değiştirmek. Bir bug fix'in içine "hazır buradayken" yapılan biçim temizliği, tam olarak açılıştaki 55 satırlık diff. Biçim değişikliği ayrı commit'te, mümkünse ayrı PR'da gelir.

Küçük Bir Deneme

★☆☆ Kendi projende rastgele seçtiğin üç dosyada private field'ların nasıl adlandırıldığını say: kaç farklı biçim var (_items, items, m_items)? Bu sayı, ekibinin yazılı olmayan bir convention'ı olup olmadığını söyler.

★★☆ Bir PR'da git diff --stat ile git diff -w --stat çıktılarını karşılaştır. Aradaki fark girinti değişikliği; -w sonrası kalan ama davranış değiştirmeyen satırları da say. Bu sayılar formatter ve analyzer'ın sana kaç satırlık review dikkati kazandıracağını gösterir.

★★★ Ekibin için bir sayfalık convention dokümanı taslağı yaz: temel kaynak, en tartışmalı üç karar ve gerekçeleri, her kuralın yanında onu uygulayacak araç. Aracı olmayan satırları işaretle; bunlar ya araca bağlanmalı ya da dokümandan çıkmalı.

Kuralları seçtik ve kayda geçirdik. Bir sonraki yazıda C# kararlarını .editorconfig'e dökeceğiz: dosya hiyerarşisi, code-style option'ları, naming rule'ların symbols, style ve severity üçlüsü ve bu kuralların IDE'de kalmayıp build'de nasıl zorlanacağı.