Yöntem çıkarma - temel varsayımlar


27

Büyük yöntemleri (ya da prosedürleri ya da işlevleri - bu soru OOP'a özgü değilken , OOP dillerinde zamanın% 99'unda çalıştığım için, en rahat olduğum terminoloji) birçok küçük içine ayırdığımda , Sık sık kendimi sonuçlardan memnun hissetmiyorum. Bu küçük yöntemlerle ilgili olarak akılda tutulması zor olanlardan sadece kod blokları olduklarından daha zorlaşır, çünkü onları çıkardığımda, arayan kişinin bağlamından gelen birçok temel varsayımı kaybederim.

Daha sonra, bu koda baktığımda ve bireysel yöntemleri gördüğümde, nereden çağırıldıklarını hemen bilmiyorum ve onları dosyanın herhangi bir yerinden çağrılabilecek sıradan özel yöntemler olarak düşünüyorum. Örneğin, bir başlatma yönteminin (yapıcı veya başka türlü) bir dizi küçük seçeneğe bölündüğünü hayal edin: yöntemin kendisinde, nesnenin durumunun hala geçersiz olduğunu açıkça biliyorsunuzdur, ancak sıradan bir özel yöntemde muhtemelen o nesnenin varsayımına girersiniz Zaten başlatıldı ve geçerli bir durumda.

Bunun için gördüğüm tek çözüm where, sadece "ebeveyn" işlevinde kullanılan küçük işlevleri tanımlamanıza izin veren Haskell'deki fıkra. Temel olarak, bu gibi görünüyor:

len x y = sqrt $ (sq x) + (sq y)
    where sq a = a * a

Ancak kullandığım diğer diller böyle bir şeye sahip değil - en yakın şey, muhtemelen daha da kafa karıştırıcı olan yerel bir kapsamda bir lambda tanımlamak.

Öyleyse benim sorum şu - bununla karşılaşıyor musunuz ve bunun bir problem olduğunu görüyor musunuz? Bunu yaparsanız, genellikle Java / C # / C ++ gibi "ana akım" OOP dillerinde bunu nasıl çözersiniz?

Çiftler hakkında düzenleme : Başkalarının farkına vardığı gibi, bölme yöntemlerini tartışan sorular ve bir soru olan küçük sorular var. Onları okudum ve arayanın bağlamından kaynaklanabilecek temel varsayımlar konusunu tartışmıyorlar (yukarıdaki örnekte, başlatılan nesne). Bu benim sorum nokta ve bu yüzden sorum farklı.

Güncelleme: Aşağıdaki soru ve tartışmayı takip ettiyseniz , konuyla ilgili özellikle John Carmack'in bu makalesinin tadını çıkarabilirsiniz :

Gerçekleştirilen kodun farkındalığının yanı sıra, satır içi işlevler, işlevi başka yerlerden çağırmayı mümkün kılma avantajına da sahiptir. Kulağa saçma geliyor ama bunun bir anlamı var. Bir kod temeli yıllarca kullanıldıkça büyüdükçe, kısayol almak için birçok fırsat olacak ve sadece yapılması gerektiğini düşündüğünüz işi yapan bir işlev çağırın. PartialUpdateA () ve PartialUpdateB () işlevini çağıran bir FullUpdate () işlevi olabilir, ancak belirli bir durumda yalnızca PartialUpdateB () yapmanız gerektiğini ve diğerlerinden kaçınarak verimli olduğunuzu anlayabilir (veya düşünebilirsiniz). iş. Bundan çok ve çok sayıda böcek kaynaklanıyor. Çoğu hata, yürütme durumunun tam olarak düşündüğünüz gibi olmamasından kaynaklanır.




@ Bağlantılı olduğunuz soruları sorgulamayınca, hiç işlev çıkarmamayı veya tartışmamayı tartışıyor. Bunun yerine, yapmak için en uygun yöntemi sorguluyorum.
Max Yankov

2
@gnat, oradan bağlantılı başka sorular var, ancak bunlardan hiçbiri bu kodun yalnızca arayanlar bağlamında geçerli olan belirli varsayımlara dayanabileceğini tartışmıyor.
Max Yankov

1
@Doval deneyimime göre, gerçekten öyle. Yeni ayıklanması, tarif gibi takılmak zahmetli yardımcı yöntemler olduğunda tutarlı sınıf bu ilgilenir
tatarcık

Yanıtlar:


29

Örneğin, bir küçükleme dizisine bölünmüş bir başlatma yöntemini hayal edin: yöntemin bağlamında, nesnenin durumunun hala geçersiz olduğunu açıkça biliyorsunuz, ancak sıradan bir özel yöntemde muhtemelen nesnenin zaten başlatıldığını ve geçerli bir durumda Bunun için gördüğüm tek çözüm ...

