Tek Sorumluluk Prensibi

19 Mart 2019

Tek Sorumluluk Prensibi (TSP), yazılımı daha anlaşılır, esnek ve sürdürülebilir hale getirmeyi amaçlayan beş yazılım tasarım ilkesinin kısaltması olan SOLID'in ilk prensibidir. Bu ilkeler, temiz yazılım tasarımıyla son derece ilgili Robert C. Martin tarafından ileri sürülmüştür.

Herhangi bir aptal bir bilgisayarın anlayabileceği kodları yazabilir. İyi programcılar, insanların anlayabileceği kodları yazar.

"Bir sınıfın değişmesi için bir ve sadece bir nedeni olmalıdır." tanımı kurucusu tarafından kendi web sitesinde yapılmıştır.

Bu ilkenin anlaşılması ve başarılması oldukça basit görünse de, çoğu zaman geliştiriciler tarafından yanlış anlaşılır ve yanlış kullanılır. Bu nedenle, TSP, SOLID ilkelerinden anlaşılması en basit, uygulaması en zor olanı olarak kabul edilir.

Karışıklık Senaryosu: Birçok Bağımlılığı Enjekte Etmek

Martin Seemann, makalesinde, TSP'ye uymak adına enjekte edilen bağımlılıkları, enjekte edilen sınıfları azaltmak için bir sarmalayıcı sınıfında gruplandırmanızı önerir. Bu, TSP'nin kendisi tarafından yanlış anlaşıldığını gösterir.

Enjekte edilen sınıflar listesi gerektiği kadar büyüyebilir, bunun için bir sınır olmamalıdır; bu bağımlılıkları kullanan sınıfın değişmesi için hala bir neden olduğu ve enjekte edilen sınıfların değişmesi için aynı tek nedeni paylaşmadığı sürece.

Bu senaryo her ne kadar TSP'yi ihlal etmese de, mevcut olması pek olası değildir. Çok büyük olasılıkla bu senaryo aşağıdaki 2 sebepten birinden dolayı hatalıdır:

  • Enjekte edilen sınıfların aşırı modülerleştirilmesi. Bu durum balığa çıkarken oltanın kendisi yerine olta tutma yeri, kanca bekçisi gibi olta parçalarını yanımıza almakla eş değerdir. Değişmek için aynı nedene sahip sınıflar ayrılmamalıdır.
  • Enjekte edilen sınıfın değişmesi için tek bir nedene sahip olmaması. Balığa çıkarken yanımıza bir olta, sandık kutusu, kapitone malzemeleri, bowling topu, mınçıka, alev atıcı vb. gibi eşyalar da götürmek bu senaryoya benzerdir. Bir sınıfın tüm bu enjeksiyonlara ihtiyacı varsa, o zaman muhtemelen değiştirmek için tek bir nedeni yoktur.

Basit Bir Problem

BankAccount sınıfı, müşterilerin hem kontrol hem de tasarruf hesaplarında kullanılır.

public abstract class BankAccount {
  void Deposit (double amount) {...}
  void Withdraw (double amount) {...}
  void AddInterest (double amount) {...}
  void Transfer (double amount, IBankAccount toAccount) {...}
}

Kontrol ve tasarruf hesapları sorumluluklarını ayrı ayrı değiştirildiğinde, bu sınıfın değişmesi gerekecektir. Bunun anlamı, bu sınıfın değişmesi için birden fazla nedeni var demektir.

public class BankAccount {
  void Deposit (double amount) {...}
  void Withdraw (double amount) {...}
  void Transfer (double amount, IBankAccount toAccount) {...}
}

public class CheckingAccount : BankAccount {...}

public class SavingsAccount : BankAccount {
  void AddInterest (double amount) {...}
}

İki ayrı sınıf oluşturularak sorun çözüldü. Artık sınıfların her birinin değişmesi için tek bir nedeni var.

Modülerlikle Karıştırılması

TSP genellikle modülerlikle karıştırılır; her ne kadar prensip modülerliği sağlayabilse de, TSP uygulamanın doğru yolu yazılımı en küçük parçalara bölmek anlamına gelmez. TSP'nin "Bir sınıfın değişmesi için bir sebep olmalı" yerine "Bir sınıfın var olmasının bir nedeni olmalı." şeklinde tanımlanmamasının bir sebebi vardır.

