“Bir Şey Yapıyor” paradigması ne zaman zararlı olur?


21

Argüman uğruna, burada belirli bir dosyanın içeriğini satır satır basan örnek bir fonksiyon.

Versiyon 1:

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  string line;
  while (std::getline(file, line)) {
    cout << line << endl;
  }
}

Fonksiyonların bir soyutlama düzeyinde bir şey yapması tavsiye edilir. Bana göre, yukarıdaki kod hemen hemen bir şey yapar ve oldukça atomiktir.

Bazı kitaplar (örneğin, Robert C. Martin Clean Code gibi) yukarıdaki kodu ayrı fonksiyonlara bölmeyi öneriyor gibi görünmektedir.

Versiyon 2:

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  printLines(file);
}

void printLines(fstream & file) {
  string line;
  while (std::getline(file, line)) {
    printLine(line);
  }
}

void printLine(const string & line) {
  cout << line << endl;
}

Neyi başarmak istediklerini anlıyorum (açık dosya / satırları oku / yazdırma satırı), ancak fazlaca dikkate almaz mı?

Orijinal versiyon basittir ve bir anlamda zaten bir şey yapar - bir dosyayı basar.

İkinci versiyon, ilk versiyondan daha az okunaklı olabilecek çok sayıda küçük fonksiyona yol açacaktır.

Bu durumda, kodu bir yerde bulundurmak daha iyi olmaz mıydı?

Hangi noktada "Bir Şey Yapıyor" paradigması zarar verir?


13
Bu tür bir kodlama uygulaması her zaman duruma göredir. Hiçbir zaman tek bir yaklaşım yoktur.
kasım

1
@Alex - Kabul edilen cevabın kelimenin tam anlamıyla soru ile ilgisi yok. Bunu gerçekten garip buluyorum.
ChaosPandion

2
Yeniden gözden geçirilmiş sürümünüzün altüst olduğunu ve okunabilirlik eksikliğine katkıda bulunduğunu unutmayın. Dosyanın aşağı Okuma, görmek beklenir printFile, printLinesnihayet, ve printLine.
Anthony Pegram

1
@Kev, bir kez daha, özellikle de bu kategorileşme konusunda sadece aynı fikirde değilim. Bu sahte değil, mesele bu! Özellikle ikinci sürümün okunabilir olamayacağını söyleyen OP. Clean Code'u özellikle ikinci versiyon için ilham kaynağı olarak gösteren OP. Benim yorumum aslında Temiz Kod'un ona bu şekilde kod yazmasına izin vermeyeceğidir . Sipariş aslında okunabilirlik açısından önemlidir, bir gazete makalesini okuduğunuz gibi okur, temelde ilginizi çekinceye kadar gittikçe daha fazla ayrıntı alırsınız.
Anthony Pegram

1
Geriye dönük bir şiir okumayı beklemeyeceğiniz gibi, ne de en düşük seviyede ayrıntıyı belirli bir sınıfın içindeki ilk şey olarak görmeyi beklemezsiniz. Demek istediğim , bu kodun hızlı bir şekilde sıralanması biraz zaman alıyor, ancak yalnızca bu kodun yazacağı tek kod olmadığını farz ediyorum. Bana göre, eğer Temiz Koddan bahsedecekse, yapabileceği en az şey onu takip etmektir . Kod düzensiz ise, kesinlikle diğerlerinden daha az okunabilir olacaktır.
Anthony Pegram

Yanıtlar:


15

Tabii ki, bu sadece " Bir şey nedir?" Sorusunu soruyor. Satır bir şey okumak ve başka bir satır yazmak mı? Yoksa bir satırın bir akıştan diğerine kopyalanması bir şey olarak kabul edilir mi? Veya bir dosyayı kopyalamak mı?

