Kodlama Yaparken Dikkat Edilmesi Gerekenler Nelerdir ?
Kodlama Yaparken Dikkat Edilmesi Gerekenler
Sizlerde biliyorsunuz ki sürdürülebilir bir uygulama geliştirebilmek, hem biz yazılımcılar açısından (ileride gelecek olan yeni fonksiyonlar ve bakım işlemleri) hem de business kuralları gereği (müşteri memnuniyeti vb.) olmazsa olmaz bir ihtiyaçtır.
Bunun aksine giden bir yolda ise geliştirmiş yada geliştiriyor olduğumuz uygulama, bir süre hızla gelişen performans gereksinimlarının karşısında ise ayakta durması imkansız bir hale gelmeye mahkum olacaktır. Bu sebeple kodlama yaparken zaman içinde yarışmanın yanı sıra, performans gereksinimlarını da göz ardı etmemeliyiz.
Bir çok yazılımcı proje yöneticileri yada product owner’lar tarafından kendilerine verilen fonksiyonları geliştirmeye odaklanmaktadır. Kendilerini bu fonksiyonları geliştirmeye odaklamaları güzel fakat çoğu zaman aslolan dikkat edilmesi ihtiyaç duyulan fonksiyonel olmayan (non-functional requirements) özellikler es geçilmektedir. Üniversitelerin Yazılım Mühendisliği derslerinden hatırlarsanız, Functional ve Non-functional requirement’lar bulunmaktaydı.
Non-functional ihtiyaçlar da genellikle:
- Arayüzler
- Kullanıcı odaklılığı
- Güvenlik
- Kalite
- Performans
- Güvence
Gibi soyut niteliklerini belirleyen gereksinimlere dikkat edilmektedir. İşte tam bu noktada bizleri bir çok yazılımcıdan ayıracak olan nokta ise functional özellikleri geliştirirken, non-functional gereksinimlarıda göz ardı etmememiz olacaktır.
1) Bir method sadece bir sorumluluğu yerine getirmelidir
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(“[email protected]”, “[email protected]”, “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(“[email protected]”, “[email protected]”, “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;
}
|
Comment (1)
takip edilmesi gereken bir site!