Kodlama yaparken en iyi yöntemin ne olduğu konusunda sürekli bir muhabbet, bir tarışma konusu oluşur. Herkesin bilgi birikimine, yaşamış olduğu tecrübelere göre en iyi yöntem, her zaman kesin olarak belli olmayacağı gibi benimde yaşadığım tecrübe ve bilgi birikimime göre C# kodlaması sırasında uyulması gereken pratikleri tek bir makale altında paylaşmaya çalıştım ve güncel olarak paylaşmaya çalışacağım.
C# Best Practices
1) Bir method sadece bir sorumluluğu yerine getirmelidir
Bu kuralı anlatacak en iyi görsellerden biri herhalde şu olacaktır.
Bu maddede aslında dikkat etmemiz gereken asıl nokta, method özünde Single Responsibility prensibine özen göstermektir. Bu sayede bize kazandıracağı bazı avantajlar ise:
- Method’un complexity seviyesini azaltmak
- Hataları azaltıp, tekrar kullanılabilirliği sağlamak
- Okunabilirliği ve genişletilebilirliği sağlamak
- Daha tutarlı testler yazılabilir hale getirmek
gibi maddeler olacaktır. Şimdi bir örnek üzerinden giderek mevcut olanı ve olması gerekene bir bakalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public bool ChangePassword(int userId, string oldPassword, string newPassword) { using (TestContext context = new TestContext()) { var user = context.User.FirstOrDefault(u => u.Id == userId); if (user != null && user.Password == oldPassword && newPassword.Length >= 6) { user.Password = newPassword; user.LastUpdatedDate = DateTime.Now; context.SaveChanges(); SmtpClient client = new SmtpClient { Host = "smtp.gmail.com", Port = 587, EnableSsl = true, Credentials = new System.Net.NetworkCredential("id", "password") }; MailMessage mailMessage = new MailMessage("blabla@blabla.com", "blabla2@blabla.com", "Your password changed!", "bla bla bla..."); client.Send(mailMessage); return true; } return false; } } |
Tamamen örnek amaçlı olan şifre değiştirmeye yarayan bir method düşünelim. İçerisine baktığımızda ise context üzerinden user’ı çekip, ilgili kontrollerden geçirdikten sonra şifre değiştirme işlemini gerçekleştiriyor ve hemen ardından şifre değişikliği üzerine bir mail gönderiyor. Gördüğümüz gibi buradaki method complexity’si artmış ve en önemlisi mail gönderim kısmının tekrar kullanılabilirliği kısıtlanmış durumdadır.
Optimal bir şekilde refactor etmek gerekirse:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public ActionResult UserSetting() { if (ChangePassword(0, "oldPassword", "newPassword")) { SendMail(); } } public bool ChangePassword(int userId, string oldPassword, string newPassword) { var user = _userEngine.GetById(userId); // Buradaki kontroller eğer business gereği çoğalıcak ise, private bir sub method olarak da bölünebilirdi. if (user != null && user.Password == oldPassword && newPassword.Length >= 6) { user.Password = newPassword; user.LastUpdatedDate = DateTime.Now; context.SaveChanges(); return true; } return false; } public void SendEmail() { SmtpClient client = new SmtpClient { Host = "smtp.gmail.com", Port = 587, EnableSsl = true, Credentials = new System.Net.NetworkCredential("id", "password") }; MailMessage mailMessage = new MailMessage("blabla@blabla.com", "blabla2@blabla.com", "Your password changed!", "bla bla bla..."); client.Send(mailMessage); } |
Gördüğümüz gibi her method sadece ilgili sorumluluğunu yerine getirmektedir. SendEmail method’uda artık tekrar kullanılabilir bir hale gelmiştir.
2) Method’lar inline olarak çok fazla parametre almamalıdır
İsminden de anlaşılabildiği gibi bir method çok fazla parametre almamalıdır. Örneğin:
1 2 3 4 5 | public bool RegisterUser(string name, string surname, string email, string password, DateTime birthDate, string country, string city, string address) { } |
Bu tarz kullanımlarda farklı bir business kararı gereği değişmesi gereken veya ekstradan eklenmesi gereken bir parametre daha gerekebilir. İşte bu durumda bu method’u kullanan her yerde bu değişimleri yapmak zorunda kalabiliriz ve parametre sıralarının kayması gibi problemleri de göze almalıyız. Bunlara ek olarak da okunabilirliği azaltıyor. Çözümü ise bu parametreleri bir obje içerisinde encapsulate etmektir.
1 2 3 4 | public bool RegisterUser(RegisterUserParameters registerUserParameters) { } |
Eğer ideal parametre sayısını merak ediyorsanız “Clean Code: A Handbook of Agile Software Craftsmanship” kitabında geçen çok güzel bir dizeyi size göstermek isterim:
The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn’t be used anyway.
3) Satırlar çok fazla uzun olmamalıdır
Kod yazarken uzun uzaya giden satırlar, belli bir süre sonunda tek hamlede okunabilirliği yüksek ölçüde azaltmaktadır. Buda ilgili kodu anlamamızı veya bazı kontrol etmemiz gereken durumları gözden kaçırmamıza sebebiyet verebilir. Çözüm olarak ilgili kodun belirli parçalarını, sub method’lar halinde mantıklı bir şekilde bölmektir. Farklı kaynaklarda tavsiye edilen satır uzunlukları 200’dür.
4) Exception’lar es geçilmemelidir
Kodlamanın herhangi bir parçasında catch bloğu içerisinde es geçilen exception’lar, ilerleyen süreçlerde farklı hata ve problemlere sebebiyet verecektir. Aynı zamanda da iyi bir best practice değildir. Bu tarz durumlarda oluşan exception’ı loglamak, iyi bir yöntem olacaktır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public void Foo() { try { // TODO... } catch (System.Exception ex) { } } //Olması gereken public void Foo() { try { // TODO... } catch (System.Exception ex) { _logger.Log(ex); } } |
5) Switch-Case clause’ları çok fazla satır içermemelidir
Switch-Case clause’ları içerisindeki fazla satırlar, okunabilirliği ciddi ölçüde azaltmaktadır. Bu anlamda olabildiğince kısa statement’lar kullanılmaya çalışılmalıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | switch (variable) { case 0: DoSomething(); DoSomething2(); DoSomething3(); DoSomething4(); DoSomething5(); break; case 1: break; ... } // Olması gereken switch (variable) { case 0: Do(); break; case 1: break; ... } public void Do() { DoSomething(); DoSomething2(); DoSomething3(); DoSomething4(); DoSomething5(); } |
Burada method çağırımının haricinde satırlarca farklı kodlar da yer alabilirdi. Okunabilirlik için olabildiğince case clause’u içerisindeki statement’ları kısaltmaya çalıştık.
6) Boolean statement’leri ters kontrol edilmemelidir
Boolean kontrollerini beklenenin aksine kontrol etmek ve ters ifadeler ile kontrol etmek hem coding standartlarına uygun değildir hem de kompleks ve maliyetli bir iştir.
1 2 3 4 5 6 7 8 9 10 | if(!(variable == 2)) { ... } // Olması gereken if(variable != 2) { ... } |
7) TODO tag’ları kontrol edilmeli ve method’lara summary eklenmelidir
Eğer herhangi bir method boş ise ve daha sonra implemente edilecekse, TODO tag’ı eklenmelidir. Bu sayede TODO tag’larına göre filtrelendiğinde gözünüzden kaçan implemente edilmemiş kod parçacığı kalmayacaktır. Bunun yanı sıra method’lara eklemiş olduğunuz summary’ler sayesinde, hem siz hemde diğer ekip arkadaşlarınız kod’a henüz bakmadan summary ile kod hakkında bir fikir sahibi olabilirler.
1 2 3 4 5 6 7 8 9 10 11 12 13 | private void DoSomething() { } // Olması gereken /// <summary> /// Bu method blabla yapar /// </summary> private void DoSomething() { //TODO: blabla bittiğinde blabla'yı buraya implemente et... } |
8) Switch-Case statement’ındaki boş DEFAULT clause’u silinmelidir
Switch-Case kullanıldığında otomatik olarak gelen DEFAULT clause’u kullanılıyorsa, o halde olması gereken uygun action kullanılmalıdır. Eğer hiç bir şey kullanılmayıp boş olarak es geçiliyorsa, kullanmanın da bir anlamı olmayacağından dolayı coding standartlarına göre silinmelidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | switch (variable) { case 0: //... break; case 1: //... break; default: break; } // Olması gereken switch (variable) { case 0: //... break; case 1: //... break; default: throw new NotSupportedException(); } |
9) Enum tanımlarken null olarak sıfırıncı değer tanımlanmalıdır
Coding standartlarında tutarlılığı sağlayabilmek adına Enum’lar tanımlanırken, sıfırıncı değer None olarak tanımlanmalıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 | enum MemberType { Standard = 1, Gold = 2 } // Olması gereken enum MemberType { None = 0, Standrt = 1, Gold = 2 } |
10) Constructor aracılığı ile inject edilen field’lar, read-only olmalıdır
Constructor aracılığı ile inject ettiğimiz field’ları read-only olarak tanımlamazsak, farklı method’lar içerisinden de ilgili field’a değer atanmaya çalışılabilir, karışıklıklara sebebiyet verebilir. Bunların önüne geçebilmek için ise read-only olarak işaretlenmelidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class User { int _userId; public User(int userId) { _userId = userId; } } //Olması gereken class User { readonly int _userId; public User(int userId) { _userId = userId; } } |
11) Interface tanımlanırken başına “I” prefix’i getirilmelidir
Coding standartlarına göre interface’ler tanımlanırken başlarına “I” prefix’i eklenmelidir. Örneğin:
1 2 3 4 | interface IUserRepository { //... } |
12) String birleştirme işlemlerinde “+” öperatörü kullanımına dikkat edilmelidir
String birleştirme işlemlerinde sadece bir kaç string’i birleştiriyor isek çok fazla problem olmayacaktır. Fakat bu işlemi daha fazla string ile gerçekleştiriyor isek memory performansı açısından StringBuilder veya String.Concat gibi işlemler uygulanmalıdır.
1 2 3 4 5 6 7 8 9 10 | public void Foo() { string blabla = "aaa" + "bbb" + "ccc" + "ddd" + "eee" + "ggg"; } //Olması gereken public void Foo() { string blabla = String.Concat("aaa", "bbb", "ccc", "ddd", "eee", "ggg"); } |
13) System type name’ler yerine Predefined type name’ler kullanılmalıdır
Int16, Single, UInt64 gibi system type name’ler yerine, Predefined type name’ler kullanılmalıdır. Örneğin:
1 2 3 4 5 6 7 8 | String name; Int32 userId; Boolean isActive; //Olması gereken string name; int userId; bool isActive; |
Bu sayede .Net Framework tarafında daha tutarlı bir okuma gerçekleştirecektir.
14) External kaynaklara erişecek bir class varsa IDisposable pattern’ini implemente edilmelidir
IO işlemleri, web servisler, sql gibi external kaynaklara ihtiyaç duyduğunuz durumlar olduğunda, memory performansı için IDisposable pattern’ini implemente edin.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { //Managed objeler burada yönetilecek } //Unmanaged objeler ise burada yönetilecek //Büyük field'lar null'a çekilecek vb. } ~Base() { Dispose(false); } |
15) Kodlama yaparken kodlar içerisinde magic string/numbers kullanılmamalıdır
İlgili kod parçacıkları içerisinde kullanılan magic string/number’lar tekrar kullanılabilirliği engellemekle kalmayıp, ilgili değer değiştiğinde ise her nerede kullanıldıysa tek tek bulunup güncellenmesi gerekmektedir. Coding standartları doğrultusunda bu tarz değişkenler global ve const olarak tanımlanmalıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public void Foo(int interval) { int a = 3 * interval; int b = a / 5; } //Olması gereken public class Blabla { private const int SomeVariable = 3; private const int DifferentVariable = 5; public void Foo(int interval) { int a = SomeVariable * interval; int b = a / DifferentVariable; } } |
16) Unmanaged kaynaklar için using statement’ı kullanılmalıdır
Bildiğimiz gibi .Net Framework içerisindeki Garbage collector hiçbir referans tarafından gösterilmeyen managed nesneleri bellekten kaldırmaktadır. Ancak Garbage collector unmanaged kodlar üzerinde tam kontrole sahip değildir. Kullanılmıyor olsalar bile bellekten serbest bırakamaz. İşte bu noktada biz yazılımcılar tarafından Dispose edilmesi gerekmektedir. Bu tarz unmanaged kaynaklara erişen objeler genelde IDisposable arayüzünü implemente etmektedir. Bu nesneleri dispose edebilmenin en iyi yolu ise, using statement’ı içerisinde kullanmaktır.
1 2 3 4 5 6 7 | using(SqlConnection sqlConnection = new SqlConnection("ConnectionString")) { using(SqlCommand command = new SqlCommand("", sqlConnection)) { //... } } |
17) Catch bloğunda exception tekrardan fırlatılırken sadece throw kullanılmalıdır
Catch bloğu içerisinde ilgili exception’ı logladıktan sonra tekrardan geriye fırlatma ihtiyacı duyulursa, bunu sadece throw keyword’ü kullanılarak gerçekleştirilmelidir. Aksi durumda oluşan orjinal exception’ın stack trace’i kaybolacaktır.
1 2 3 4 5 6 7 8 9 10 11 12 | catch(Exception ex) { _logger.Log(ex); throw ex; } //Olması gereken catch(Exception ex) { _logger.Log(ex); throw; } |
18) İsimlendirme Kuralları ve Standartları
C#’da genellikle 2 farklı isimlendirme standartı kullanılır bu isimlendirme standartları ise şöyledir;
- Pascal Case
- Camel Case
Bu isimlendirme standartlarını kısaca özetlemek gerekirse PascalCase tüm kelimelerin baş harfleri büyük olacak şekilde yazıma sahip olan bir isimlendirme standartıdır. CamelCase ise ilk kelimenin baş harfi ve sonraki kelimelerin baş harfi büyük olacak şekilde yazıma sahip bir standarttır.
Örnek verecek olursak PascalCase için MuratOner yazımı doğrudur ama CamelCase için muratOner yazımı doğrudur.
Peki bu 2 isimlendirme standartını C#’da nerede kullanacağız?
Sınıf, Metod ve Property isimlerinde PascalCase kullanılır.
1 2 3 4 5 6 7 8 9 | public class PascalCase { public string Name { get; set; } void MethodName(string name) { // ...... } } |
Gördüğünüz üzere PascalCase adındaki sınıfımız, MethodName adındaki metod adımız ve Name adındaki Property’imiz PascalCase standartında yazılmıştır.
Class kapsamında tanımlanan private field’lar, metod propertyleri ve metod kapsamında tanımlanan değişkenler CamelCase isimlendirme standart’ına uygun şekilde yazılmalıdır. Altta bu konu ile alakalı örneğe gözatabilirsiniz.
1 2 3 4 5 | int totalCount = 0; void MethodName(string name) { string fullMessage = "Hello " + name; } |
19) Namespace’leri Using Bloklarında Kullanın
Namespaceleri using bloğunda kullanmadığımız sürece namepspace’ler altındaki her nesneye erişimde uzun kod satırları oluşabiliyor örnek olarak UTF-8 encoding türünü kullanmak için System.Text namespace’i altında yer alan Encoding sınıfının statik bir üyesi olan UTF8 özelliği üzerinden ulaşabilecğiz ama namespace’i using bloğunda kullanmadığımızda şu şekilde bir kod çıkacak ortaya.
1 2 | System.Text.Encoding encoding = System.Text.Encoding.UTF8; // var encoding = System.Text.Encoding.UTF8; // <- Böylede kullanıyor olabilirsiniz. |
Ama kullanılması gereken asıl yöntem şu şekilde olmalıdır.
1 2 3 4 | using System.Text; Encoding encoding = Encoding.UTF8; // var encoding = Encoding.UTF8; // <- Veya böylede kullanabilirsiniz. |
20) Property veya field’e bir örneğinizi atamak istediğiniz new() kodunu kullanın
Daha önce property ve field tanımlarken bir sınıf yada veri kümesinden örnek almak istediğimizde alttaki gibi bir kullanımla ilerliyorduk.
1 2 3 4 | // property public Friend Friend {get; set;} = new Friend(); // field Friend friend = new Friend(); |
Örnek property ve field için örnek tanımı alttaki gibi kısa sözdizimi ile kullanabilirsiniz.
1 2 3 4 | // property public Friend Friend {get; set;} = new(); // field Friend friend = new(); |
Sizlerle önemli olarak ele aldığım ve alınan, code review’ler sırasında da dikkat ettiğim, farklı kaynaklardan da derlediğim best practice’leri paylaşmaya çalıştım. Umarım herkes için faydalı bir makale olur.
✍ Lütfen olumlu-olumsuz tüm görüşlerinizi bana yorum yada mail yolu ile iletmeyi ihmal etmeyin.
🔗 Sosyal medya kanallarından makaleyi paylaşarak destek olursanız çok sevinirim.
👋 Bir sonraki makalede görüşmek dileğiyle.