Bir yöntem neden birden fazla işaretlenmiş istisna türü atmamalıdır?


47

Java kodumuzu analiz etmek için SonarQube kullanıyoruz ve bu kuralı var (kritik olarak ayarlanmış):

Genel yöntemler en fazla bir tane işaretlenmiş istisna atmalıdır

İşaretli istisnaları kullanmak, yöntemi arayanları, ya onları ya da bunları kullanarak ya hata yaparak başa çıkmaya zorlar. Bu, istisnaları tamamen yöntemin API'sinin bir parçası yapar.

Arayanlar için karmaşıklığı makul tutmak için, yöntemler birden fazla kontrol edilmiş istisna oluşturmamalıdır. "

Sonar başka bit vardır bu :

Genel yöntemler en fazla bir tane işaretlenmiş istisna atmalıdır

İşaretli istisnaları kullanmak, yöntemi arayanları, ya onları ya da bunları kullanarak ya hata yaparak başa çıkmaya zorlar. Bu, istisnaları tamamen yöntemin API'sinin bir parçası yapar.

Arayanlar için karmaşıklığı makul tutmak için, yöntemler birden fazla kontrol edilmiş istisna oluşturmamalıdır.

Aşağıdaki kod:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

içine bakılmalıdır:

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

Geçersiz kılma yöntemleri bu kural tarafından kontrol edilmez ve kontrol edilen birkaç istisna atmasına izin verilir.

İstisna işleme konusundaki okumalarımda bu kural / tavsiyeye hiç rastlamadım ve konuyla ilgili bazı standartlar, tartışmalar vb. Bulmaya çalıştım. Bulduğum tek şey CodeRach’tan geliyor: Bir yöntem en fazla kaç istisna atmalıdır ?

Bu iyi kabul edilmiş bir standart mı?


7
Ne yapmak sen düşünmek? SonarQube'den alıntı yaptığınız açıklamalar mantıklı görünüyor; Bundan şüphelenmek için bir nedeniniz var mı?
Robert Harvey

3
Birden fazla istisna atan çok sayıda kod yazdım ve birden fazla istisna da atan birçok kütüphaneden yararlandım. Ayrıca, istisnaların ele alınmasına ilişkin kitaplarda / makalelerde, atılan istisnaların sayısını sınırlama konusu genellikle gündeme gelmez. Ancak, örneklerin birçoğu, uygulama için gizli bir onay veren birden fazla fırlatma / yakalamayı göstermektedir. Bu yüzden, kuralı şaşırtıcı buldum ve istisnaların ele alınmasının en iyi uygulamaları / felsefesi üzerine sadece temel nasıl yapılır örnekleri üzerine daha fazla araştırma yapmak istedim.
sdoca

Yanıtlar:


32

Sağlanan kodun bulunduğu durumu düşünelim:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

Buradaki tehlike, aramak için yazdığınız kodun şöyle delete()görünmesidir:

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

Bu da kötü. Ve istisna sınıfını temel alan bayraklara işaret eden başka bir kuralla yakalanacak.

Anahtar, başka yerde kötü kod yazmak istemenize neden olan kod yazmamaktır.

Karşılaştığınız kural oldukça yaygın bir kuraldır. Checkstyle tasarım kurallarına sahiptir:

ThrowsCount

Kısıtlamalar ifadeleri belirtilen bir sayıya atar (varsayılan olarak 1).

Gerekçe: İstisnalar, bir yöntemin arayüzünün bir parçasını oluşturur. Çok fazla farklı köklü istisnalar atmak için bir yöntem belirtmek istisnaları istisna hale getirir ve catch gibi kod yazma gibi kötü programlama uygulamalarına yol açar (İstisna ex). Bu kontrol, geliştiricilerin istisnaları bir hiyerarşiye koymaya zorlar; öyle ki en basit durumda, yalnızca bir istisna türünün bir arayan tarafından kontrol edilmesi gerekir, ancak gerekirse herhangi bir alt sınıfın yakalanması gerekir.

Bu tam olarak sorunu ve sorunun ne olduğunu ve neden yapmamanız gerektiğini açıklar. Birçok statik analiz aracının tanımlayacağı ve işaretleyeceği iyi kabul edilmiş bir standarttır.