Endişeniz köklü. Başka bir çözüm var.

Bir adım geri at. Bir yöntemin amacı esas olarak nedir? Yöntemler sadece iki şeyden birini yapar:

  • Bir değer üret
  • Bir sonuç neden

Veya, ne yazık ki, ikisi de. Her ikisini de yapan yöntemlerden kaçınmaya çalışıyorum, ancak çok fazla. Diyelim ki, üretilen etkinin veya üretilen değer, yöntemin “sonucu”.

Yöntemlerin "bağlam" olarak adlandırıldığını unutmayın. Bu bağlam nedir?

  • Argümanların değerleri
  • Programın yöntem dışında durumu

Temel olarak işaret ettiğiniz şey: yöntemin sonucunun doğruluğu, çağrıldığı içeriğe bağlıdır .

Biz diyoruz bir yöntem gövdesi doğru bir sonuç üretmek için yöntem için başlamadan önce gerekli koşulları onun önkoşulları ve biz diyoruz yöntem vücut döndükten sonra üretilecek koşulları onun Hedefşartlar .

Esasen işaret ettiğiniz şey şudur: Bir kod bloğunu kendi yöntemine aktardığımda, ön koşullar ve son koşullar hakkında bağlamsal bilgileri kaybediyorum .

Bu sorunun çözümü , ön koşulları ve ön koşulları programda açık hale getirmektir . Örneğin, C # ' Debug.Assertda ön koşulları ve son koşulları ifade etmek için Kod Sözleşmelerini kullanabilirsiniz.

Örneğin: Eskiden derlemenin birkaç "aşamasından" geçen bir derleyici üzerinde çalışırdım. İlk önce kod yazılır, sonra ayrıştırılır, sonra türler çözülür, daha sonra devralma hiyerarşileri döngüler için kontrol edilir, vb. Kodun her biti içeriğine çok hassastır; Örneğin, "bu tür o türden dönüştürülebilir mi?" diye sormak felaket olur. Baz tipleri grafiğinin henüz asiklik olmadığı bilinmiyorsa! Bu nedenle, her kod parçası ön koşullarını açıkça belgelemiştir. Biz olurdu assertyöntem denilebilir nereye biz zaten "baz tipleri, asiklik" çek geçmişti ve daha sonra okuyucuya belli oldu o tip konvertibilite için kontrol yöntemi ve nerede denir edilemedi.

Elbette, iyi yöntem tasarımının tanımladığınız sorunu hafifletmesinin birçok yolu vardır:

  • Etkileri veya değerleri için yararlı olan ancak her ikisi için de faydalı olmayan yöntemler kullanın.
  • mümkün olduğunca "saf" olan yöntemleri yapın; "saf" bir yöntem yalnızca argümanlarına dayanan bir değer üretir ve etkisizdir. Bunlar, akıl alması en kolay yöntem çünkü ihtiyaç duydukları "bağlam" çok yerelleştirilmiş.
  • program durumunda olan mutasyon miktarını en aza indirir; mutasyonlar, kodun nedenini zorlaştırdığı noktalardır.

Sorunu önkoşullar / sonkoşullar açısından açıklayan cevap olarak +1.
QuestionC

5
Öncesi ve sonrası koşulları kontrol etmek için tip sistemine genellikle izin verilmesi gerektiğini (ve iyi bir fikir!) Eklerdim. Eğer onu alan stringve veritabanına kaydeden bir işleve sahipseniz, temizlemeyi unutursanız, SQL enjeksiyon riskiniz vardır. Öte yandan, işleviniz a'yı alırsa SanitisedStringve bunu elde etmenin tek yolu SantisiedStringçağrı yapmaksa Sanitise, SQL enjeksiyon hatalarını yapıya göre reddetmiş olursunuz. Derleyicimin yanlış kodu reddetmesini sağlayacak yollar aramaya başladım.
Benjamin Hodgson

+1 Dikkat edilmesi gereken en önemli şey, büyük bir yöntemi daha küçük parçalara bölmenin bir maliyeti olduğu: ön koşullar ve son koşullar başlangıçta olduğundan daha rahat olmadıkça, genellikle kullanışsızdır ve aksi takdirde yapmış olacağınız kontrolleri tekrar yaparak masrafı ödeyebilirsiniz. Tamamen "özgür" bir yeniden düzenleme süreci değil.
Mehrdad

"Bu bağlam nedir?" Sadece açıklığa kavuşturmak için, çoğunlukla bu yöntemin çağrıldığı nesnenin özel durumu demek istedim. Sanırım ikinci kategoriye girmiş.
Max Yankov