Buna sert ve nesnel bir cevap yok. Sana kalmış. Karar verebilirsin. Sen zorunda karar verirler. "Bir şeyi yap" paradigmasının temel amacı, mümkün olduğu kadar kolay anlaşılan kod üretmektir, bu nedenle bunu bir kılavuz olarak kullanabilirsiniz. Ne yazık ki, bu da nesnel olarak ölçülemez, bu yüzden bağırsak hislerinize ve “WTF?” Ye güvenmek zorundasınız. kod incelemesinde sayın .

IMO sadece tek bir kod satırından oluşan bir fonksiyon nadiren sorun çıkarmaz. Sizin printLine()kullanarak üstünlüğünün bulunmaması std::cout << line << '\n'1 direk. Ben görürsem printLine(), ben de onun adı ne diyor varsayalım veya Araştırıp onay gerekiyor. Görürsem std::cout << line << '\n', ne yaptığını hemen biliyorum, çünkü bu bir dizgenin içeriğini bir çizgi olarak çıkarmanın kanonik yoludur std::cout.

Bununla birlikte, paradigmanın bir diğer önemli amacı, kodun yeniden kullanılmasına izin vermektir ve bu çok daha objektif bir önlemdir. Örneğin, 2 sürümünde printLines() olabilir kolaylıkla bir evrensel olarak kullanışlı algoritma böylece yazılacak bu akıştan başka kopyalar hatları:

void copyLines(std::istream& is, std::ostream& os)
{
  std::string line;
  while( std::getline(is, line) );
    os << line << '\n';
  }
}

Böyle bir algoritma başka bağlamlarda da tekrar kullanılabilir.

Daha sonra bu kullanım senaryosuna özgü her şeyi, bu genel algoritmayı çağıran bir işleve koyabilirsiniz:

void printFile(const std::string& filePath) {
  std::ifstream file(filePath.c_str());
  printLines(file, std::cout);
}

1 yerine kullandığımı unutmayın . Yeni satır çıkışı için varsayılan seçenek olmalıdır , tuhaf bir durumdur .'\n'std::endl'\n'std::endl


2
+1 - Çoğunlukla aynı fikirdeyim ama sanırım "içgüdüsel duygudan" daha fazlası var. Sorun, insanların uygulama detaylarını sayarak "bir şeyi" yargılamasıdır. Bana göre, fonksiyon tek bir açıklamalı soyutlama yapmalı (ve adını açıklamalı). Asla "do_x_and_y" işlevine isim vermemelisiniz. Uygulama birkaç (daha basit) şey yapabilir ve yapmalıdır - ve bu daha basit şeylerin her biri birkaç hatta daha basit şeylere ayrıştırılabilir. Bu sadece ekstra kurallarla işlevsel bir ayrıştırmadır - fonksiyonların (ve isimlerinin) her birinin tek bir açık kavramı / görevi / neyi tanımlaması gerekir.
Steve314

@ Steve314: Uygulama ayrıntılarını olasılık olarak listelemedim. Satırları bir akıştan diğerine açıkça kopyalamak bir şeyden soyutlamadır. Yoksa öyle mi? Ve bunun yerine do_x_and_y()fonksiyonu isimlendirmekten kaçınmak kolaydır do_everything(). Evet, saçma bir örnek, ancak bu kuralın en kötü tasarım örneklerini bile önleyemediğini gösteriyor. IMO bu , sözleşmelerde belirtilen kadar içgüdüsel bir karardır. Aksi takdirde, nesnel olsaydı, bunun için bir metrik bulabilirdiniz - ki bu yapamazsınız.
sbi

1
Çelişmek istememiştim - sadece bir ek önermek için. Söylemeyi unuttuğum şeyin sanırım, sorudan, vb'ye ayrışmanın printLinegeçerli olduğu - bunların her birinin tek bir soyutlama olduğu - ama bunun gerekli olduğu anlamına gelmiyor. printFilezaten "bir şey". Bunu, üç ayrı düşük seviye soyutlamada ayrıştırabilmenize rağmen, mümkün olan her soyutlama düzeyinde ayrıştırmanız gerekmez . Her fonksiyon "bir şey" yapmalı, fakat her mümkün "bir şey" nin bir fonksiyon olması gerekmez. Çok fazla karmaşıklığı çağrı grafiğine taşımak sorun olabilir.
Steve314