Eğer ederken olabilecek dil tasarımına göre bunu, ve orada olabilir bunu yapmak için doğru şey olduğunda kat, bunu görüp hemen dönmeli şeydir "um, bunu neden yapıyorum?" Herkesin asla yetmeyeceği kadar disiplinli olduğu iç kurallar için kabul edilebilir catch (Exception e) {}, ancak daha çok, insanların özellikle iç ortamlarda köşelerini kestiklerini görmediğimden daha sık.

Sınıfınızı kullanan kişilerin kötü kod yazmak istememelerini sağlayın.


Bunun öneminin Java SE 7 ve sonrasında azaltıldığını belirtmeliyim, çünkü tek bir catch ifadesi birden fazla istisna yakalayabilir ( Birden Fazla İstisna Türünü Yakalama ve Oracle'dan Geliştirilmiş Tip Denetimi ile İstisnaları Yeniden İnceleme ).

Java 6 ve önceki sürümlerde, şuna benzeyen bir kodunuz olurdu:

public void delete() throws IOException, SQLException {
  /* ... */
}

ve

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

veya

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

Java 6 ile bu seçeneklerin hiçbiri ideal değil. Birinci yaklaşım ihlal KURU . Birden fazla blok aynı şeyi tekrar tekrar yapıyor - her istisna için bir kez. İstisnayı günlüğe kaydetmek ve yeniden açmak ister misiniz? Tamam. Her istisna için aynı kod satırı.

İkinci seçenek birkaç nedenden dolayı daha kötü. İlk olarak, tüm istisnaları yakaladığınız anlamına gelir. Boş gösterici orada yakalanır (ve olmamalıdır). Ayrıca, bir yeniden atma vardır Exceptionyöntem imzası olacağını hangi araçlar deleteSomething() throws Exceptionsadece şimdi vardır kodunuzu kullanan kişiler olarak yığını daha yukarı bir karışıklık yapar zorla etmek catch(Exception e).

Java 7 ile bu kadar önemli değildir çünkü bunun yerine:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

Dahası, biri olmadığını kontrol tipi gelmez istisnalar türlerini yakalamak atılan:

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

Tip denetleyicisi bunun yalnızca veya türünde eolabileceğini tanır . Bu tarzın kullanımı konusunda hala çok fazla hevesli değilim, ancak Java 6'da olduğu gibi kötü bir koda neden olmuyor (burada, yöntem imzasını istisnaların genişlettiği üst sınıf olmaya zorlar).IOExceptionSQLException

Tüm bu değişikliklere rağmen, birçok statik analiz aracı (Sonar, PMD, Checkstyle) hala Java 6 stil rehberlerini uygulamaktadır. Bu kötü bir şey değil. Bunların hala uygulanmaları yönünde bir uyarıda bulunma eğilimindeyim , ancak ekibinizin önceliklerine göre onlara göre önceliği büyük veya küçük olarak değiştirebilirsiniz.

İstisnalar kontrol veya kontrolsüz şekilde olursa ... o meselesidir g r e bir t tartışmayı kolayca Tartışmanın her tarafını kaplıyor sayısız blog postaları bulabilir. Ancak, işaretli istisnalar ile çalışıyorsanız, en azından Java 6'nın altında birden fazla tür atmaktan kaçınmanız gerekir.


Bunu iyi kabul edilmiş bir standart olduğu konusundaki soruma cevap olarak kabul etmek. Bununla birlikte, standardın ne olacağını belirlemek için cevabında verilen Panzercrisis bağlantısıyla başlayan çeşitli pro / con tartışmaları okuyorum.
sdoca

“Tip denetleyicisi, e'nin yalnızca IOException veya SQLException tipinde olabileceğini kabul edecektir.”: Bu ne anlama geliyor? Başka bir türün istisnası atıldığında ne olur foo.delete()? Hala yakalandı mı ve yeniden düştü mü?
Giorgio

@Giorgio Bu deleteörnekte IOException veya SQLException dışında işaretli bir istisna atarsanız derleme zamanı hatası olur . Yapmaya çalıştığım kilit nokta, rethrowException'ı çağıran bir yöntemin Java 7'deki Özel Durum türünü almaya devam edeceğidir. Java 6'da, bunların tümü Exceptionstatik analizi ve diğer kodlayıcıları üzen ortak türle birleştirilir .

Anlıyorum. Bana biraz sarsılmış görünüyor. Bunu yasaklamayı catch (Exception e)ve onu ya catch (IOException e)da catch (SQLException e)onun yerine olmaya zorlamayı daha sezgisel buluyorum .
Giorgio

@Giorgio İyi kod yazmayı kolaylaştırmak için Java 6'dan ileriye doğru artan bir adımdır. Ne yazık ki, hatalı kod yazma seçeneği uzun süre bizimle olacak. catch(IOException|SQLException ex)Bunun yerine Java 7'nin yapabileceğini unutmayın . Ancak, yalnızca istisnayı yeniden yazacaksanız, tür denetleyicisinin kodu basitleştirmek için istisna bilgisinin gerçek türünü yaymasına izin vermek kötü bir şey değildir.

22

İdeal olarak, yalnızca bir istisna türü atmak istemesinin nedeni, aksi takdirde muhtemelen Tek Sorumluluk ve Bağımlılık Tersine Çevirme İlkelerini ihlal ettiğidir . Göstermek için bir örnek kullanalım.

Diyelim ki, kalıcılıktan veri alan bir metod var ve bu kalıcılık bir dizi dosya. Dosyalarla ilgilendiğimiz için şunlara sahip olabiliriz FileNotFoundException:

public String getData(int id) throws FileNotFoundException

Şimdi gereksinimlerimizde değişiklik var ve verilerimiz bir veritabanından geliyor. Bir yerine FileNotFoundException(dosyalarla ilgilenmediğimiz için), şimdi şunu atarız SQLException:

public String getData(int id) throws SQLException

Şimdi, yöntemimizi kullanan tüm kodları gözden geçirmemiz ve kontrol etmek zorunda olduğumuz istisnayı değiştirmemiz gerekir, aksi halde kod derlenmez. Metodumuz uzak ve geniş olarak çağrılırsa, bu değişmesi / başkalarının değişmesi çok olabilir. Çok zaman alıyor ve insanlar mutlu olmayacak.

Bağımlılık inversiyonu, bu istisnaların hiçbirini atmamamız gerektiğini, çünkü kapsüllemek için çalıştığımız iç uygulama ayrıntılarını ortaya koyuyorlar. Arama kodunun ne tür bir kalıcılık kullandığımızı bilmesi gerekir, ne zaman kaydın alınabileceği konusunda gerçekten endişelenmeniz gerekir. Bunun yerine, hatayı API'mız aracılığıyla ortaya çıkardığımız aynı soyutlama düzeyinde ileten bir istisna atmalıyız:

public String getData(int id) throws InvalidRecordException

Şimdi, eğer içsel uygulamayı değiştirirsek, bu istisnayı basitçe bir araya getirip InvalidRecordExceptiongeçebiliriz (ya da sarmaz ve sadece yeni bir şey atarız InvalidRecordException). Dış kod, ne tür bir ısrarın kullanıldığını bilmiyor veya umursamıyor. Hepsi kapsüllenmiş.


Tek Sorumluluk'a gelince, birbiriyle alakasız istisnalar ortaya çıkaran kodu düşünmemiz gerekir. Diyelim ki aşağıdaki yöntemimiz var:

public Record parseFile(String filename) throws IOException, ParseException

Bu yöntem hakkında ne söyleyebiliriz? Biz sadece bir dosya açılır imza söyleyebilirim ve bunu ayrıştırır. Bir yöntemin tanımındaki "ve" veya "veya" gibi bir bağlantı gördüğümüzde, bunun birden fazla şey yaptığını biliyoruz; birden fazla sorumluluğu var . Birden fazla sorumluluğu olan yöntemlerin, sorumluluklardan herhangi birinin değişmesi durumunda değişebilecekleri için yönetmeleri zordur. Bunun yerine, metotları dağıtmalıyız, böylece tek bir sorumluluğu var:

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

Verileri ayrıştırma sorumluluğundan dosyayı okuma sorumluluğunu aldık. Bellek içi Ayrıca test edebilirsiniz vb dosyası, ağ, Bu bir yan etkisi Artık herhangi bir kaynaktan ayrıştırma verilere herhangi Dize verilerinde geçebilir olmasıdır parseşimdi daha kolay biz diskte bir dosya gerekmez çünkü karşı testler yapmak.


Bazen bir yöntemden atabileceğimiz gerçekten iki (veya daha fazla) istisna vardır, ancak SRP ve DIP'ye bağlı kalırsak, bu durumla karşılaştığımız zamanlar daha nadir hale gelir.


Örneğinize göre daha düşük seviyedeki istisnaları tamamıyla kabul ediyorum. Bunu düzenli olarak yapıyoruz ve MyAppExceptions'ın değişikliklerini yapıyoruz. Birden fazla istisna attığım örneklerden biri veritabanındaki bir kaydı güncellemeye çalışmaktır. Bu yöntem bir RecordNotFoundException atar. Ancak, kayıt yalnızca belirli bir durumda olduğunda güncellenebilir, bu nedenle yöntem aynı zamanda bir InvalidRecordStateException atar. Bunun geçerli olduğunu ve arayan kişiye değerli bilgiler sağladığını düşünüyorum.
sdoca

@sdoca Eğer updateyönteminiz yapabildiğiniz kadar atomikse ve istisnalar uygun bir soyutlama seviyesindeyse, evet, iki istisnai durum olduğundan iki farklı istisna türü atmanız gerekecek gibi görünüyor. Bu, (bazen keyfi) linter kurallarından ziyade kaç istisna atılabileceğinin ölçüsü olmalıdır.
cbojar

2
Ancak bir akıştan veri okuyan ve devam ederken ayrıştıran bir yöntemim varsa, bu iki işlevi tüm akışı okumadan tuhaf bir belleğe okumadan tutamayacağım, tuhaf olabilir. Ayrıca, istisnaları nasıl doğru şekilde kullanacağınıza karar veren kod, okuma ve ayrıştırma işlemini yapan koddan ayrı olabilir. Okuma ve ayrıştırma kodunu yazarken, kodumu çağıran kodun her iki İstisna türünü nasıl ele almak isteyebileceğini bilmiyorum, bu yüzden ikisinin de geçmesine izin vermem gerekiyor.
user3294068

+1: Bu cevabı çok seviyorum, çünkü sorunu modelleme açısından ele alıyor. Genellikle başka bir deyim kullanmak gerekmez, catch (IOException | SQLException ex)çünkü asıl sorun program modelinde / tasarımındadır.
Giorgio

3

Bir süre önce Java ile oynarken biraz bununla uğraştığımı hatırlıyorum, ancak sorunuzu okuyana kadar kontrol edilmiş ve kontrol edilmemiş arasındaki farkın farkında değildim. Bu makaleyi Google’da oldukça hızlı bir şekilde buldum ve bazı belirgin tartışmalara girdim:

http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

Bu adamın işaretli istisnalar dışında bahsettiği konulardan biri, throwsyalnızca yöntem beyanlarınızdaki maddelere ek olarak bir dizi işaretli istisna eklemeye devam ederseniz (ve bunu kişisel olarak Java ile baştan başlattım). Daha yüksek seviyeli yöntemlere geçerken onu desteklemek için daha fazla kazan kodu koymak zorunda mıyım, ancak daha düşük seviyedeki yöntemlere daha fazla istisna türü sunmaya çalıştığınızda sadece daha büyük bir karışıklık yaratır ve uyumluluğu bozar mı? Alt düzey bir yönteme işaretli bir istisna türü eklerseniz, kodunuzu yeniden gözden geçirmeniz ve diğer birkaç yöntem bildirimini ayarlamanız gerekir.

Makalede belirtilen bir azaltma noktası - ve yazar bunu kişisel olarak beğenmedi - bir temel sınıf istisnası yaratmak, throwscümlelerini yalnızca kullanmak için sınırlandırmak ve sonra da içeride alt sınıfları yükseltmek oldu. Bu şekilde, tüm kodlarınızı yeniden kullanmanıza gerek kalmadan yeni kontrol edilen istisna türleri oluşturabilirsiniz.

Bu makalenin yazarı bu kadar hoşlanmamış olabilir, ancak kişisel deneyimimde (özellikle de tüm alt sınıfların ne olduğunu bulabiliyorsanız) mükemmel bir anlam ifade ediyor ve bahsettiğiniz tavsiyenin bu yüzden size verildiğine bahse girerim her birini işaretli istisnalara göre doldurun. Dahası, bahsettiğiniz tavsiyenin, halka açık olmayan yöntemlerde çoklu kontrol istisna türlerine izin vermesidir; Bu sadece özel bir yöntem veya zaten benzer bir şeyse, küçük bir şeyi değiştirdiğinizde kod tabanınızın yarısını geçemezsiniz.

Bunun kabul edilebilir bir standart olup olmadığını sordunuz, ancak bahsettiğiniz araştırmanız arasında, bu makul düşünülmüş makale ve sadece kişisel programlama deneyiminden bahsederken, kesinlikle hiçbir şekilde öne çıkmadığı görülüyor.


2
Neden Throwabletüm kendi hiyerarşinizi icat etmek yerine, sadece atmak ve atmak istemiyorsunuz?
Deduplicator

@Deduplicator Yazarın da fikri beğenmemesinin nedeni bu; Bunu yapacaksanız, kontrol edilmemiş tüm kullanımları kullanabileceğinizi düşündü. Ancak, API'yi kim kullanıyorsa (muhtemelen iş arkadaşınız), temel sınıfınızdan türetilen tüm alt sınıflanmış istisnaların bir listesi varsa, en azından beklenen tüm istisnaların olduğunu bildirmek için bir miktar yarar görüyorum. belirli bir alt sınıf kümesi içinde; öyleyse, onlardan birinin diğerlerinden daha "elle tutulabilir" olduğunu hissederlerse, kendisine özgü bir işleyiciden vazgeçmeye meyilli olmazlardı.
Panzercrisis

Genel olarak kontrol edilen istisnaların nedeni kötü karma basittir: Viral, her kullanıcıyı etkiliyor. Kasıtlı olarak en geniş mümkün şartname belirtilmesi, tüm istisnaların işaretli olmadığını söylemenin ve böylece karışıklığı önlemenin bir yoludur. Evet, ele almak isteyebileceğiniz şeylerin belgelenmesi iyi bir fikirdir, belgeler için : Hangi fonksiyonun bir fonksiyondan gelebileceğini bilmek kesinlikle sınırlı bir değere sahiptir (hiçbiri / belki de hiçbiri dışında, fakat Java buna izin vermez) .
Deduplicator

1
@Deduplicator Bu fikri onaylamıyorum ve mutlaka bunu da sevmiyorum. Ben sadece OP'nin verdiği tavsiyenin nereden geldiği yönünden bahsediyorum.
Panzercrisis

1
Bağlantı için teşekkürler. Bu konuda okuma yapmak için harika bir başlangıç ​​yeriydi.
sdoca

-1

Birden fazla kontrol edilen istisna dışında atmak, yapılacak birden fazla makul şeyin olması mantıklıdır.

Örneğin, yönteminiz olduğunu söyleyelim

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

bu durum istisna kuralını ihlal ediyor, ancak mantıklı geliyor.

Maalesef, genelde olan biten gibi yöntemler

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

Burada, arayan kişinin istisna türüne göre belirli bir şey yapması için çok az şans vardır. Bu yüzden onu, bu yöntemlerin CAN ve bazen yanlış gideceğini fark etmesi için dürtmek istiyorsak, sadece SomethingMightGoWrongException'ı atlatmak çok ve daha iyidir.

Bu nedenle, kural en çok bir tane istisnai durum kontrol edildi.

Ancak, projeniz çok sayıda anlamlı kontrol istisnasının olduğu yerlerde tasarım kullanıyorsa, bu kural geçerli olmamalıdır.

Sidenote: Bir şey aslında hemen hemen her yerde ters gidebilir, bu yüzden kullanmayı düşünebilir misiniz? RuntimeException'ı genişletiyor, ancak "hepimiz hata yapıyoruz" ile "bu dış sistemden bahsediyor ve bazen işe yarayacak" ile başa çıkmak arasında bir fark var.


1
"Birden çok makul şeyler yapmak" pek uygun SRP - Bu noktada ortaya atılmıştır önce cevap
tatarcık

Öyle. Bir işlevde bir sorunu (bir yönü ele alırken) diğerinde de başka bir sorunu da (bir yönü ele alır) bir işlevi ele alır, yani bu iki işlevi çağırırsınız. Ve arayan, bir denemeyi yakalama katmanında (bir işlevde) bir sorunu HANDLE edebilir, bir başkasını yukarı doğru geçirebilir ve onu tek başına ele alabilir.
user470365
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.