• Cum. May 24th, 2024

Arif GÖKÇE

Senior Software Engineer

Clean Code, okunması, sürdürülmesi ve anlaşılması kolay olan kod demektir.

Clean Code yazma yeteneğini bir beceri olarak görüyorum.

Bu yetenek, bilinçli bir pratiği takip ederek öğrenilebilecek ve geliştirilebilecek bir yetenektir.

Clean Code yazma pratiği için en sevdiğim yaklaşım, (clean code principles) kod iyileştirme egzersizlerini yapmaktır.

Bu yüzden bugün sizin için bir tane hazırladım ve temiz kod prensiplerini uygulayarak adım adım iyileştireceğiz.

Hadi yapalım!

Yeni kavramları öğrenirken, genellikle bir sorunla başlamanın en etkili yol olduğuna inanıyorum.

Ve bu sorun, ne kadar açıklayıcı ve betimleyici ise, öğrenme deneyimimiz o kadar etkili olur.

İşte refaktoring için başlangıç noktamız olarak, kötü yazılmış bir kod kullanacağız.

Her adımda mevcut sorunu vurgulayacak ve nasıl çözeceğimizi açıklayacağım.

Aşağıda ki örnek koda göz atalım.

public void Process(Product? product)
{
    if (product!= null)
    {
        if (product.IsVerified)
        {
            if (product.Items.Count > 0)
            {
                if (product.Items.Count > 10)
                {
                    throw new Exception(
                        "The product" + product.Id + " has too many items");
                }

                if (product.Status != "ReadyToProcess")
                {
                    throw new Exception(
                        "The product" + product.Id + " isn't ready to process");
                }
                product.IsProcessed = true;
            }
        }
    }
}

Process metodunu incelediğimizde, şu sorunları tespit ediyorum:

  • Kodun iç içe geçmiş yapısı – tam 4 seviye derinlikte
  • “if” kontrollerinin sıralı bir şekilde uygulanması
  • Başarısızlığı temsil etmek için exfırlatılması

Şimdi, bu kodu temiz hale getirmek için neler yapabileceğimize birlikte göz atalım.

  1. Erken dönüş prensibi (Early Return Principle)

Şu aşamada, Process metodunun, önkoşul kontrollerini uygulayan if ifadeleri nedeniyle oldukça derin iç içe olduğu oldukça açık.

Bu sorunu çözmek için “early return principle” kullanacağız. Bu ilke, bir yöntemden hemen dönmemiz gerektiğini söyler, yani ilgili koşullar karşılandığında işlemin sonlandırılması gerektiğini ifade eder. Process methodu için bu, derin iç içe geçmiş yapının yerine bir dizi koruma koşulu kullanmamız gerektiği anlamına gelir.

public void Process(Product? product)
{
    if (productis null)
    {
        return;
    }

    if (!product.IsVerified)
    {
        return;
    }

    if (product.Items.Count == 0)
    {
        return;
    }

    if (product.Items.Count > 15)
    {
        throw new Exception(
            "The product" + product.Id + " has too many items");
    }

    if (product.Status != "ReadyToProcess")
    {
        throw new Exception(
            "The product" + product.Id + " isn't ready to process");
    }

    product.IsProcessed = true;
}

2. İf tanımlarında okunabilirlik

Early return principle, Process yöntemini daha okunabilir hale getirir.

Birden fazla if bloğu bulunduğunda, hepsini ayrı ayrı if blokları yerine tek bir if koşulu içinde yazmalıyız.

Bu nedenle hepsini bir if deyiminde birleştirebiliriz.

Process yönteminin davranışı değişmez, ancak fazla kodu kaldırırız.

public void Process(Product? product)
{
    if (productis null ||
        !product.IsVerified ||
        product.Items.Count == 0)
    {
        return;
    }

    if (product.Items.Count > 15)
    {
        throw new Exception(
            "The product" + product.Id + " has too many items");
    }

    if (product.Status != "ReadyToProcess")
    {
        throw new Exception(
            "The product" + product.Id + " isn't ready to process");
    }

    product.IsProcessed = true;
}

3. Daha Az Kod Yaz

Kısa bir iyileştirme, kodu daha öz ve anlaşılır yapmak için LINQ kullanılabilir.

Items.Count == 0 kontrolü yerine, LINQ Any yöntemini kullanmayı tercih ederim.

Bazıları LINQ’in performansının daha kötü olduğunu iddia edebilir, ancak ben her zaman okunabilirliği optimize ederim.