TSP'nin her bir amaç için sistemi sınıflara ayırdığı düşünülürse, kod sebepsiz yere en küçük parçalarına bölünür. Bu şu durumlara yol açar:

  • kapsüllemenin çökmesi; alanlar sınıflar tarafından ulaşılması için public olarak tanımlanır.
  • uyumun bozulması; aynı sınıf içinde olması gereken ilgili şeyler ayrılır.
  • hiçbir şey olmasa bile, sistem, sınıflarını tek metotlarla ve birkaç kod satırıyla doldurarak gereksiz karmaşıklık oluşturur.

Bir sınıf, çok sayıda kod satırıyla birden fazla sorumluluğa sahip olabilir, ancak tüm bu sorumluluklar aynı anda tek bir nedenden dolayı değişebilir olmalıdır.

Sınıflar ne zaman ayrılmalı?

TSP değişimin etkilerini sınırlamakla ilgilidir; sistem muhtemel değişim kaynaklarına göre modülerleştirilmelidir.

Bir yazılım tasarımcısı ne bir değişiklik meydana gelinceye kadar TSP uygulamayı beklemeli, ne aşırıya kaçmalı ne de bir oltaya ihtiyaç duyarken oltayı parçalarına bölmelidir. Bu nedenle, TSP'nin doğru uygulanması yazılım tasarımcısının iyi karar vermesine bağlıdır.

Bazen bilim, bilimden çok sanattır.

Sistemde bir değişiklik olduğunda, iyi yargı uygulanarak ve işlevsellikler belirli değişikliklere göre güzelce ayrılırsa, değişiklik yalnızca o belirli sınıf üzerinde etkili olur. Bu aynı zamanda etkilenen diğer sınıfların yeniden test edilmesi yükünü de ortadan kaldırır.

Araba Motoru Örneği

Örnek olarak bir araba motoru düşünün. Motorun tam olarak nasıl çalıştığını önemsiyor musunuz? Pistonların boyutunu önemsiyor musunuz? Eksantrik milinin saniyede kaç devir yaptığı ile ilgileniyor musunuz? Yoksa sadece araca bindiğinizde motorun istediğiniz gibi çalışmasını mı bekliyorsunuz?

Bu durum tamamen içeriğe bağlıdır. Bir motordaki problemleri çözebilecek bir tamirciyseniz, iç kısımları bilir ve ilgilenirsiniz. Motorun parçalarının değişmesi gerektiğinde, değişikliği parça bazında ele alabilirsiniz. Bu, gelecekte iç kısımlarda bazı değişiklikler olabileceği anlamına gelir.

Ancak, sadece ulaşım için araba kullanan sıradan bir şoförseniz, motorun iç kısımlarıyla ilgili her şeyi bilmeniz gerekmez. Bu, tek tek parçaların değişiklikleriyle başa çıkamayacağınız, ancak motoru tamamen değiştirmeniz gerektiği anlamına gelir. Bu, gelecekte iç kısımlardaki değişikliklerin olası olmadığı anlamına gelir.

Her iki durumda da içerik, sorumlulukların uygun şekilde ayrılmasının ne olduğunu belirlemiştir.

  • Mekanik senaryosu için ayrı sınıfların olması makul olacaktır; çünkü motorun iç parçaları ayrı ayrı değiştirilebilir.
  • Sıradan bir sürücünün yer aldığı ikinci senaryoda, tüm iç parçaların içinde gruplandırılmış bir motor sınıfına sahip olmanın yeterli olduğu açıktır; çünkü değişiklik sadece motor seviyesinde meydana gelebilir.

Bu nedenle, yazılım tasarımcısı, olası değişim kaynaklarına göre, sınıfların tek tek mi düzenlenebileceğine veya bir bütün olarak mı tutulması gerektiğine karar vermelidir.

Tek bir sınıf içinde birçok sorumluluk toplanırsa, ancak tüm bu sorumluluklar aynı nedenden dolayı değişebilirse, o zaman sınıfta yanlış bir şey yoktur. Bununla birlikte, sınıfın sorumlulukları farklı nedenlerle değişebilirse, sınıf belirli bölümlere ayrılmalıdır.

