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.
- 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 olarakItem
ın sorumluluklarını kendi üzerine almaya çalışıyor. - 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 Item
la 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