Sürdürülebilir Yazılım 101: Esnek Bağın Önemi (Loose Coupling)

Bu yazı ilk olarak Medium'da (Appcent yayını) yayımlanmıştır.

Uzun bir aradan sonra tekrar merhaba 👋

Büyük yazılım geliştirmeleri tek başına olabilecek bir şey değildir, ekip işidir. Özellikle geniş kitlelere hitap eden uygulamalar/bileşenler geliştiriyorsanız ekibe ihtiyacınız vardır. Ekipler kendi sorumluluğunda geliştirmeler yapar; bu noktada ekibin yönetilmesi, yapılacak işler, uygulanacak süreçler ve metotlar ortaya çıkar. En önemli kriterlerden biri her zaman sürdürülebilir uygulama yaratmaktır — yeni geliştirmeleri tamamlayabilen, bakımı göz ardı edilmeyen, test edilebilir bir uygulama.

Her ekip planlı teslimat (canlı ortama taşımak) hedefindedir. Planlı teslimat bu işin sihirli kelimesi: hedef bellidir — X geliştirmesini Y tarihinde yayına almak istiyoruz.

Örnekler

Biraz daha derine gidelim, net örnekler verelim. Olabildiğince basit tutalım. Basit bir login uygulamamız olsun.

public class UserManagerLoginResponse
{
    public bool Status { get; set; }
    public string Message { get; set; }
    public UserManagerLoginResponse(bool status, string message)
    {
        Status = status;
        Message = message;
    }
}

public class UserManager
{
    public UserManagerLoginResponse Login(string customerNo, string password)
    {
        //İş kuralı kontrolü

        //Login başarılı
        var result = new UserManagerLoginResponse(true, "Giriş başarılı");
        return result;
    }
}

Sistemdeki kullanıcılar için yukarıdaki kodları içeren bir oturum uygulamamız olsun. Ardından müşterimiz bir user story (kullanıcı hikayesi) paylaşarak yeni bir geliştirme talebinde bulunsun:

Giriş bilgileri doğru olan kullanıcılara ikinci bir kontrol için SMS gönderilmeli. Sistemde kayıtlı telefon numarasına gönderilen şifre, bir sonraki ekranda kullanıcıdan istenmeli. Bu kontroller sonrası kullanıcının başarılı giriş yaptığı kabul edilir.

Çok basit bir talep değil mi? 😅 Büyük ihtimalle SMS servisini hızlıca uyarlayıp aşağıdakine benzer bir kod yazacaktınız.

public class SMSSenderService
{
    public bool Send(string number, string message)
    {
        //SMS Servisine gidilip atıldı
        var result = true;
        return result;
    }
}

public class UserManager
{
    public UserManagerLoginResponse Login(string customerNo, string password)
    {
        //İş kuralı kontrolü
        string phoneNumber = "+90XXXXXXXXXX";
        SMSSenderService smsService = new SMSSenderService();

        string pin = "1545"; //Cache atıldı.
        string smsMessage = $"Mobil Onay Kodunuz: {pin}";
        var smsServiceResult = smsService.Send(phoneNumber, smsMessage);

        //İlk giriş başarılı
        var result = new UserManagerLoginResponse(true, "Lütfen telefonunuza gelen şifreyi giriniz.");
        return result;
    }

    public UserManagerLoginResponse LoginSecondStep(string customerNo, string pin)
    {
        //Cache okunur, pin kontrol edilir.
        var result = new UserManagerLoginResponse(true, "Hoşgeldiniz");
        return result;
    }
}

Yalnız bu kodu yazarken ister istemez bir bağımlılık yarattınız. SMSSenderService nesnesi doğrudan UserManager tarafından bağımlı hale geldi. Ek olarak kodunuz test edilemez de oldu; çünkü Dependency Injection (bağımlılık enjeksiyonu) kullanmadınız ve doğrudan concrete (somut) nesne oluşturdunuz. Bir interface yazsaydık? Belki ileride lazım olurdu.

Anahtar Kelimeler: Interface ve Abstract · Anahtar Yöntem: Injection

Ekipler geliştirme sürelerince kaçınılmaz bağımlılıklar yaratır. Düşük maliyetli gibi görünse de bu bağımlılıklar uzun vadede büyük sorunlara yol açabilir. Test edilemeyen bir kod, ileride ciddi bakım/değişim maliyeti doğurur: bağımlı olduğunuz koda test yazamaz, onu debug etmeniz ya da mock'layabilmek için ekstra geliştirme yapmanız gerekir.