public void Process(Product? product)
{
    if (productis null ||
        !product.IsVerified ||
        !product.Items.Any())
    {
        return;
    }

    if (product.Items.Count > 15)
    {
        throw new Exception(
            "The product" + product.Id + " has too many items");
    }

    if (product.Status != "ReadyToProcess")
    {
        throw new Exception(
            "The product" + product.Id + " isn't ready to process");
    }

    product.IsProcessed = true;
}

4. Kod karmaşıklığını azaltıp okunabilir methodlara yönlenmek

Çeşitli koşulları tek bir if deyiminde birleştirmek, daha az kod yazmanın işareti olabilir, ancak karmaşık if blokları okunabilirliği azaltabilir.

Ancak, bu sorunu çözebilir ve okunabilirliği artırabilirsiniz, açıklayıcı bir isme sahip bir değişken veya metod kullanarak.

Ben genellikle metodları tercih ederim, bu yüzden if kontrolü için IsProcessable adını taşıyan bir metod oluşturacağım.

public void Process(Product? product)
{
    if (!IsProcessable(product))
    {
        return;
    }

    if (product.Items.Count > 15)
    {
        throw new Exception(
            "The product" + product.Id + " has too many items");
    }

    if (product.Status != "ReadyToProcess")
    {
        throw new Exception(
            "The product" + product.Id + " isn't ready to process");
    }

    productis .IsProcessed = true;
}

static bool IsProcessable(Product? product)
{
    return product is not null &&
           product.IsVerified &&
           product.Items.Any();
}

5. Özel anlaşılabilir hata mesajları oluşturun

Şimdi hata fırlatmaktan bahsedelim. Ben hataları yalnızca “olağanüstü” durumlar için kullanmayı tercih ederim ve kodumda akış kontrolü için kullanmam.

Bununla birlikte, eğer hataları akış kontrolü için kullanmak istiyorsanız, özel hata mesajları kullanmak daha iyidir.

Hata fırlatılma nedenini daha iyi açıklayarak customize throw bilgisi ekleyebilirsiniz

Ve bu istisnaları genel olarak ele almak isterseniz, belirli istisnaları yakalayabilmek için bir temel sınıf oluşturabilirsiniz.

public void Process(Product? product)
{
    if (!IsProcessable(productis ))
    {
        return;
    }

    if (productis .Items.Count > 15)
    {
        throw new TooManyLineItemsException(product.Id);
    }

    if (product.Status != "ReadyToProcess")
    {
        throw new NotReadyForProcessingException(product.Id);
    }

    product.IsProcessed = true;
}

static bool IsProcessable(Product? product)
{
    return product is not null &&
           product.IsVerified &&
           product.Items.Any();
}

6. Sabit sayılar için const kullanın

Sabit sayıların kullanılması, sıkça karşılaşılan bir durumdur. Bu, genellikle sayısal koşulların kontrolünde kullanıldıkları için hızlıca fark edilir. Sabit sayıların temel sorunu, anlam taşımadan kullanılmalarıdır. Bu durum, kodun anlaşılmasını zorlaştırır ve hataların artmasına yol açabilir. Sabit sayıları düzeltmek oldukça basittir, çözüm const kullanmaktır.

const int MaxNumberOfLineItems = 15;

public void Process(Product? product)
{
    if (!IsProcessable(product))
    {
        return;
    }

    if (product.Items.Count > MaxNumberOfLineItems)
    {
        throw new TooManyLineItemsException(product.Id);
    }

    if (product.Status != "ReadyToProcess")
    {
        throw new NotReadyForProcessingException(product.Id);
    }

    product.IsProcessed = true;
}

static bool IsProcessable(Product? product)
{
    return product is not null &&
           product.IsVerified &&
           product.Items.Any();
}

7. Sabit değerlerde enum kullanın

Clean Code yazımızın sonuna doğru yaklaşırken, sıkça rastlanan bir kod yazım şekli, sabit metinler veya sayıları doğrudan kod içinde kullanmaktır

Bu sabit değerler için tipik bir kullanım durumudur ve genelde statu,kategori vb. durumları temsil etmektir.

Görüldüğü gibi, Product.Status değerini hazır olup olmadığını kontrol etmek için bir string ifadesi ile karşılaştırıyoruz. Bu durum, sıkça yazım hatalarına neden olur ve kodun refactoring edilmesini zorlaştırır.

gelin beraber bir ProductStatus enum oluşturalım:

enum ProductStatus
{
    Pending = 0,
    ReadyToProcess = 1,
    Processed = 2
}

Hadi bunu örnek kodumuzda kullanalım.

const int MaxNumberOfLineItems = 15;

public void Process(Product? product)
{
    if (!IsProcessable(product))
    {
        return;
    }

    if (product.Items.Count > MaxNumberOfLineItems)
    {
        throw new TooManyLineItemsException(product.Id);
    }

    if (product.Status != productStatus.ReadyToProcess)
    {
        throw new NotReadyForProcessingException(product.Id);
    }

    product.IsProcessed = true;
    product.Status = productStatus.Processed;
}