Bu mükemmel ve düşündürücü bir cevap, teşekkür ederim. (Tabii ki diğer cevapların kötü olduğu söylenemez). Soruyu henüz cevaplanmış olarak işaretlemem, çünkü buradaki tartışmayı gerçekten seviyorum (ve cevap cevap olarak işaretlendiğinde durma eğilimindeyim) ve işlemek ve düşünmek için zamana ihtiyacım var.
Max Yankov

13

Bunu sık sık görüyorum ve bunun bir sorun olduğu konusunda hemfikirim. Genellikle onu bir yöntem nesnesi oluşturarak çözerim : üyeleri orijinal, çok büyük bir yöntemin yerel değişkenleri olan yeni bir özel sınıf.

Yeni sınıf, 'İhracatçı' veya 'Tablolama' gibi bir isme sahip olma eğilimindedir ve bu özel işi daha geniş bir bağlamda yapmak için gerekli olan her türlü bilgiyi geçer. Sonra herhangi bir şey için kullanılıyor tehlikesinde bile daha küçük bir yardımcı kod parçacıkları tanımlamak özgürdür ama tabulating veya ihraç.


Bu fikri gerçekten daha çok düşünüyorum. Genel veya iç sınıf içinde özel bir sınıf olabilir. Ad alanınızı yalnızca yerel olarak çok önemsediğiniz sınıflarla karıştırmazsınız ve bunların “kurucu yardımcıları” veya “yardımcıları” ya da her neyse bunları işaretlemenin bir yoludur.
Mike, Monica

Son zamanlarda bunun için mimari açıdan ideal olabilecek bir durumdaydım. Bir render oluşturucu sınıfına sahip bir yazılım oluşturucuyu ve diğer metodları çağırmak için kullandığı LOT bağlamı olan bir public render metodunu yazdım. Bunun için ayrı bir RenderContext sınıfı yaratmayı düşünmüştüm, ancak bu projeyi her çerçevede tahsis etmek ve dağıtmak son derece gereksiz görünüyordu. github.com/golergka/tinyrenderer/blob/master/src/renderer.h
Max Yankov

6

Birçok dil, Haskell gibi işlevleri yerleştirmenize izin verir. Java / C # / C ++ aslında bu bakımdan göreceli aykırı değerlerdir. Ne yazık ki, insanlar düşünecek olursak o kadar popüler, "Bu sahiptir aksi sevdiğim 'ana akım' dili, onu sağlayacak, kötü bir fikir olması."

Java / C # / C ++, temel olarak bir sınıfın ihtiyaç duyduğunuz tek yöntem grubu olması gerektiğini düşünüyor. Bağlamlarını belirleyemeyecek kadar çok yönteminiz varsa, almanız gereken iki genel yaklaşım vardır: bunları bağlama göre sıralayın ya da bağlama göre ayırın.

Bağlama göre sıralama , yazarın "TO paragrafları" modelini tanımladığı Temiz Kod'da yapılan bir öneridir. Bu, temel olarak yardımcı işlevlerinizi, onları çağıran işlevden hemen sonra koymaktır; böylece, bir gazete makalesinde paragraflar gibi okuyabilir ve daha fazla ayrıntı okursunuz. Bence videolarında onları bile içeriyor.

Diğer yaklaşım ise sınıflarınızı bölmektir. Bu çok ileriye götürülemiyor, çünkü can sıkıcı nesneleri üzerlerinde herhangi bir metot çağırmadan önce somutlaştırmaya ihtiyaç duyuyor ve birkaç minik sınıftan hangisinin her bir veri parçasına sahip olması gerektiğine karar vermekle ilgili doğal problemler var. Ancak, zaten yalnızca bir bağlamda gerçekten uygun olan birkaç yöntem tanımladıysanız, muhtemelen kendi sınıflarına girmeyi düşünmek için iyi bir adaydır. Örneğin, karmaşık başlatma, oluşturucu gibi yaratıcı bir düzende yapılabilir.


Yerleştirme işlevleri ... C # (ve Java 8) 'de lambda işlevlerinin elde ettiği şey değil mi?
Arturo Torres Sánchez

Daha çok bu piton örnekleri gibi bir adla tanımlanan bir kapanma gibi düşünüyordum . Lambdalar böyle bir şeyi yapmanın en net yolu değil. Bir filtre yüklemi gibi kısa ifadeler için daha fazladırlar.
Karl Bielefeldt