Aynı nedenlerle değişen şeyleri bir araya getirin. Farklı nedenlerle değişen şeyleri birbirinden ayırın.

Kod Örneği: WinForms Uygulaması

TSP'nin ihlal edildiği bazı kodlar yazmak ve görmek pek de zor değildir. Aşağıdaki örnekte olduğu gibi.

public partial class Form1 : Form {
  private void btnLoad_Click(object sender, EventArgs e)
  {
    listView1.Items.Clear();
    var sourceIndicator = txtsourceIndicator.Text;
    using (var fs = new FileStream(sourceIndicator, FileMode.Open))
    {
      var reader = XmlReader.Create(fs);
      while (reader.Read()) {
        if (reader.Name != "product") continue;
        var id = reader.GetAttribute("id");
        var name = reader.GetAttribute("name");
        var unitPrice = reader.GetAttribute("unitPrice");
        var discontinued = reader.GetAttribute("discontinued");
        var item = new string[]{id, name, unitPrice, discontinued};
        var lvItem = new ListViewItem(item);
        listView1.Items.Add(lvItem);
      }
    }
  }
}
Bir XML Dosyasını Ayrıştırıp Listview Öğelerini Gösteren Basit WinForms Uygulaması

Bu tür bir kodla karşılaşmak pek de olasılıksız olmasa da, kodun TSP'ye dayalı bazı ciddi sorunları vardır. Sınıfın bir amaca hizmet etmediği ve bu amaçların aynı sebeple değişmeyeceği açıktır.

Sınıfın 4 sorumluluğu vardır ve bu sorumlulukların tümü farklı zamanlarda farklı nedenlerle değişebilir:

  • 1) listview değişikliklerini kontrol etme.
  • listView1.Items.Clear();
    ...
    var item = new ListViewItem(new string[]{id, name, unitPrice, discontinued});
    listView1.Items.Add(item);
  • 2) Veri kaynağından veri yükleme.
  • using (var fs = new FileStream(sourceIndicator, FileMode.Open))
  • 3) Veri kaynağından veri okuma.
  • var reader = XmlReader.Create(fs);
    while (reader.Read()) {
  • 4) (örtülü) Model haritalama.
  • if (reader.Name != "product") continue;
    var id = reader.GetAttribute("id");
    var name = reader.GetAttribute("name");
    var unitPrice = reader.GetAttribute("unitPrice");
    var discontinued = reader.GetAttribute("discontinued");
    var item = new string[]{id, name, unitPrice, discontinued};

Bunların hepsi birbirinden bağımsız değişebilir. Gelecekte,

  • (1) liste görünümünün içeriği değişebilir; yeni sütunlar eklenebilir veya kaldırılabilir.
  • (2) veri kaynağı değişebilir; veri kaynağını bir veritabanından almanız gerekebilir.
  • (3) veri kaynağının türü değişebilir; örneğin xml yerine json kullanılarak.
  • (4) dosyanın içeriği değişebilir; yeni alanlar eklenebilir.

Bu nedenle, bu işlemi TSP standartlarında başarıyla gerçekleştirmek için 3 farklı sınıfa daha ihtiyacımız var. Sorumlulukları kendi sınıflarına bölmeye başlayalım.

public interface ISourceProvider
{
   Stream Load(string sourceIndicator);
}
public class SourceProvider : ISourceProvider
{
   public Stream Load(string sourceIndicator)
   {
       return new FileStream(sourceIndicator, FileMode.Open);
   }
}
Veri kaynağından gelecekteki olası yükleme değişikliklerini ele alan SourceProvider sınıfı
public interface ISourceReader
{
    ISourceReader Create(Stream stream);
    string Name { get; }
    string GetAttribute(string attribute);
    bool Read();
}
public class SourceReader : ISourceReader
{
    private XmlReader reader;

    public string Name { get { return reader.Name; } }

    public ISourceReader Create(Stream stream)
    {
        reader = XmlReader.Create(stream);
        return this;
    }

    public bool Read()
    {
        return reader.Read();
    }