static bool IsProcessable(Product? product)
{
    return product is not null &&
           product.IsVerified &&
           product.Items.Any();
}

8. Result sınıfları kullanın

Clean Code yazımızın sonuna geldik

Dediğim gibi, akış kontrolü için hata fırlatmayı tercih etmiyorum. Peki, bunu nasıl düzeltebiliriz?

Bir çözüm, result nesnesi desenini kullanmaktır.

Genel bir Result class veya ProcessProductResult gibi belirli bir classı temsil etmek için kullanabilirsiniz.

Result classınızı encapsulated hale getirmek için, somut result type oluşturmak için bir factory kullanabilirsiniz.

public class ProcessproductResult
{
    private ProcessproductResult(
        ProcessProductResultType type,
        long productId,
        string message)
    {
        Type = type;
        productId = productId;
        Message = message;
    }

    public ProcessProductResultType Type { get; }

    public long productId { get; }

    public string? Message { get; }

    public static ProcessProductResult NotProcessable() =>
      new(ProcessProductResultType.NotProcessable, default, "Not processable");

    public static ProcessProductResult TooManyLineItems(long oderId) =>
      new(ProcessProductResultType.TooManyLineItems, productId, "Too many items");

    public static ProcessProductResult NotReadyForProcessing(long oderId) =>
      new(ProcessProductResultType.NotReadyForProcessing, oderId, "Not ready");

    public static ProcessProductResult Success(long oderId) =>
      new(ProcessProductResultType.Success, oderId, "Success");
}

ProcessProductResultType gibi bir enum kullanmak, result classınızı switch ifadeleri ile daha kolay kullanmasını sağlar. İşte ProcessProductResult.Type için kullanılan enum

public enum ProcessProductResultType
{
    NotProcessable = 0,
    TooManyLineItems = 1,
    NotReadyForProcessing = 2,
    Success = 3
}

Şimdi tekrar kodumuza dönelim.

const int MaxNumberOfLineItems = 15;

public ProcessProductResult Process(Product? product)
{
    if (!IsProcessable(product))
    {
        return ProcessProductResult.NotProcessable();
    }

    if (product.Items.Count > MaxNumberOfLineItems)
    {
        return ProcessProductResult.TooManyLineItems(product);
    }

    if (product.Status != productStatus.ReadyToProcess)
    {
        return ProcessProductResult.NotReadyForProcessing(product);
    }

    product.IsProcessed = true;
    product.Status = productStatus.Processed;

    return ProcessProductResult.Success(product);
}

static bool IsProcessable(Product? product)
{
    return product is not null &&
           product.IsVerified &&
           product.Items.Any();
}

ProcessProductResult.Type için bir enum kullanmanın, bir switch ifadesi yazmanıza olanak tanıdığına dair örnek:

var result = Process(product);

result.Type switch
{
    ProcessProductResultType.TooManyLineItems =>
        Console.WriteLine($"Too many line items: {result.productId}"),

    ProcessProductResultType.NotReadyForProcessing =>
        Console.WriteLine($"Not ready for processing {result.productId}"),

    ProcessProductResultType.Success =>
        Console.WriteLine($"Processed successfully {result.productId}"),

    _ => Console.WriteLine("Failed to process: {productId}", result.productId),
};

İşte Cleane code yazmak için 8 ipucu:

  1. Erken dönüş prensibi
  2. If tanımlarında okunabilirlik.
  3. Daha az kod yaz
  4. Kod karmaşıklığını azaltıp okunabilir methodlara yönlenmek
  5. Özel anlaşılabilir hata mesajları oluşturun
  6. Sabit sayılar için const kullanın
  7. Sabit değerlerde enum kullanın
  8. Result classlar kullanmak

Cleane code yazmak, bilinçli bir pratiği ve deneyimi gerektiren bir konudur.

Cleane code yazma prensipleri, birçok yazılım geliştiricinin okuma listenin üst sıralarında yer alır. Ancak, bu prensipleri öğrenmek ve anlamak bir şeydir, onları günlük kodlama pratiklerinize entegre etmek ise bambaşka bir mesele.

İşte burada fark yaratabilirsiniz. Cleane code ilkelerine sadık kalarak, kod tabanınızı daha okunaklı ve sürdürülebilir hale getirebilir, hata ayıklamayı kolaylaştırabilir ve geliştirme sürecinizi daha verimli kılabilirsiniz.

Cleane code yolculuğunuzda size rehberlik etmek için buradayız. İyi kodlamalar!