7

Bir işleve sahip olmak, yalnızca "tek bir şey" yapar, Tanrı'nın emri değil, istenen iki sonun aracıdır:

  1. Eğer fonksiyonunuz sadece "bir şeyi" yaparsa, kod çoğaltmasından ve API şişmesinden kaçınmanıza yardımcı olacaktır, çünkü daha üst düzeyde daha az karmaşık olan fonksiyonların birleşimsel patlaması yerine, daha karmaşık işleri yapmak için fonksiyonlar oluşturabileceksiniz. .

  2. Having işlevleri sadece do "Bir şey" olabilir kodu daha okunabilir olun. Bu, şeyleri çözerek daha fazla netlik ve muhakeme kolaylığı elde edip etmeyeceğinize, şeyleri ayırmanıza izin veren yapıların ayrıntılı, dolaylı ve kavramsal yüklerine kaybedeceğinize bağlıdır.

Bu nedenle, "bir şey" kaçınılmaz olarak özneldir ve programınızla ne kadar soyutlamanın uygun olduğuna bağlıdır. Eğer printLinestek bir temel çalışma ve sizin amaçlı ardından, yaklaşık yaklaşık veya öngörülebilirliklerini bakımını önemsediğimi hatları baskı tek yolu olarak düşünülür printLinestek bir şey yapar. İkinci sürümü daha okunaklı bulmazsanız (Ben bilmiyorum), ilk sürüm iyi.

Daha düşük soyutlama seviyeleri üzerinde daha fazla kontrole ihtiyaç duymaya başlarsanız ve ince çoğaltma ve birleştirme patlamasıyla bitiyorsanız (yani, bir printLinesdosya adı için ve nesneler printLinesiçin tamamen ayrı fstreambir, bir printLineskonsol ve printLinesdosyalar için tamamen ayrı bir durumda ) printLines, seviyede birden fazla şey yapıyorsunuz demektir. umurunda olan soyutlama.


Üçüncüsü eklerdim ve küçük fonksiyonlar daha kolay test edilir. İşlev yalnızca bir şey yaparsa gerekli olan daha az giriş olması nedeniyle, bağımsız bir şekilde test edilmesini kolaylaştırır.
PersonalNexus

@PersonalNexus: Test konusunda biraz katılıyorum, ancak IMHO uygulama detaylarını test etmek aptalca. Bana göre bir birim testi cevabımda tanımlandığı gibi "bir şeyi" test etmelidir. Daha ince taneli olan herhangi bir şey testlerinizi kırılgan kılar (uygulama detaylarını değiştirmek testlerin değişmesini gerektirecektir çünkü) ve kodunuz can sıkıcı, dolaylı vb.
dsimcha

6

Bu ölçekte, önemli değil. Tek fonksiyonlu uygulama tamamen açık ve anlaşılabilir bir durumdur. Ancak, biraz daha fazla karmaşıklık eklemek, yinelemeyi işlemden ayırmayı çok çekici kılar. Örneğin, "* .txt" gibi bir desen tarafından belirtilen bir dizi dosyadan satır yazdırmanız gerektiğini varsayalım. Sonra yinelemeyi işlemden ayırırım:

printLines(FileSet files) {
   files.each({ 
       file -> file.eachLine({ 
           line -> printLine(line); 
       })
   })
}

Şimdi dosya yineleme ayrı ayrı test edilebilir.

Testleri basitleştirmek veya okunabilirliği artırmak için işlevleri böldüm. Her veri satırında gerçekleştirilen eylem bir yorumda bulunacak kadar karmaşık olsaydı, kesinlikle ayrı bir işleve bölerdim.


