Yanlış soyutlamadansa tekrarı tercih edin (2016)
(sandimetz.com)- Yanlış soyutlamaya kıyasla kod tekrarının çok daha ucuz olduğu ve erken ortaklaştırmanın uzun vadeli bakım maliyetini artırdığı savunuluyor
- Başta makul görünen bir çıkarım bile gereksinimler zamanla biraz değişince parametreler ve koşullu ifadelerle dolup taşarak asıl niyeti bulanıklaştırır
- Ortak bir soyutlama birden fazla fikri taşımaya başladığında kod, koşul odaklı bir prosedüre dönüşür ve yeni özellikler eklendikçe daha kolay kırılır hale gelir
- Mevcut koda harcanan emeği koruma isteğiyle oluşan batık maliyet yanılgısına karşı dikkatli olunmalı; gerekirse soyutlama tekrar çağrı noktalarına inline edilerek yalnızca gerçekten gereken kod bırakılmalıdır
- Yanlış bir soyutlama ortaya çıktıysa, tekrarları yeniden devreye alıp mevcut gereksinimlerin ortak yönlerini yeniden gözlemledikten sonra tekrar çıkarım yapmak daha hızlıdır
Yanlış soyutlamanın oluşma süreci
- “duplication is far cheaper than the wrong abstraction” sözü RailsConf 2014 sunumunun bir parçasıydı, ancak sonrasında da sıkça anılmaya devam etti
- Yaygın başarısızlık yolu şöyledir
- Geliştirici A bir tekrar fark eder
- Bu tekrar bir metoda ya da sınıfa çıkarılır ve ad verilerek yeni bir soyutlama oluşturulur
- Çağrı noktalarındaki tekrar eden kod, yeni soyutlamanın çağrılarıyla değiştirilir
- Zaman geçtikçe neredeyse uyan ama tamamen aynı olmayan yeni bir gereksinim ortaya çıkar
- Geliştirici B mevcut soyutlamayı korumaya çalışarak parametre ekler ve değere göre farklı yollar izleyen koşullu ifadeler yerleştirir
- Sonrasında her yeni gereksinimle birlikte parametreler ve koşullu ifadeler artar, kodu anlamak giderek zorlaşır
- Bir kez yazılmış kod, korunması gereken bir yatırım gibi görünmeye çok yatkındır
- Zaten harcanmış emeği boşa gitmiş saymak istemeyen bir psikoloji devreye girer
- Kod ne kadar karmaşık ve anlaşılması güçse, o kadar önemli ve yazması uzun sürmüş gibi algılanır; bu da ondan vazgeçmeyi zorlaştırır
- Bu durum batık maliyet yanılgısıyla bağlantılıdır
Tekrara dönüp yeniden çıkarım yapmak
- Yeni gereksinimler yanlış bir soyutlama üzerinde uygulanmaya devam ederse, paylaşılan kod koşullu ifadeler merkezli hale gelir ve her yeni özellikle birlikte daha da kırılganlaşır
- Bu noktada hızlı yol daha fazla zorlamak değil, geri dönmektir
- Soyutlanmış kod her çağrı noktasına tekrar inline edilerek tekrarlar yeniden devreye alınır
- Her çağrı noktasında aktarılan parametrelere göre gerçekten çalışan kodun hangisi olduğu incelenir
- O çağrı noktasında gerekmeyen kod silinir
- Inline etme süreci hem soyutlamayı hem de koşullu ifadeleri ortadan kaldırır ve her çağrı noktasını yalnızca kendisinin ihtiyaç duyduğu koda indirger
- Aynı soyutlamayı çağırıyor gibi görünen kodlar bile gerçekte her çağrı noktasında oldukça kendine özgü kod yolları çalıştırıyor olabilir
- Ancak önceki soyutlama tamamen kaldırıldıktan sonra tekrarlar yeniden gözlemlenebilir ve mevcut gereksinimlere uygun yeni bir soyutlama çıkarılabilir
- Parametreler ve koşullu yollar paylaşılan koda sürekli ekleniyorsa, o soyutlama artık uygun olmayabilir
- Başlangıçta doğru soyutlama olmuş olabilir
- Gereksinimler değiştikçe artık aynı biçimde sürdürülmesi zorlaşmış olabilir
- Yanlış soyutlamada tekrarları yeniden devreye almak bir geri adım değil, daha iyi bir ilerleme olur
5 yorum
Bunun ikili bir yorum gerektiren bir konu olup olmadığından pek emin değilim.
Ah, buna gerçekten katılıyorum.
Düzenlenmemiş olanı düzenleyebilirsiniz ama
zaten düzenlenmiş olanı tersine çevirmenin maliyeti daha yüksek gibi görünüyor.
ponytail bunu paylaşmıştı, tam da böyle bir yazı haha
Her zaman karşı karşıya geliyor.
Hacker News görüşleri
Tek bir doğruluk kaynağı (single source of truth) ilkesine her zaman uyulması gerektiğini düşünüyorum
Birbirinden saparsa bug üreten yinelenmiş kod varsa refactor edilmelidir. Aksi halde gelecekteki geliştiricilerin bug patlayana kadar fark etmesinin zor olduğu uzak mesafeli bağlaşım ortaya çıkar
Ancak bu ilke ihlal edilmiyorsa soyutlama sadece bir kolaylıktır; rahatsız edici olmaya başladıysa görevini yapmıyordur ve kullanmak için bir neden yoktur. Bir fonksiyon özelleştirilmiş davranış için birden fazla flag gerektiriyorsa bu büyük olasılıkla hatalı bir soyutlamadır ya da tek sorumluluk ilkesinin ihlalidir
Gerçekten çok fazla özelleştirme gerekiyorsa, argüman olarak fonksiyon/functor alan bir yaklaşım çoğu zaman iyidir. Örneğin
solve(f:double -> double, max_iters = 99, x_abs_tol = 1e-15, x_rel_tol = 1e-15, ...)yerinesolve(f:double -> double, stopping_criteria: StoppingCriteriaClass)gibi yapılabilirKodun iki noktasının aynı algoritmayı mı kullandığı, yoksa biraz farklı sürümler mi olduğu ve daha da önemlisi aynı nedenle değişip değişmeyeceği belirsiz
Başlıktaki deyiş, farklı şeyleri zorla aynıymış gibi yapmanın, aynı şeyi kopyalayıp sonradan farklılaştırmaktan daha acı verici olduğunu söylüyor; ben de katılıyorum. İkincisinde aynı değişikliği iki kez yaparsınız ya da soyutlama getiren bir refactor yaparsınız, ama ilkinde soyutlamanın üstüne eklemeye devam etmeniz veya geri almanız gerekir
Özellikle yerelliği (locality) bozar; değişiklik yaparken gerçekten önemli olan tek özellik de budur. Sadece bu değişikliği yapmak ve sistemin alakasız kısımlarında yan etki olur mu diye endişelenmek istemiyorum
Bunun tipik örneği pyproject.toml / requirements.txt senkronizasyonudur ve gerçekten en iyi seçenek olduğu durumlar vardır; daha geniş alanlara da uygulanabilir gibi görünüyor. Ön kabul, zaten tek bir doğruluk kaynağının imkânsız olacağı kadar işlerin raydan çıkmış olmasıdır; tedaviden çok hasarı azaltmaya benzer
Belli bir anda iki kod parçası benzer göründüğü için aşırı soyutladığım, sonra da zamanla ayrıştıklarını gördüğüm durumları sık yaşadım
Özellikle junior geliştiriciler bazen tekrarın bütün kötülüklerin kaynağıymış gibi davranıyor
Bazen bu sorunu düşünüyorum. Yakın zamanda kişisel bir projede RTS birimleri için 2D sprite'larla uğraşırken karşıma çıktı; birim sprite'ları sprite sheet içine tutarlı bir şekilde yerleştirilmişti: 8 yönde 5 sprite, bunların 3 yönü mirror ediliyor ve sıra stand, move, attack, die şeklindeydi
Bu yüzden action + direction alıp oynatılacak sprite dizisini veren bir loader yaptım
Ama sonra yönü olmayan patlama sprite'ları, 4 yön ve yalnızca 2 mirror içeren ceset sprite'ları ve üstelik ilk dördü dışında orklar ile insanların büyük ölçüde aynı şeyleri paylaştığı durumlar çıktı
Tüm bunların ortak soyutlamasının ne olduğunu kısa süre düşündüm ama sonunda yükleme kodunun yalnızca bir kısmını ayırdım, sonra da UnitLoader, CorpseLoader ve EffectLoader oluşturdum. Üç loader da biraz benzer şeylerle uğraştığı için daha iyi bir soyutlama olabilir, ama varsa da sonra bulunur. Şimdi tüm durumları ele almaya çalışan karmaşık bir EverythingLoader yapmak yerine, ileride zamanı gelince tekrarı kaldırmak daha kolay
Programlamada genelleme yoluyla kodu basitleştirmeye yönelik bir içgüdü var, ama gerçek dünya dağınık olduğu için çoğu zaman aşırı basitleştiriyoruz. Metinde dendiği gibi, zaman geçip yeni gereksinimler çıktığında bunun fazla erken yapılmış bir basitleştirme olduğu ortaya çıkıyor
“Erken soyutlama birçok çirkinliğin kaynağıdır” diye bir özdeyiş rahatlıkla olabilir
Bunun üst katmanında, yani sprite sheet yerleşimini yorumlama ve oynatma modu işleme kısmında çeşitli varyasyonlar var ve her duruma uyan ortak bir soyutlama olmayabilir
Görünmeyen bir soyutlamayı zorla yaratmaya ya da kusurlu bir soyutlamaya uydurmaya çalışmak yerine, şu an yaptığınız gibi davranmayı tercih ederim. Soyutlama tamamen netleşip ihtiyaç da açıkça ortaya çıkana kadar beklemek iyi bir şeydir
DRY'ın karşı tarafında WET vardır. Anlamı, her şeyi iki ya da üç kez yazmaktır. Daha da önemlisi, bence yalnızca gerçekten kanıtlanmış kullanım senaryoları için — genellikle önce tekrar olarak ortaya çıkan şeyler için — soyutlama yapılmalıdır. Henüz var olmayan gelecekteki kullanım senaryoları için yazılan kod, gerçekten sahip olduğunuz şeyi soyutlamayı sık sık zorlaştırır; bu olduğunda da durum biraz komik olur
Zor ve sıkıcı işler, projenin son %10'una geldiğinizde de yapılabilir
Üstelik tekrarın yarattığı “bug” bazen oyuncuların sevdiği eğlenceli bir özelliğe bile dönüşebilir
OOP kullandığım dönemde soyutlamalar yüzünden çok zorlandım, ama neredeyse saf fonksiyonel yaklaşıma geçtikten sonra kod tekrarı nadirleşti
Sadece bir fonksiyon yazıp iki yerde çağırmanız yeterli. Asıl soyutlama sorunu veri yapılarında oluyor ama TypeScript interface'leri özünde duck typing olduğundan burada da çok büyük bir sorun yaşanmıyor
Bu yüzden soyutlama sorunlarından kaynaklanan kod tekrarı nadir. Geliştiricilerin silo hâline gelmesinden doğan kod tekrarı ise çok daha yaygın
Modern dillerin çoğu fonksiyonel programlama teorisinin üstüne rahatça kurulabilir ve Haskell bilmek şart değil. Herkesin zihni farklı çalışır ama küçük, basit ve bazen esnek parçaların bütünü oluşturduğu fikri bana çok uygun geliyor
Bu, büyük, karmaşık ve her şeyi yapan bir şekil dönüştürme makinesinin tam tersi
Takım büyüklüğü belli bir seviyeyi geçip herkesin diğer herkesin ne yaptığını bilmesi imkânsız hâle geldiğinde kod tekrarı oldukça kaçınılmazdır. Herkes fonksiyonel tarzda yazsa bile bu değişmez
Nitekim geçen ay şirkette tam da böyle bir şey oldu. Yeni bir saf helper fonksiyonu yazıp dosyanın başına koydum; bir hafta sonra bir iş arkadaşım, fiilen aynı işi yapan ama imzası farklı benzer bir helper fonksiyonunun aynı dosyanın sonunda zaten bulunduğunu söyledi
Metinle aynı bağlamda, ikisini de yaşamış olan herkes buna katılacaktır. Eksik tasarlanmış bir kod tabanı, aşırı tasarlanmış bir kod tabanından çok daha kolay yönetilir
Bakımını yapmak zorunda kaldığım en kötü kod, DRY ilkesine uymaya çalışan koddu. Ama bu ilkenin asıl niyetini anlamaya çalışmamışlardı.
O keşmekeşten çıkmanın tek yolu, geniş ölçekte kod tekrarını yeniden devreye sokmaktı
Burada aklıma iki sunum geliyor: Mike Acton’ın Data-Oriented Design and C++ [1] ve Brian Cantrill’in The Complexity of Simplicity [2]
Mike’ın sunumu, koddaki çözümün gerçek dünyayı modellemek zorunda olmadığını; farklı verilerin farklı problemler oluşturduğunu ve bu yüzden farklı çözümler gerektirdiğini söylüyor. Sunumu yeterince iyi aktarmam zor ama beni çok etkilemişti
Brian’ın sunumu ise genel olarak soyutlamaları ve “doğru” soyutlamayı bulmanın ne kadar zor olduğunu ele alıyor
Okuldan mezun oluşumun üzerinden daha birkaç yıl geçmişken Rust ile bir connection pool uyguluyordum; en mantıklı uygulama, connection nesnesinin pool’a zayıf bir referans tutması ve
dropolduğunda otomatik olarak geri dönmesiydi.Çok deneyimli bir yönetici olan müdürüm, “kitaplığı elinde tutan kitaptır değil, kitaplığı tutan kitaplık olur” diyerek bu fikirden hoşlanmamıştı. Tasarımı değiştirecek kadar ikna edici bir gerekçe gibi gelmemişti ama o, bu problemi o metaforun merceği dışında ele almak istemiyordu.
Sonunda başka bir yönetici, “Kütüphane kitabı kütüphaneyi içermez ama arkasında iade edileceği yeri gösteren kütüphane adı damgası vardır” deyince kilit açıldı. Görünüşe göre o yönetici bu benzetmeyi genişletmeyi makul bulmuştu
Daha deneyimli olsaydım, konunun özünden taviz vermeden o metaforun içinde konuşmanın yolunu bulabilirdim belki; ama bugün bile, kodun soyutlamasını ve kütüphaneyi kullanma deneyiminin sonuçlarını tartmak yerine o metaforu standart çerçeve diye dayatmasını tamamen tuhaf buluyorum
Kimse dinlemek istemiyor. Gerçekten kimse dinlemiyor. Şirketlerin %90’ında, yeni soyutlamalar üretirken mest olan sözde kıdemli geliştiriciler vardır
Aşırı tasarım, soyutlama ve erken optimizasyon, mühendisliğin üç büyük felaketidir
Öte yandan bunlar yüzünden her zaman iş olacak olması da sevindirici
Benzer şekilde, bazı geliştiriciler satır içi string’lerin veya sayısal sabitlerin tamamen şeytan işi olduğunu sanıyor gibi. Bir PR’da şunu görmüştüm
HTTPS_SCHEME = 'https'DOMAIN = 'www.example.com'url = HTTPS_SCHEME + '://' + DOMAIN“Sabit değer gömmeyin” lafını cargo cult gibi tekrar etmek dışında bunun ne kazandırdığını anlayamıyorum. Üstelik sabit tanımları dosyanın en üstündeydi, URL’yi oluşturan kod ise yüzlerce satır aşağıdaydı
Regex’leri de dosyanın tepesine koymak yerine kullanıldığı yerde bırakabilirsiniz. Dil yeterince akıllıysa muhtemelen bunun sabit olduğunu zaten anlar
Çok küçük bir fonksiyonsa doğrudan lambda kullanın. Bir ya da iki kez kullanılan tek satırlık fonksiyonların çok uzakta tanımlanmasını istemem
Test veya staging ortamında https yerine http kullanmanız gerekecekse, şemayı ve domain’i ayırmak ve sabitleri üstte ya da ayrı bir dosyada tutmak mantıklıdır.
url’nin birçok yerde mi yoksa tek bir yerde mi oluşturulduğu da önemlidirDosyanın tepesinde isimlendirilmiş sabitler tutmak son derece yaygın bir stildir ve bazen takımın kodlama standartlarının bir parçasıdır
Başka nedenler de olabilir; bu yüzden Chesterton’s Fence ilkesini hatırlamak iyi olur. Her hâlükârda buna doğrudan cargo cult demek iyi bir fikir değil. Bir başkası da satır içi literal kullanmayı aynı şekilde cargo cult sayabilir. Tuhaf görünüyorsa sorabilirsiniz; iyi bir sebebi olabilir ya da kimse önemsememiştir, siz de refactor edip sabitleri satır içine alırsanız memnun bile olabilirler
Onu sabite çıkarırsanız, sonra projeleri tek tek açıp kullanımları bul yapmak zorunda kalırsınız
Mikroservis kullanırsanız ikisini de yapabilirsiniz
Bir servisin bakımından sorumluysanız, başka bir servisteki kodu umursamanız için hiçbir neden yoktur. Başka bir ekibin koduysa neden umursayasınız? Hatta o ekibin var olduğunu bile bilmeniz gerekmez. Büyük sistemlerde, var olan tüm uygulamaları bilmek zaten gerçekçi olmayabiliyor
Yalnızca $19.95 karşılığında tek bir single point of failure’ı birden fazla single point of failure’a dönüştürüyoruz!
Servis odaklı mimari kullanıp sadece monolith dağıtmak daha iyidir. Test etmesi daha kolaydır ve serialization/deserialization gibi ek katmanlardan da kaçınırsınız
Çoğu kıdemli geliştiricinin DRY'yi körü körüne izlememek gerektiğini bildiğini düşünüyorum. Yine de çoğumuz, birden fazla yinelenen kod kaynağını sürdürmek zorunda olma fikrinden rahatsız oluruz
Bunu ele almak için, iki çağırıcının ortak koda bağımlı olduğu basit modeli dikkatle incelemek gerekir. Eğer ortak kod, yalnızca bir çağırıcının ihtiyaçları yüzünden değişmek zorundaysa, o kod ortak olana ait değildir
DRY'nin yanlış hedefi, kapsülleme ile çözmeye çalıştığı şeydir. Kapsülleme, yeniden düzenleme işini çağırıcılardan ortak koda taşır. Ancak ortak kodu güncellemenin etkisi, çağırıcılara kıyasla çok daha büyük olduğundan bu istenen yön değildir
Kapsüllemeden kaçınırken de DRY'ye uyulabilir. Çağıranın farkında olması gereken birden fazla ince soyutlama bulundurmak daha iyidir. OOP'de bunun için SRP ve IoC öğrenilir; prosedürel programlamada ise bir dizi yardımcı fonksiyon çağırmak şeklinde doğal olarak ortaya çıkar