Map Bloklarında Mantıksal İşlem Yapmayı Bırakalım

Joël Quenneville
Çeviren Laçin Bilgin
Bu makaleyi şu dillerde de okuyabilirsiniz: English

Rubyciler olarak sık sık map blokları yazıyoruz ancak bu bloklar aslında başka yerde olması gereken işlemleri içermeye meyilli oluyor. İlk bakışta masum görünen aşağıdaki kod parçasını inceleyelim.

class ShoppingCart
  # other methods

  # SMELLY
  def sub_totals
    items.map do |item|
      item.base_cost + item.bonus_cost
    end
  end
end

Biraz daha dikkatli baktığımızda birkaç farklı code smell olduğunu fark edebiliriz.

  1. Bloğumuz Feature Envy dediğimiz farklı bir nesnenin özelliklerini barındırmaya çalışma davranışından muzdarip çünkü Item üzerindeki propertylerle haşır neşir olarak Itemın sorumluluklarını kendi üzerine almaya çalışıyor.
  2. Tam tersine Item ise Anemic Model dediğimiz içinde veri bulunduran ama bu veriyle herhangi bir işlem yapmayan bir model olarak karşımıza çıkıyor.

Mantıksal İşlemleri Gezdiğimiz Objelere Taşıyalım

Map bloğuna gezdiğimiz her nesneye uygulanmasını istediğimiz kodu yazıyoruz. Peki bunun yerine Item modeline bu işlemleri yaptırsak nasıl olur? Böylece Item daha fazla domainiyle alakalı operasyon gerçekleştirmiş olacak ve Itemla alakalı mantıksal işlemler yalnızca bir yerde toplanacak.

class Item
  # other methods

  def total_cost
    base_cost + bonus_cost
  end
end

Şimdi bu domain operasyonuna yeni bir ad verdik ve kodumuz artık single source of truth prensibine uyuyor. Bu oluşturduğumuz yeni metodu çağırdığımız yer daha high-level hale geldi ve Itemın iç işlerine burnunu sokmayı kesti. Aynı zamanda bu şekilde symbol to proc syntaxini de kullanabiliyoruz.

def sub_totals
  items.map(&:total_cost)
end

Karmaşık Bloklar

Peki ya bloğumuz daha karmaşık işlemler yapıyorsa ve başka nesnelerle de etkileşime geçiyorsa ne yapacağız? Bu durumda da yaptığımız işlemlerin çoğunun yeri muhtemelen yine Item nesnesi. Ne de olsa map bloğunun amacı belli bir işlemi gezdiğimiz listenin her elemanına yaptırmak. Eğer bir nesne üzerinde mantıksal işlem yapıyorsak, bu aslında nesnenin kendisi yaptırdığımız işlemin davranışına sahip olmalı demek oluyor.

# SMELLY
def sub_totals
  items.map |item|
    if coupon.applies_to_id == item.id
      (item.base_cost + item.bonus_cost) * coupon.percent_off
    else
      item.base_cost + item.bonus_cost
    end
  end
end

Yukarıda yaptığımız işlemler hep Item üzerinden yürüyor. item değişkeninin kaç kere kullanıldığına bir bakın! Ama bu sefer bloğun içerisinde coupon diye yeni bir değişken daha kullanılıyor. Bu karşılaşabileceğimiz bir durum. Item kendi toplam ücretini kupon uygulayarak da olsa hesaplayabilmeli. Bu işlemi Item üzerindeki bir metoda coupon argümanı vererek yapabiliriz.

sub_totals = items.map { |item| item.total_cost(coupon: coupon) }

Mantıksal İşlemleri Private Metoda Çekmekten Kaçınalım

Bu tarz karmaşık bir blokla karşı karşıya geldiğimizde ilk aklımıza gelen kodun mantığının bir kısmını ShoppingCart içindeki bir private metoda çekmek olabilir. Bu kodu daha okunaklı kılsa da bulduğumuz çözüm yine bahsettiğimiz code smellerden muzdarip. Onun yerine mantığı gezdiğimiz nesnelerin içine çekelim.

class ShoppingCart
  # other methods

  # STILL SMELLY
  def sub_totals
    items.map { |item| sub_total_for(item) }
  end

  private

  def sub_total_for(item)
    if coupon.applies_to_id == item.id
      (item.base_cost + item.bonus_cost) * coupon.percent_off
    else
      item.base_cost + item.bonus_cost
    end
  end
end