4
Sanırım başardın. Bir satırı açıklamak için bir yoruma ihtiyacımız olursa, o zaman bir yöntemi çıkarmanın zamanı gelmiştir.
Roger CS Wernersson 24:11

5

Bir şeyi açıklamak için bir yoruma ihtiyaç duyduğunuzda yöntemleri çıkarın.

Sadece isimlerinin ne söylendiğini açıkça söyleyen yöntemleri yazın ya da akıllıca adlandırılmış yöntemleri çağırarak bir hikaye anlatın.


3

Basit durumunuzda bile, Tek Sorumluluk İlkesinin daha iyi yönetmenize yardımcı olacağı konusunda ayrıntıları kaçırıyorsunuz. Örneğin, dosyayı açarken bir şeyler ters gittiğinde ne olur? Dosya erişim kenarı davalarına karşı sertleşmek için istisna işlemine ekleme, işlevinize 7-10 satır kod ekler.

Dosyayı açtıktan sonra hala güvende değilsin. Sizden tıkanmış olabilir (özellikle ağdaki bir dosya ise), hafızanız tükenebilir, tekrar sertleşmek istediğiniz ve yekpare işlevinizi şişirmek için bir dizi son durum olabilir.

Tek astar, baskı çizgisi yeterince zararsız görünüyor. Ancak dosya yazıcısına yeni işlevler eklendikçe (metni ayrıştırma ve biçimlendirme, farklı türlerdeki ekranlara oluşturma vb.) Artacak ve kendinize daha sonra teşekkür edeceksiniz.

SRP'nin amacı, aynı anda tek bir görev hakkında düşünmenize olanak sağlamaktır. Okuyucu, geçmeye çalıştığınız noktayı anlayabilmesi için büyük bir metin bloğunu birden fazla paragrafa bölmek gibidir. Bu ilkelere uyan kod yazmak biraz zaman alır. Ancak bunu yaparken bu kodu okumayı kolaylaştırıyoruz. Gelecekteki benliğinizin koddaki bir hatayı takip etmesi ve onu düzgünce bölümlere ayırması gerektiğinde ne kadar mutlu olacağını düşünün.


2
Buna cevap verdim çünkü buna katılmasam da mantığı sevdim! Gelecekte neler olabileceğine dair bazı karmaşık düşüncelere dayanan yapı sağlayın. İhtiyacınız olduğunda çarpan kodu. İhtiyacınız olana kadar soyut şeyler yapmayın. Modern kod, sadece çalışan kod yazmak yerine ve isteksizce uyarlamak yerine, slavice kurallara uymaya çalışan insanlar tarafından kandırılıyor . İyi programcılar tembeldir .
Yttrill 24/11

Yorumunuz için teşekkürler. Not Erken soyutlamayı savunmuyorum, sadece mantıksal işlemleri bölerek daha sonra yapmak daha kolay olur.
Michael Brown

2

Ben kişisel olarak ikinci yaklaşımı tercih ediyorum, çünkü ileride çalışmanızı önlüyor ve "genel bir şekilde nasıl yapılacağını" zihniyete zorluyor. Buna rağmen durumunuzda Versiyon 1, Versiyon 2'den daha iyidir - sadece Versiyon 2 tarafından çözülen problemler çok önemsiz ve fstream'e özgüdür. Sanırım şu şekilde yapılması gerekiyor (Nawaz tarafından önerilen hata düzeltme dahil):

Genel yardımcı program işlevleri:

void printLine(ostream& output, const string & line) { 
    output << line << endl; 
} 

void printLines(istream& input, ostream& output) { 
    string line; 
    while (getline(input, line)) {
        printLine(output, line); 
    } 
} 

Etki alanına özgü işlev:

void printFile(const string & filePath, ostream& output = std::cout) { 
    fstream file(filePath, ios::in); 
    printLines(file, output); 
} 

Şimdi printLinesve printLineyalnızca fstreamakışla değil, akışla da çalışabiliriz .


