Software design principles: Single level of abstraction

Jun 28, 2023//-5 min

Single Level of Abstraction (SLA), fonksiyondaki ifadelerin aynı soyutlama düzeyinde (abstraction level) olması gerektiğini belirten ve fonksiyonların okunabilirliğini arttırmayı amaçlayan yazılım tasarım prensibidir. Aynı zamanda Single Responsibility (SR) prensibine uygun hale gelmesini de sağlar.

Konuyla ilgili diğer makalelerde, soyutlama düzeyinin ne olduğunu açıklamadan (adeta sahneleri kesilmiş korsan film edasıyla) örneklere atlıyorlar. Neyse ki masadayız ve en önemli noktaya değinmeden geçmeyeceğiz.

Soyutlama Düzeyi Nedir?

Her fonksiyon bir eylem gerçekleştirir. Eylemler de çoğu zaman alt eylemlerin bütünüdür. İşte bu alt eylemlerin düzeyi, soyutlama düzeyini belirtir. Teorik teorik konuşup anlamını bulandırmaya gerek yoktur.

Kahve makinesini örneği üzerinden ele alalım.

single-level-of-abstraction-001
single-level-of-abstraction-001

Bir makineden kahve almak istersek parayı atar, istediğimiz çeşidi seçer, hazırlanmasını bekler ve alırız. Bu adımlar, birinci soyutlama düzeyindedirler çünkü otomattan kahve almak için gereken temel adımlardır.

Kullanıcının etkileşime girdiği bu eylemler, arkaplanda başka eylemleri harekete geçirir. Kahve hazırlama aşamasında makine; bardağı alır, kahveyi koyar, sıcak su ekler ve karıştırır. Bu adımlar ise ikinci soyutlama düzeyindedir (parayı alma eyleminin alt eylemleri de aynı şekildedir). Kahve almak isteyen kullanıcının odaklanmaması gereken eylemlerdir. Makineye bardağı sizin koymanız ya da kahveyi sizin karıştırmanız gereksiz bir detay olurdu. Ancak bölmeye bardak konulmuyorsa o zaman ilgili fonksiyonelliği kontrol etmeniz gerekir.

Prensibini ihlal eden talimat dizisini dummy koda dökelim.

1getCoffee() {
2  checkMoney();       // abstraction lvl 2
3  addToBallance();    // abstraction lvl 2
4  chooseCoffeeType(); // abstraction lvl 1
5  getGlass();         // abstraction lvl 2
6  putCoffee();        // abstraction lvl 2
7  putWater();         // abstraction lvl 2
8  shuffleGlass();     // abstraction lvl 2
9  giveCoffee();       // abstraction lvl 1
10}

Parayı bakiyeyi kendimiz eklemiyoruz ya da paranın sahtelik kontrolünü kendimiz yapmıyoruz değil mi? Yani bu iş kullanıcıdan soyutlanmıştır. Ekstra soyutlaştırma ekleyecek şekilde kodu refactor edelim.

1getCoffee() {
2  getMoney();         // abstraction lvl 1
3  chooseCoffeeType(); // abstraction lvl 1
4  prepareCoffee();    // abstraction lvl 1
5  giveCoffee();       // abstraction lvl 1
6}

Artık prensibe uygun hale getirmiş olduk.

Kod Örnekleri

calculateSum()
SLA prensibine uygundur, yalnızca toplamın hesaplanmasıyla ilgili ayrıntıları içerir. Hesaplama sonucunu tutan bir değişken tanımlar, diğer sayıları buna ekler ve döndürür.

1public int calculateSum(int[] numbers) {
2  int sum = 0;
3
4  for (int num : numbers) {
5   sum += num;
6  }
7
8  return sum;
9}

validateUser()
fonksiyonu ise farklı soyutlama düzeyleri içerir.
isValidEmail()
ikinci soyutlama düzeyindedir. Fonksiyonun asıl amacı kullanıcının geçerli bir kullanıcı olduğunu kontrol etmektir. Ancak koda baktığımızda, doğrulama ve loglama işlemlerinin detaylarını da görürüz. Bu da birden fazla soyutlama düzeyinin varlığına işaret eder. Refactor sonrasında okunabilirlikleri karşılaştırabilmeniz adına
validateUser()
fonksiyonunu inceleyin.

1public boolean validateUser(User user) {
2  if (user.getName().isEmpty()) {
3    System.out.println("Name cannot be empty.");
4    return false;
5  }
6
7  if (user.getEmail().isEmpty()) {
8    System.out.println("Email cannot be empty.");
9    return false;
10  }
11
12  if (!isValidEmail(user.getEmail())) {
13    System.out.println("Invalid email format.");
14    return false;
15  }
16
17  if (user.getPassword().isEmpty()) {
18    System.out.println("Password cannot be empty.");
19    return false;
20  }
21
22  if (user.getPassword().length() < 8) {
23    System.out.println("Password must be at least 8 characters long.");
24    return false;
25  }
26
27  return true;
28}

validateUser()
'ın refactor edilmiş hali aşağıdaki gibidir.

1public boolean validateUser(User user) {
2  if (!hasValidName(user)) { // isim doğrulama detayları soyutlandı
3    logError("Invalid name"); // loglama detayları soyutlandı
4    return false;
5  }
6
7  if (!hasValidEmail(user)) { // email doğrulama detayları soyutlandı
8    logError("Invalid email"); // loglama detayları soyutlandı
9    return false;
10  }
11
12  if (!hasValidPassword(user)) { // şifre doğrulama detayları soyutlandı
13    logError("Invalid password"); // loglama detayları soyutlandı
14    return false;
15  }
16
17  return true
18}