Nesneye erişeceksek bunu mümkünse Dependency Injection ile, yapıcı metotlar (constructor) üzerinden geçirmemiz yararımıza olur.

public class UserManager
{
    private SMSSenderService _smsSenderService { get; set; }

    public UserManager(SMSSenderService smsSenderService)
    {
        _smsSenderService = smsSenderService;
    }

    public UserManagerLoginResponse Login(string customerNo, string password)
    {
        string phoneNumber = "+90XXXXXXXXXX";
        string pin = "1545";
        string smsMessage = $"Mobil Onay Kodunuz: {pin}";
        var smsServiceResult = _smsSenderService.Send(phoneNumber, smsMessage);

        var result = new UserManagerLoginResponse(true, "Lütfen telefonunuza gelen şifreyi giriniz.");
        return result;
    }
}

Bu, servisi biraz esnetti ama hâlâ bağımlıyız; çünkü SMSSenderService hâlâ concrete bir nesne. Yarın A SMS servisi yerine B servisine geçtiğinizde düzenleme tam bir iş yükü olur. Bu yüzden önce interface kullanarak servisi arayüz hâline getirelim; ihtiyaç hâlinde servisleri kolayca entegre edelim ve kodu sürdürülebilir kılalım.

public interface ISMSSenderService
{
    bool Send(string number, string message);
}

public class ASMSSenderService : ISMSSenderService
{
    public bool Send(string number, string message)
    {
        //A SMS Servisine gidilip atıldı
        return true;
    }
}

public class BSMSSenderService : ISMSSenderService
{
    public bool Send(string number, string message)
    {
        //B SMS Servisine gidilip atıldı
        return true;
    }
}

public class Program
{
    public static void Main()
    {
        UserManager userManager = new UserManager(new BSMSSenderService());
    }
}

public class UserManager
{
    private ISMSSenderService _smsSenderService { get; set; }

    public UserManager(ISMSSenderService smsSenderService)
    {
        _smsSenderService = smsSenderService;
    }
}

Fark ettiniz mi, UserManager içinde artık istediğim SMS servisini kullanabilirim. Tek yapmam gereken yeni servisi ISMSSenderService'den implement etmek. Bu sayede bir mock nesneyle unit test bile yazabilirim 😁 İşte esnek bağ yapısını böyle oturttuk.

Bir talep daha yapalım ve örneği pekiştirelim:

Gönderilen SMS mesajlarına ek olarak, aynı telefon numarasına tanımlı aktif cihazlara mobil uygulama üzerinden bildirim ile aynı mesajın gönderilmesi gerekmektedir.

Aynı numaraya aynı mesaj… Bir dakika, bunu interface ile yapabilir miyiz? Yaparız.

public interface IMessageSenderService
{
    bool Send(string number, string message);
}

public class ASMSSenderService : IMessageSenderService
{
    public bool Send(string number, string message) => true; // A SMS
}

public class BSMSSenderService : IMessageSenderService
{
    public bool Send(string number, string message) => true; // B SMS
}

public class NotificationSenderService : IMessageSenderService
{
    public bool Send(string number, string message) => true; // Bildirim
}

public class Program
{
    public static void Main()
    {
        List<IMessageSenderService> messageSenders = new List<IMessageSenderService>()
        {
            new BSMSSenderService(),
            new NotificationSenderService()
        };
        UserManager userManager = new UserManager(messageSenders);
    }
}

public class UserManager
{
    private List<IMessageSenderService> _messageSenderService { get; set; }

    public UserManager(List<IMessageSenderService> messageSenderService)
    {
        _messageSenderService = messageSenderService;
    }

    public UserManagerLoginResponse Login(string customerNo, string password)
    {
        string phoneNumber = "+90XXXXXXXXXX";
        string pin = "1545";
        string smsMessage = $"Mobil Onay Kodunuz: {pin}";

        foreach (var sender in _messageSenderService)
        {
            var serviceResult = sender.Send(phoneNumber, smsMessage);
        }

        return new UserManagerLoginResponse(true, "Lütfen telefonunuza gelen şifreyi giriniz.");
    }
}

Çıkaracağımız Dersler

  • En basit bağımlı görünen kod, uzun vadede büyük efor getirebilir.
  • Interface ve Abstract kavramları ve önemi.
  • Dependency Injection yöntemi ve önemi.
  • Esnek bağ (loose coupling) yaratmanın sağladığı faydalar.

Kalın sağlıcakla.