2
Katılmıyorum. Bu printLine()işlevin değeri yok. Cevabımı gör .
sbi

1
Eğer printLine () 'yı tutarsak, satır numaraları veya sözdizimi renklendirmesi ekleyen bir dekoratör ekleyebiliriz. Bunu söyledikten sonra, bir sebep bulana kadar bu yöntemleri çıkarmayacağım.
Roger CS Wernersson 24:11

2

İzlenmesi gereken her paradigma (sadece alıntı yaptığınız şey değil), biraz disipline ve dolayısıyla “ifade özgürlüğünü” azaltmaya ihtiyaç duyar - bir başlangıç ​​ek yüküyle sonuçlanır (en azından bunu öğrenmek zorunda olduğunuz için!). Bu anlamda, bu ek yükün maliyeti, paradigmanın kendisiyle devam etmek için tasarlandığı avantajla aşırı telafi edilmediğinde, her paradigma zararlı olabilir.

Dolayısıyla, sorunun gerçek cevabı, geleceği "öngörmek" için iyi bir yetenek gerektirir:

  • Ben şimdi yapmam gerekiyor AveB
  • Olasılık bir de, nedir yakın gelecekte ben de yapmak için gerekli olacak A-ve B+(yani A ve B, ama sadece biraz farklı gibi görünüyor yani bir şey)?
  • Daha uzak bir gelecekte olasılık A + olacağı, ne A*ya A*-?

Eğer bu olasılık göreceli olarak yüksekse, A ve B hakkında düşünürken onların muhtemel değişkenlerini de düşünürsem, böylece ortak parçaları izole etmek için onları tekrar kullanabilmem için iyi bir şans olur.

Bu olasılık çok düşükse (etrafındaki değişken ne olursa olsun Aesasen Akendisinden başka bir şey değildir ), A'yı nasıl daha fazla ayrıştıracağınızı inceleyin, büyük olasılıkla boşa harcanan zamanla sonuçlanacaktır.

Örnek olarak, size bu gerçek hikayeyi anlatayım:

Bir öğretmen olarak geçmiş yaşamım boyunca, öğrencinin projelerinin çoğunda - pratik olarak hepsinin bir C dizesinin uzunluğunu hesaplamak için kendi işlevlerini sağladığını keşfettim .