Yeni fonksiyonlarla birlikte kod miktarı artsa bile rağmen ne kadar kolay okunabildiğine ve bakımının ne kadar kolay olacağına dikkat edin. Örneğin doğrulama aşamasında yaş doğrulamasının olup olmadığını kontrol ettiğinizi düşünün:

  • validateUser()
    'ı kontrol edersiniz.
  • Hangi doğrulama aşamalarını barındırdığını hızlıca görüp yaş doğrulamasının olmadığını farkedersiniz.
  • Gerekliyse yaş doğrulaması için doğrulama fonksiyonu oluşturup koda entegre edersiniz.

Bitti gitti. Soyutlama çok keskin olduğundan şifre ve email doğrulama detaylarını ayıklayarak hangi kod yaş doğruluyor diye yüzlerce satır incelemenize gerek kalmaz.

Uygulama

Verdiğimiz ürün listesinin toplam fiyatını hesaplayan bir fonksiyonunuzun olduğunu varsayalım ve bir dakikanızı ayırarak kodun SLA prensibini neden ihlal ettiğini analiz edin. Emeksiz Ekmek Olmaz (EEO) prensibi gereği kendiniz refactor etmeye çalışın.

1public int calculateTotalCartPrice(CartItem[] cartItems) {
2  int totalPrice = 0;
3
4  for (CartItem cartItem : cartItems) {
5    totalPrice += cartItem.getPrice();
6
7    if (cartItem.isTaxable()) {
8      totalPrice += cartItem.getPrice() * 0.18;
9    }
10
11    if (cartItem.getCategory().equals("Electronics")) {
12      if (cartItem.getPrice() > 500) {
13        totalPrice -= 50;
14      }
15    }
16  }
17
18  return totalPrice;
19}

Fonksiyonu incelediğimizde ürün fiyatını toplama tutara eklediğini, üzerine ürün vergisini eklediğini ve son olarak mevcutsa indirim miktarını düştüğünü görebiliriz. Kodu mantıksal olarak üç parçaya bölebiliriz. Adım adım refactor edelim:

  • calculateTotalCartPrice()
    'daki döngü içinde ürün fiyatını hesaplayan mantık kodun geri kalanından farklı soyutlama düzeyindedir. Ayrı bir fonksiyona çıkaralım.

    Bir fonksiyonda döngü varsa Single Level of Abstraction'a aykırı kod parçası içermesi muhtemeldir.

1public int calculateTotalCartPrice(CartItem[] cartItems) {
2  int totalPrice = 0;
3
4  for (CartItem cartItem : cartItems) {
5    totalPrice += calculateCartItemPrice(cartItem);
6  }
7
8  return totalPrice;
9}
10
11private int calculateCartItemPrice(CartItem cartItem) {
12  int cartItemPrice = cartItem.getPrice();       // abstraction level 1
13
14  if (cartItem.isTaxable()) {
15    cartItemPrice += cartItem.getPrice() * 0.18; // abstraction level 2
16  }
17
18  if (cartItem.getCategory().equals("Electronics")) {
19    if (cartItem.getPrice() > 500) {             // abstraction level 2
20      cartItemPrice -= 50;
21    }
22  }
23
24  return cartItemPrice;
25}
  • calculateTotalCartPrice()
    'ı başarıyla refactor ettik ancak yeni fonksiyonumuz da farklı soyutlama düzeylerine sahiptir. Bu fonksiyonun ana eylemi ürünün fiyatını döndürmektir ancak verginin ve indirimin hesaplanması alt eylemlerdir.

    Kodunuzun ne yaptığını seslendirirken eylem belirtiyorsanız orada fonksiyona çıkarmanız gereken kod parçası vardır. Yukarıdaki örneğe bakacak olursak:

    • Ürün fiyatını oku (bu eylem zaten bu fonksiyonun oluşturulma sebebidir)
    • Vergiyi hesapla (fonksiyona çıkarılması gerekir)
    • İndirimi hesapla (fonksiyona çıkarılması gerekir)
    • Nihai fiyatı dön (bu eylem zaten bu fonksiyonun oluşturulma sebebidir)
1public int calculateTotalCartPrice(CartItem[] cartItems) {
2  int totalPrice = 0;
3
4  for (CartItem cartItem : cartItems) {
5    totalPrice += calculateCartItemPrice(cartItem);
6  }
7
8  return totalPrice;
9}
10
11private int calculateCartItemPrice(CartItem cartItem) {
12  int cartItemPrice = cartItem.getPrice();
13
14  if (cartItem.isTaxable()) {
15    cartItemPrice += calculateTax(cartItem.getPrice());
16  }
17
18  if (isElectronicItem(cartItem)) {
19    cartItemPrice -= calculateDiscount(cartItem.getPrice());
20  }
21
22  return cartItemPrice;
23}
24
25private int isElectronicItem(CartItem cartItem) {
26  return cartItem.getCategory().equals("Electronics");
27}
28
29private int calculateTax(int price) {
30  return price * 0.18;
31}
32
33private int calculateDiscount(int price) {
34  return price > 500 ? 50 : 0;
35}

Refactor sonucunda pirüpak bir kod elde etmiş oluruz ve gelecek nesillerin duasını alırız.

Kodlarımızı hatayı minimize edecek ve ileride kolayca esnekleştirebilecek biçimde yazmak isteriz. Single Level of Abstraction gibi prensipler de detayları arındırtarak iyi kod yazmaya teşvik ediyor. Kullanın kullandırın.