Bu Python örnekleri kesinlikle C # ile mümkündür. Örneğin, faktoring . Daha ayrıntılı olabilirler, ancak% 100 mümkündür.
Arturo Torres Sánchez

2
Kimse mümkün olmadığını söylemedi. OP sorusunda lambda kullandığından bile söz etti. Sadece okunabilirlik uğruna bir yöntem çıkarırsanız, daha okunaklı olsaydı iyi olurdu.
Karl Bielefeldt

İlk paragrafınız, özellikle teklifinizle mümkün olmadığını ima ediyor gibi görünüyor: "Kötü bir fikir olmalı, aksi halde en sevdiğim 'ana akım' dili buna izin verirdi."
Arturo Torres Sánchez

4

Çoğu durumda cevabın bağlam olduğunu düşünüyorum. Bir geliştirici kod yazarken, gelecekte kodunuzun değişeceğini varsaymalısınız. Bir sınıf başka bir sınıfa entegre olabilir, kendi iç algoritmasının yerini alabilir veya soyutlama oluşturmak için birkaç sınıfa ayrılabilir. Bunlar başlangıç ​​seviyesindeki geliştiricilerin genellikle dikkate almadığı, dağınık geçici çözümlere ihtiyaç duyduğu veya daha sonra tam olarak elden geçirebileceği şeyler.

Çıkarma yöntemleri iyidir, ancak bir dereceye kadar. Muayene yaparken veya kod yazmadan önce kendime şu soruları sormaya çalışıyorum:

  • Bu kod yalnızca bu sınıf / işlev tarafından mı kullanılıyor? gelecekte aynı kalacak mı?
  • Somut uygulamalardan bazılarını kapatmam gerekirse, bunu kolayca yapabilir miyim?
  • Ekibimdeki diğer geliştiriciler bu işlevde neler yapıldığını anlayabilir mi?
  • Aynı kod bu sınıfta başka bir yerde mi kullanılıyor? Neredeyse tüm durumlarda çoğaltmayı önlemek gerekir.

Her durumda, her zaman tek bir sorumluluk düşünün. Bir sınıfın bir sorumluluğu olmalı, işlevleri tek bir sabit hizmete hizmet etmeli ve çok sayıda eylemde bulunursa, bu eylemlerin kendi işlevlerine sahip olması gerekir, bu nedenle bunları daha sonra ayırt etmek veya değiştirmek kolaydır.


1

Bu küçük yöntemlerle ilgili olarak akılda tutulması zor olanı sadece kod blokları olduklarından daha zor hale getirir, çünkü onları çıkardığımda, arayan kişinin bağlamından gelen bir sürü temel varsayımı kaybederim.

Daha büyük, döngüsel sistem işlevlerini (sadece sistemlerde işlevli olan sistemler var) ve ham verilere doğru akan bağımlılıkları , soyutlamaları değil, benimsemeye kadar bunun ne kadar büyük bir sorun olduğunun farkında değildim .

Bu, benim sürprizime göre, geçmişte çalıştığım kod tabanlarına kıyasla akılda tutulması ve bakımı için çok daha kolay bir kod temeli oluşturdu; hata ayıklama sırasında, sık sık soyut fonksiyonlar aracılığıyla, her türlü ufacık küçük fonksiyonlar boyunca izlemeniz gerekti. Nerede izleyene kadar kim bilir, sadece kodun asla yol göstermeyeceğini düşündüğünüz yerlere yol açan bazı olaylar dizisini ortaya çıkarmak için saf arayüzler.

John Carmack'den farklı olarak, bu kod tabanlarındaki en büyük sorunum performans değildi; çünkü AAA oyun motorlarında ve performans sorunlarımızın çoğunda verim ile ilgili olarak bu kadar çok gecikme süresi olmadı. Tabii ki, teenier ve teenier fonksiyonlarının ve sınıflarının daha dar ve daha dar sınırları içinde çalışırken, bu yapıya girmeden (tüm bu ufacık parçaları geri kaynamanızı gerektiren), sıcak noktaları daha da zorlaştırmaya da başlayabilirsiniz. daha büyük bir şeye bile etkili bir şekilde baş etmeye bile başlayabilirsiniz).

Yine de benim için en büyük sorun, geçen bütün sınavlara rağmen sistemin genel doğruluğu hakkında güvenle düşünememek oldu. Beynime girip kavramak için çok fazla şey vardı, çünkü bu tür bir sistem, tüm bu küçük ayrıntıları ve her yerde olan küçük işlevler ve nesneler arasındaki sonsuz etkileşimleri hesaba katmadan bunun hakkında bir nedene izin vermedi. Çok fazla "eğer öyleyse?" Vardı, doğru zamanda çağrılması gereken çok fazla şey, yanlış zamanda çağrılırsa ne olacağı hakkında çok fazla soru vardı (bu, siz ne zaman paranoya noktasına çıkmaya başladı?) bir olayın başka bir olayı tetiklemesini, diğerinin sizi her türlü öngörülemeyen yerlere yönlendirmesini sağlayın), vb.