Bazı araştırmalardan sonra, sık sık bir sorun olarak, tüm öğrencilerin bunun için bir işlev kullanma fikrine geldiğini keşfettim. Onlara bunun için bir kütüphane işlevi olduğunu söyledikten sonra strlen, birçoğu, sorunun çok basit ve önemsiz olduğu için, kendi işlevlerini (2 satır kod) yazmalarının, C kütüphane kılavuzunu aramaktan daha etkili olduğunu söyledi. (1984’ü, WEB'yi ve google’ı unuttu!) bunun için hazır bir işlev olup olmadığını görmek için kesin alfabetik sırada.

Bu, aynı zamanda "tekerleği yeniden icat etmeme" paradigmasının etkili bir tekerlek kataloğu olmadan zararlı olabileceği bir örnektir!


2

Örneği, belirli bir görevi yapmak için dün gerekli olan bir fırlatma aracında kullanılması iyi. Veya doğrudan bir yönetici tarafından kontrol edilen bir yönetim aracı olarak. Şimdi müşterilerinize uygun olmasını sağlamlaştırın.

Anlamlı mesajlarla uygun hata / istisna işleme ekleyin. Belki de var olan dosyaların nasıl kullanılacağı gibi, alınması gereken kararlar da dahil olmak üzere parametre doğrulaması gerekir. Belki bilgi ve hata ayıklama gibi farklı düzeylerde günlük kaydı işlevini ekleyin. Ekip arkadaşlarınızın orada neler olduğunu bilmeleri için yorumlar ekleyin. Kod örneklerini verirken genellikle kırılganlık için ihmal edilen ve okuyucunun alıştırması olarak bırakılan tüm parçaları ekleyin. Ünite testlerini unutma.

Sizin güzel ve oldukça lineer bir küçük işlevi aniden karmaşık bir karmaşa içinde biter o dilenir ayrı fonksiyonlara içine parçalı edilecek.


2

IMO, bir işin başka bir işleve delege etmekten başka hiçbir şey yapamayacağı kadar uzadıkça zararlı hale gelir, çünkü bu, artık bir şey soyutlamanın olmadığı ve bu tür işlevlere yol açan zihniyetin her zaman tehlikede olduğu işaretidir. daha kötü şeyler yapmak ...

Orijinal yayından

void printLine(const string & line) {
  cout << line << endl;
}

Yeterince bilgiçiyseniz, printLine'ın hala iki şey yaptığını fark edebilirsiniz: satırı kesmek ve bir "bitiş çizgisi" karakteri eklemek. Bazı insanlar yeni işlevler oluşturarak bunu ele almak isteyebilir:

void printLine(const string & line) {
  reallyPrintLine(line);
  addEndLine();
}

void reallyPrintLine(const string & line) {
  cout << line;
}

void addEndLine() {
  cout << endl;
}

Oh hayır, şimdi sorunu daha da kötüleştirdik! Şimdi printLine'ın İKİ şey yaptığı bile OBVIOUS! !!! 1! Bir satır yazdırmanın satırın kendisini yazdırmaktan ve satır sonu karakteri eklemekten ibaret olduğu kaçınılmaz sorundan kurtulmak için hayal edilebilecek en saçma "çalışma ortamları" nı oluşturmak çok aptallık yapmaz.

void printLine(const string & line) {
  for (int i=0; i<2; i++)
    reallyPrintLine(line, i);
}

void reallyPrintLine(const string & line, int action) {
  cout << (action==0?line:endl);
}

1

Kısa cevap ... buna bağlı.

Bunu düşünün: peki, gelecekte yalnızca standart çıktıya değil, bir dosyaya yazdırmak istemezseniz.

YAGNI'nin ne olduğunu biliyorum, ancak yalnızca bazı uygulamaların gerekli olduğu bilinen ancak ertelenen vakalar olabileceğini söylüyorum. Bu yüzden, belki de mimar ya da işleyen ne bilirse, bir dosyaya yazdırabilmesi gerekir, ancak şu anda uygulamayı yapmak istemez. Bu yüzden bu ekstra işlevi yaratır, bu nedenle, gelecekte çıktıyı yalnızca bir yerde değiştirmeniz gerekir. Mantıklı?

Bununla birlikte, yalnızca konsolda çıktıya ihtiyacınız olduğundan eminseniz, pek mantıklı gelmiyor. Üzerine "sarmalayıcı" yazmak cout <<işe yaramaz görünüyor.


1
Fakat kesinlikle konuşursak, printLine işlevi satırları yinelemekten farklı bir soyutlama düzeyi değil mi?

@Petr Sanırım, bu yüzden işlevselliği ayırmanızı öneriyorlar. Bence bu kavram doğru, ancak bunu duruma göre uygulamanız gerekiyor.

1

“Bir şeyi yapın” erdemlerine bölümler ayıran kitapların olmasının nedeni, 4 sayfalık uzunluğa sahip fonksiyon yazıyor ve koşullu 6 seviyeli yuva geliştiricileri hala var. Kodunuz basit ve anlaşılırsa doğru yaptınız.


0

Diğer afişlerin de söylediği gibi, bir şeyi yapmak bir ölçek meselesidir.

Ayrıca, Bir Şey fikrinin insanları yan etki kodlamasını durdurmak olduğunu da önerebilirim. Bu, sıralı kuplaj ile örneklenmiştir. 'doğru' sonucu elde etmek için yöntemlerin belirli bir sıra ile çağrılması gereken .

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.