    public string GetAttribute(string attribute)
    {
        return reader.GetAttribute(attribute);
    }
}
Veri kaynağı türünde gelecekteki olası değişikliklerin üstesinden gelmek üzere XmlReader için sarıcı sınıfı (Adaptör Deseni)
public interface IProductMapper
{
   Product Map(ISourceReader reader);
}
public class ProductMapper : IProductMapper
{
   public Product Map(ISourceReader reader)
   {
      if (reader == null)
         throw new ArgumentNullException("Source reader is null");

      if (reader.Name != "product")
         return null;

      var product = new Product();
      product.Id = int.Parse(reader.GetAttribute("id"));
      product.Name = reader.GetAttribute("name");
      product.UnitPrice = decimal.Parse(reader.GetAttribute("unitPrice"));
      product.Discontinued = bool.Parse(reader.GetAttribute("discontinued"));

      return product;
   }
}
Model haritalamanın gelecekteki olası değişikliklerini ele alan ProductMapper sınıfı
public class Form1 : Form {
   private readonly ISourceProvider sourceProvider;
   private readonly ISourceReader sourceReader;
   private readonly IProductMapper productMapper;

   public ProductRepository()
   {
      this.sourceProvider = new SourceProvider();
      this.sourceReader = new SourceReader();
      this.productMapper = new ProductMapper();
   }

   private void btnLoad_Click(object sender, EventArgs e)
   {
      listView1.Items.Clear();
      var sourceIndicator = txtsourceIndicator.Text;

      var products = GetProducts(sourceIndicator);

      foreach (Product product in products)
      {
         var item = new ListViewItem
         (
            new[]
            {
               product.Id.ToString(),
               product.Name,
               product.UnitPrice.ToString(),
               product.Discontinued.ToString()
             }
         );

         listView1.Items.Add(item);
      }
  }

  private IEnumerable GetProducts(string sourceIndicator)
  {
     var products = new List();
     using (Stream stream = sourceProvider.Load(sourceIndicator))
     {
        var reader = sourceReader.Create(stream);
        while (reader.Read())
        {
           var product = productMapper.Map(reader);
           if (product != null)
              products.Add(product);
        }
     }

     return products;
  }
}
Şimdi sınıfımızın tek bir sorumluluğu var ve bu da listview değişikliklerini kontrol etme.

Bu sadece tüm bu sorumlulukların gelecekte belirtilen şekilde değişebileceğini varsayan örnek bir uygulamadır. Projenizde tam olarak bu sınıfa sahip olabilirsiniz ve sınıftaki ayrı sorumlulukların aynı sebeple aynı anda değişeceğinden yüzde yüz emin iseniz, o zaman sınıfı düzenlemenize gerek olmayacaktır (hatta düzenlememeniz gerekmektedir).

TSP tamamen projenizin geleceği hakkında iyi bir değerlendirme yapmakla ilgilidir.

Örneğin, genişletilmiş, bütün ve işlerin ayrılmış sürümünü buradan inceleyebilirsiniz.

TSP'nin Yararları

Son olarak, makale boyunca bir kısmı gün ışığına çıkarılsa da, TSP'nin avantajlarını açıkça belirtmek istiyorum.

Birçok kişi tarafından iddia edildiği gibi, TSP modülerliği doğrudan sağlamaz; bu yüzden modülerliğin faydalarını TSP'nin yararları olarak sayamayız. TSP, modülerliği dolaylı olarak sağlayabilir; modüler hale getirildiği için kodu okunması daha kolay, anlaşılması daha kolay hale getirir; ancak bunların TSP'nin faydaları olduğunu söyleyemeyiz. Bunlar modülerliğin faydalarıdır.

TSP, aynı nedenlerle değişen şeyler arasındaki uyumu (cohesion) arttırır ve farklı nedenlerle değişen şeyler arasındaki bağı (coupling) azaltır.

  • Bir sınıftaki değişiklikler diğer modüllerde daha az değişiklik gerektirir. Değişikliğin sistem üzerindeki etkisi azalır.
  • Değişiklik söz konusu olduğunda karmaşıklık azalır. Bir değişiklik olacağında, aramanız gereken sınıf önceden belirlenmiştir.
  • Bir değişiklik yalnızca ilgili sınıfı etkileyeceğinden, diğer sınıfların tekrar test edilmesi gerekmez.