Şimdi büyük kıçımın 80 çizgi işlevlerini burada ve orada seviyorum, hâlâ tekil ve açık bir sorumluluk üstlendikleri ve 8 seviyeli iç içe bloklara sahip olmadıkları sürece. Bu büyük işlevlerin daha küçük, parçalanmış versiyonları sadece başkaları tarafından aranamayan özel uygulama detayları olsa bile, test etmek ve anlamak için sistemde daha az şeylerin olduğu hissine yol açarlar. sistemde daha az etkileşimin olduğu gibi hissetme eğilimindedir. Daha az fonksiyon anlamına gelirse, karmaşık mantık olmadığı sürece (sadece 2-3 satır kod söyler), çok mütevazı bir kod çoğaltmayı bile seviyorum. Carmack'in oradaki mantığı, bu işlevselliği kaynak dosyada başka yerde arama yapmak imkansız kılan inline etme konusundaki mantığını seviyorum. Orada'

Seçenek bir etli işlev ile karmaşık bir bağımlılık grafiği ile birbirlerini çağıran 12 basit basit işlev arasındaysa, basitlik her zaman karmaşıklığı azaltmaz. Günün sonunda, genellikle bir fonksiyonun ötesinde olup bitenler hakkında aklınıza gelmek zorundasınız, bu fonksiyonların nihayetinde yaptıkları hakkında aklınıza gelmek zorundasınız. En küçük puzzle parçaları.

Elbette, iyi bir şekilde test edilmiş olan çok amaçlı kitaplık türü kodu bu kuraldan muaf tutulabilir, çünkü bu çok amaçlı kitap kodları çoğu zaman tek başına iyi çalışır ve durur. Ayrıca , uygulamanızın etki alanına biraz daha yakın olan kodla karşılaştırıldığında ufacık olma eğilimindedir (milyonlarca satır değil, binlerce kod satırı) ve günlük kelime dağarcığının bir parçası olmaya başladığı için çok yaygındır. Ancak, sistem genelinde değişmeyenlerin tek bir işlevin veya sınıfın çok ötesine geçmesi gereken uygulamanız için daha belirgin olan bir şeyle, ne sebeple olursa olsun daha küçük işlevlere sahip olmaya yardımcı olma eğilimindeyim. Büyük resimde neler olup bittiğini anlamaya çalışırken daha büyük bulmaca parçalarıyla çalışmayı çok daha kolay buluyorum.


0

Bunun büyük bir sorun olduğunu sanmıyorum , ancak sorunlu olduğuna katılıyorum. Genellikle, yardımcısını hemen faydalanıcısından hemen sonra yerleştirir ve bir "Yardımcı" ekini eklerim. Bu artı privateerişim belirteci rolünü açıkça belirtmelidir. Yardımcı çağrıldığında tutmayan bir değişmez varsa, yardımcıya bir yorum eklerim.

Bu çözüm, yardım ettiği işlevin kapsamını yakalamamanın talihsiz dezavantajına sahiptir. İdeal olarak, işlevleriniz küçüktür, bu yüzden umarım bu çok fazla parametreye neden olmaz. Normalde bunu, parametreleri bir araya getirmek için yeni yapılar veya sınıflar tanımlayarak çözersiniz, ancak bunun için gerekli olan kazan plakası miktarı, yardımcının kendisinden daha uzun olabilir ve daha sonra açık bir ilişkilendirme yolu ile başladığınız yere geri dönersiniz. İşlevli yapı.

Diğer çözümden zaten bahsettiniz - ana fonksiyonun içindeki yardımcıyı tanımlayın. Bazı dillerde çok nadir görülen bir deyim olabilir, ancak kafa karıştırıcı olacağını düşünmüyorum (akranlarınız genel olarak lambdalar tarafından karıştırılmıyorsa). Bu, yalnızca işlevleri veya işlev benzeri nesneleri kolayca tanımlayabiliyorsanız çalışır. Örneğin, bunu Java 7'de denemem, çünkü anonim bir sınıf en küçük "işlev" için bile 2 yuvalama düzeyi eklemeyi gerektirir. Bu alabileceğiniz en yakın letya da wheremaddeye yakındır ; Tanımdan önce yerel değişkenlere başvurabilirsiniz ve yardımcı bu kapsamın dışında kullanılamaz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.