Temiz Kod ve İstisna İşleme Sanatı
Yayınlanan: 2022-03-11İstisnalar, programlamanın kendisi kadar eskidir. Programlamanın donanımda veya düşük seviyeli programlama dilleri aracılığıyla yapıldığı günlerde, programın akışını değiştirmek ve donanım arızalarını önlemek için istisnalar kullanıldı. Bugün, Wikipedia istisnaları şu şekilde tanımlıyor:
özel işleme gerektiren anormal veya istisnai koşullar – genellikle program yürütmenin normal akışını değiştirir…
Ve bunları işlemek şunları gerektirir:
özel programlama dili yapıları veya bilgisayar donanım mekanizmaları.
Bu nedenle, istisnalar özel muamele gerektirir ve işlenmeyen bir istisna, beklenmeyen davranışlara neden olabilir. Sonuçlar genellikle muhteşemdir. 1996 yılında, ünlü Ariane 5 roket fırlatma başarısızlığı, işlenmeyen bir taşma istisnasına bağlandı. Tarihin En Kötü Yazılım Hataları, işlenmeyen veya yanlış işlenen istisnalara atfedilebilecek diğer bazı hataları içerir.
Zamanla, bu hatalar ve sayısız diğerleri (belki de o kadar dramatik olmasa da, ilgililer için hala felaketti), istisnaların kötü olduğu izlenimine katkıda bulundu.
Ancak istisnalar, modern programlamanın temel bir unsurudur; yazılımımızı daha iyi hale getirmek için varlar. İstisnalardan korkmak yerine onları kucaklamalı ve onlardan yararlanmayı öğrenmeliyiz. Bu makalede, istisnaların nasıl zarif bir şekilde yönetileceğini ve bunları daha sürdürülebilir temiz kod yazmak için kullanmayı tartışacağız.
İstisna İşleme: Bu İyi Bir Şey
Nesne yönelimli programlamanın (OOP) yükselişiyle birlikte, istisna desteği modern programlama dillerinin çok önemli bir unsuru haline geldi. Günümüzde çoğu dilde sağlam bir istisna işleme sistemi yerleşiktir. Örneğin, Ruby aşağıdaki tipik modeli sağlar:
begin do_something_that_might_not_work! rescue SpecificError => e do_some_specific_error_clean_up retry if some_condition_met? ensure this_will_always_be_executed end
Önceki kodda yanlış bir şey yok. Ancak bu kalıpları aşırı kullanmak kod kokularına neden olur ve mutlaka faydalı olmaz. Benzer şekilde, bunları yanlış kullanmak kod tabanınıza çok fazla zarar verebilir, onu kırılgan hale getirebilir veya hataların nedenini gizleyebilir.
İstisnaları çevreleyen damgalama, genellikle programcıların kendilerini kayıp hissetmelerine neden olur. İstisnalardan kaçınılamayacağı hayatın bir gerçeğidir, ancak genellikle bize bunların hızlı ve kararlı bir şekilde ele alınması gerektiği öğretilir. Göreceğimiz gibi, bu mutlaka doğru değildir. Bunun yerine, istisnaları incelikle ele alma sanatını öğrenmeliyiz, onları kodumuzun geri kalanıyla uyumlu hale getirmeliyiz.
Aşağıda, istisnaları benimsemenize ve kodunuzu sürdürülebilir , genişletilebilir ve okunabilir kılmak için bu istisnaları ve yeteneklerini kullanmanıza yardımcı olacak bazı önerilen uygulamalar verilmiştir:
- sürdürülebilirlik : Mevcut işlevselliği bozma, daha fazla hata ekleme veya zamanla artan karmaşıklık nedeniyle kodu tamamen terk etme korkusu olmadan yeni hataları kolayca bulmamızı ve düzeltmemizi sağlar.
- genişletilebilirlik : Mevcut işlevselliği bozmadan yeni veya değiştirilmiş gereksinimleri uygulayarak kod tabanımıza kolayca eklememizi sağlar. Genişletilebilirlik esneklik sağlar ve kod tabanımız için yüksek düzeyde yeniden kullanılabilirlik sağlar.
- okunabilirlik : Kodu kolayca okumamızı ve çok fazla zaman harcamadan amacını keşfetmemizi sağlar. Bu, hataları ve test edilmemiş kodları verimli bir şekilde keşfetmek için kritik öneme sahiptir.
Bu unsurlar, bu çizgi romanda gösterildiği gibi, doğrudan bir ölçü olmayıp, bunun yerine önceki noktaların birleşik etkisi olan temizlik veya kalite diyebileceğimiz şeyin ana faktörleridir:
Bununla birlikte, bu uygulamalara dalalım ve her birinin bu üç önlemi nasıl etkilediğini görelim.
Not: Ruby'den örnekler sunacağız, ancak burada gösterilen tüm yapıların en yaygın OOP dillerinde eşdeğerleri vardır.
Her zaman kendi ApplicationError
hiyerarşinizi oluşturun
Çoğu dil, diğer OOP sınıfları gibi, bir kalıtım hiyerarşisinde düzenlenen çeşitli istisna sınıflarıyla birlikte gelir. Kodumuzun okunabilirliğini, sürdürülebilirliğini ve genişletilebilirliğini korumak için, temel istisna sınıfını genişleten uygulamaya özel istisnalardan oluşan kendi alt ağacımızı oluşturmak iyi bir fikirdir. Bu hiyerarşiyi mantıksal olarak yapılandırmak için biraz zaman ayırmak son derece faydalı olabilir. Örneğin:
class ApplicationError < StandardError; end # Validation Errors class ValidationError < ApplicationError; end class RequiredFieldError < ValidationError; end class UniqueFieldError < ValidationError; end # HTTP 4XX Response Errors class ResponseError < ApplicationError; end class BadRequestError < ResponseError; end class UnauthorizedError < ResponseError; end # ...
Uygulamamız için genişletilebilir, kapsamlı bir istisna paketine sahip olmak, uygulamaya özel bu durumların ele alınmasını çok daha kolay hale getiriyor. Örneğin, hangi istisnaları daha doğal bir şekilde ele alacağımıza karar verebiliriz. Bu, yalnızca kodumuzun okunabilirliğini artırmakla kalmaz, aynı zamanda uygulamalarımızın ve kitaplıklarımızın (mücevherler) sürdürülebilirliğini de artırır.
Okunabilirlik açısından okunması çok daha kolay:
rescue ValidationError => e
Okumaktansa:
rescue RequiredFieldError, UniqueFieldError, ... => e
Sürdürülebilirlik açısından, örneğin, bir JSON API uyguluyoruz ve bir istemci hatalı bir istek gönderdiğinde kullanılmak üzere kendi ClientError
birkaç alt türle tanımladık. Bunlardan herhangi biri ortaya çıkarsa, uygulama yanıtında hatanın JSON temsilini oluşturmalıdır. Her olası istemci hatası üzerinde döngü yapmak ve her biri için aynı işleyici kodunu uygulamak yerine, ClientError
ları işleyen tek bir bloğu düzeltmek veya bu bloğa mantık eklemek daha kolay olacaktır. Genişletilebilirlik açısından, daha sonra başka bir istemci hatası türü uygulamak zorunda kalırsak, bunun burada zaten düzgün bir şekilde ele alınacağına güvenebiliriz.
Ayrıca, bu, çağrı yığınında daha önce belirli istemci hataları için ek özel işleme uygulamamızı veya yol boyunca aynı istisna nesnesini değiştirmemizi engellemez:
# app/controller/pseudo_controller.rb def authenticate_user! fail AuthenticationError if token_invalid? || token_expired? User.find_by(authentication_token: token) rescue AuthenticationError => e report_suspicious_activity if token_invalid? raise e end def show authenticate_user! show_private_stuff!(params[:id]) rescue ClientError => e render_error(e) end
Gördüğünüz gibi, bu özel istisnayı yükseltmek, onu farklı düzeylerde işlememizi, değiştirmemizi, yeniden yükseltmemizi ve üst sınıf işleyicisinin onu çözmesine izin vermemizi engellemedi.
Burada dikkat edilmesi gereken iki şey:
- Tüm diller, bir özel durum işleyici içinden özel durum oluşturmayı desteklemez.
- Çoğu dilde, bir işleyiciden yeni bir istisna oluşturmak, orijinal istisnanın sonsuza kadar kaybolmasına neden olur, bu nedenle orijinal sebebin izini kaybetmemek için aynı istisna nesnesini (yukarıdaki örnekte olduğu gibi) yeniden yükseltmek daha iyidir. hata. (Bunu kasıtlı olarak yapmıyorsanız).
İstisnayı asla rescue Exception
Diğer bir deyişle, temel istisna türü için hiçbir zaman bir tümünü yakalama işleyicisi uygulamaya çalışmayın. Tüm istisnaları toptan kurtarmak veya yakalamak, küresel olarak temel uygulama düzeyinde veya yalnızca bir kez kullanılan küçük gömülü bir yöntemde olsun, hiçbir dilde asla iyi bir fikir değildir. Exception
kurtarmak istemiyoruz çünkü gerçekten olan her şeyi karartacak, hem sürdürülebilirliğe hem de genişletilebilirliğe zarar verecek. Bir sözdizimi hatası kadar basit olabileceği zaman, asıl sorunun ne olduğunu hata ayıklamak için çok fazla zaman harcayabiliriz:
# main.rb def bad_example i_might_raise_exception! rescue Exception nah_i_will_always_be_here_for_you end # elsewhere.rb def i_might_raise_exception! retrun do_a_lot_of_work! end
Önceki örnekteki hatayı fark etmiş olabilirsiniz; return
yanlış yazılmış. Modern düzenleyiciler bu tür sözdizimi hatasına karşı bir miktar koruma sağlasa da, bu örnek, rescue Exception
kodumuza nasıl zarar verdiğini göstermektedir. Hiçbir noktada istisnanın gerçek türü (bu durumda bir NoMethodError
) ele alınmaz ve geliştiriciye hiçbir zaman açıklanmaz, bu da daireler çizerek çok fazla zaman kaybetmemize neden olabilir.
Gerektiğinden daha fazla istisnayı asla rescue
Önceki nokta, bu kuralın özel bir durumudur: Her zaman istisna işleyicilerimizi aşırı genelleştirmemeye dikkat etmeliyiz. Nedenler aynı; Ne zaman yapmamız gerekenden daha fazla istisna kurtarırsak, uygulama mantığının bölümlerini uygulamanın daha yüksek seviyelerinden gizleriz, geliştiricinin istisnayı kendi başına ele alma yeteneğini bastırmaktan bahsetmeye gerek yok. Bu, kodun genişletilebilirliğini ve sürdürülebilirliğini ciddi şekilde etkiler.
Aynı işleyicide farklı istisna alt türlerini işlemeye çalışırsak, çok fazla sorumluluğu olan yağ kod blokları sunarız. Örneğin, uzak bir API kullanan bir kitaplık oluşturuyorsak, MethodNotAllowedError
(HTTP 405) işlemek, her ikisi de ResponseError
s olmalarına rağmen genellikle bir UnauthorizedError
(HTTP 401) işlemekten farklıdır.
Göreceğimiz gibi, genellikle uygulamanın belirli istisnaları daha KURU bir şekilde ele almak için daha uygun olan farklı bir bölümü vardır.
Bu nedenle, sınıfınızın veya yönteminizin tek sorumluluğunu tanımlayın ve bu sorumluluk gereksinimini karşılayan minimum istisnaları ele alın . Örneğin, uzak bir API'den stok bilgisi almaktan bir yöntem sorumluysa, yalnızca bu bilgiyi almaktan kaynaklanan istisnaları ele almalı ve diğer hataların işlenmesini özellikle bu sorumluluklar için tasarlanmış farklı bir yönteme bırakmalıdır:

def get_info begin response = HTTP.get(STOCKS_URL + "#{@symbol}/info") fail AuthenticationError if response.code == 401 fail StockNotFoundError, @symbol if response.code == 404 return JSON.parse response.body rescue JSON::ParserError retry end end
Burada bize sadece hisse senedi hakkında bilgi almak için bu yöntemin sözleşmesini tanımladık. Eksik veya hatalı biçimlendirilmiş JSON yanıtı gibi uç noktaya özgü hataları işler. Kimlik doğrulama başarısız olduğunda veya sona erdiğinde veya stok mevcut olmadığında durumu ele almaz. Bunlar başka birinin sorumluluğundadır ve bu hataları KURU bir şekilde ele almak için daha iyi bir yer olması gereken çağrı yığınından açıkça geçirilir.
İstisnaları hemen ele alma dürtüsüne karşı koyun
Bu, son noktanın tamamlayıcısıdır. Bir istisna, çağrı yığınının herhangi bir noktasında ve sınıf hiyerarşisindeki herhangi bir noktada işlenebilir, bu nedenle tam olarak nerede ele alınacağını bilmek şaşırtıcı olabilir. Bu bilmeceyi çözmek için, birçok geliştirici herhangi bir istisnayı ortaya çıkar çıkmaz ele almayı tercih eder, ancak bunu düşünmek için zaman ayırmak, genellikle belirli istisnaları ele almak için daha uygun bir yer bulmakla sonuçlanacaktır.
Rails uygulamalarında (özellikle yalnızca JSON API'lerini ortaya çıkaranlarda) gördüğümüz yaygın bir kalıp, aşağıdaki denetleyici yöntemidir:
# app/controllers/client_controller.rb def create @client = Client.new(params[:client]) if @client.save render json: @client else render json: @client.errors end end
(Bunun teknik olarak bir istisna işleyicisi olmamasına rağmen, işlevsel olarak aynı amaca hizmet ettiğini unutmayın, çünkü @client.save
yalnızca bir istisna ile karşılaştığında false döndürür.)
Ancak bu durumda, her denetleyici eyleminde aynı hata işleyiciyi tekrarlamak, DRY'nin tersidir ve sürdürülebilirliğe ve genişletilebilirliğe zarar verir. Bunun yerine, istisna yayılımının özel doğasını kullanabilir ve bunları yalnızca bir kez üst denetleyici sınıfında, ApplicationController
işleyebiliriz:
# app/controllers/client_controller.rb def create @client = Client.create!(params[:client]) render json: @client end
# app/controller/application_controller.rb rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity def render_unprocessable_entity(e) render \ json: { errors: e.record.errors }, status: 422 end
Bu şekilde, tüm ActiveRecord::RecordInvalid
hatalarının düzgün ve KURU olarak tek bir yerde, temel ApplicationController
düzeyinde işlenmesini sağlayabiliriz. Bu, belirli vakaları daha düşük düzeyde ele almak istiyorsak veya basitçe onların zarif bir şekilde yayılmasına izin vermek istiyorsak, onlarla oynama özgürlüğü verir.
Tüm istisnaların işlenmesi gerekmez
Bir mücevher veya kitaplık geliştirirken, birçok geliştirici işlevselliği kapsüllemeye çalışacak ve herhangi bir istisnanın kitaplıktan dışarı yayılmasına izin vermeyecektir. Ancak bazen, belirli bir uygulama uygulanana kadar bir istisnanın nasıl ele alınacağı açık değildir.
İdeal çözüme örnek olarak ActiveRecord
alalım. Kitaplık, geliştiricilere eksiksizlik için iki yaklaşım sağlar. save
yöntemi, istisnaları yaymadan işler, kaydederken false
döndürmesi yeterlidir save!
başarısız olduğunda bir istisna oluşturur. Bu, geliştiricilere belirli hata durumlarını farklı şekilde ele alma veya herhangi bir hatayı genel bir şekilde ele alma seçeneği sunar.
Peki ya bu kadar eksiksiz bir uygulama sağlamak için zamanınız veya kaynaklarınız yoksa? Bu durumda, herhangi bir belirsizlik varsa , istisnayı ortaya çıkarmak ve onu vahşi doğaya bırakmak en iyisidir.
İşte nedeni: Neredeyse her zaman taşıma gereksinimleriyle çalışıyoruz ve bir istisnanın her zaman belirli bir şekilde ele alınacağına karar vermek, uygulamamıza gerçekten zarar verebilir, genişletilebilirliğe ve sürdürülebilirliğe zarar verebilir ve özellikle geliştirme sırasında potansiyel olarak büyük teknik borçlar ekleyebilir. kütüphaneler.
Hisse senedi fiyatlarını getiren bir hisse senedi API tüketicisinin önceki örneğini alın. Eksik ve hatalı biçimlendirilmiş yanıtı yerinde işlemeyi seçtik ve geçerli bir yanıt alana kadar aynı isteği yeniden denemeyi seçtik. Ancak daha sonra gereksinimler değişebilir, öyle ki talebi yeniden denemek yerine kayıtlı geçmiş stok verilerine geri dönmemiz gerekir.
Bu noktada, bağımlı projeler bu istisnayı işlemeyeceğinden, bu istisnanın nasıl ele alındığını güncelleyerek kitaplığın kendisini değiştirmek zorunda kalacağız. (Nasıl yapabilirler? Daha önce onlara hiç maruz kalmamıştı.) Kütüphanemize güvenen proje sahiplerini de bilgilendirmemiz gerekecek. Bu tür birçok proje varsa, bu hatanın belirli bir şekilde ele alınacağı varsayımı üzerine inşa edilmiş olmaları muhtemel olduğundan, bu bir kabus olabilir.
Artık bağımlılık yönetimi ile nereye gittiğimizi görebiliriz. Görünüm iyi değil. Bu durum oldukça sık meydana gelir ve çoğu zaman kütüphanenin kullanışlılığını, genişletilebilirliğini ve esnekliğini düşürür.
Sonuç olarak işte burada: Bir istisnanın nasıl ele alınması gerektiği açık değilse, bırakın zarafetle yayılmasına izin verin . İstisnayı dahili olarak ele almak için net bir yerin bulunduğu birçok durum vardır, ancak istisnayı ortaya çıkarmanın daha iyi olduğu başka birçok durum vardır. Bu nedenle, istisnayı ele almayı seçmeden önce, bir kez daha düşünün. İyi bir kural, yalnızca son kullanıcıyla doğrudan etkileşimde bulunduğunuzda istisnaları ele almakta ısrar etmektir.
Sözleşmeyi takip edin
Ruby'nin ve hatta dahası Rails'in uygulanması, method_names
ve method_names!
bir "patlama" ile. Ruby'de patlama, yöntemin kendisini çağıran nesneyi değiştireceğini ve Rails'de, beklenen davranışı gerçekleştiremezse yöntemin bir istisna oluşturacağı anlamına gelir. Özellikle kitaplığınızı açık kaynaklı hale getirecekseniz, aynı sözleşmeye uymaya çalışın.
Yeni bir method!
Bir Rails uygulamasında bir patlama ile bu sözleşmeleri hesaba katmalıyız. Bu yöntem başarısız olduğunda bizi bir istisna oluşturmaya zorlayan hiçbir şey yoktur, ancak bu yöntem, konvansiyondan saparak, programcıları, istisnaları kendileri ele alma şansının kendilerine verileceğine, aslında vermeyeceklerine inanmaya yönlendirebilir.
Jim Weirich'e atfedilen başka bir Ruby kuralı, yöntem başarısızlığını belirtmek için fail
kullanmak ve yalnızca istisnayı yeniden yükseltiyorsanız raise
kullanmaktır.
Bir kenara, hataları belirtmek için istisnalar kullandığım için, Ruby'de hemen hemen her zaman
raise
anahtar sözcüğü yerinefail
anahtar sözcüğünü kullanırım. Başarısız ve yükseltme eşanlamlıdır, bu nedenle başarısızlığın yöntemin başarısız olduğunu daha açık bir şekilde bildirmesi dışında hiçbir fark yoktur. Yükseltmeyi kullandığım tek zaman, bir istisna yakalayıp yeniden yükselttiğim zamandır, çünkü burada başarısız olmuyorum, açıkça ve kasıtlı olarak bir istisna yükseltiyorum. Bu benim takip ettiğim bir stil sorunu ama diğer birçok insanın yaptığından şüpheliyim.
Diğer birçok dil topluluğu, istisnaların nasıl ele alındığına ilişkin bunun gibi kuralları benimsemiştir ve bu kuralların yok sayılması, kodumuzun okunabilirliğine ve sürdürülebilirliğine zarar verecektir.
Logger.log(her şey)
Bu uygulama elbette yalnızca istisnalar için geçerli değildir, ancak her zaman kaydedilmesi gereken bir şey varsa, o da bir istisnadır.
Günlük kaydı son derece önemlidir (Ruby'nin standart sürümüyle bir kaydedici göndermesi için yeterince önemlidir). Bu, uygulamalarımızın günlüğüdür ve uygulamalarımızın nasıl başarılı olduğunun bir kaydını tutmaktan daha da önemlisi, nasıl ve ne zaman başarısız olduklarını günlüğe kaydetmektir.
Günlük kitaplıklarında veya günlük tabanlı hizmetlerde ve tasarım modellerinde herhangi bir eksiklik yoktur. İstisnalarımızı takip etmek çok önemlidir, böylece neler olduğunu gözden geçirebilir ve bir şeylerin doğru görünüp görünmediğini araştırabiliriz. Uygun günlük mesajları, geliştiricileri doğrudan bir sorunun nedenine yönlendirerek onlara ölçülemeyecek kadar zaman kazandırır.
O Temiz Kod Güveni
İstisnalar, her programlama dilinin temel bir parçasıdır. Özel ve son derece güçlüdürler ve onlarla savaşarak kendimizi tüketmek yerine kodumuzun kalitesini yükseltmek için güçlerinden yararlanmalıyız.
Bu makalede, istisna ağaçlarımızı yapılandırmak için bazı iyi uygulamalara ve bunları mantıksal olarak yapılandırmanın okunabilirlik ve kalite açısından nasıl faydalı olabileceğine değindik. İstisnaları tek bir yerde veya birden çok düzeyde ele almak için farklı yaklaşımlara baktık.
“Hepsini yakalamanın” kötü olduğunu ve onların havada süzülmelerine ve kabarcıklanmalarına izin vermenin sorun olmadığını gördük.
İstisnaları DRY olarak nerede ele alacağımıza baktık ve ilk ortaya çıktıklarında veya nerede onları ele almak zorunda olmadığımızı öğrendik.
Bunları ele almanın tam olarak ne zaman iyi bir fikir olduğunu, ne zaman kötü bir fikir olduğunu ve neden şüpheye düştüğünüzde bunların yayılmasına izin vermenin iyi bir fikir olduğunu tartıştık.
Son olarak, kuralları takip etmek ve her şeyi günlüğe kaydetmek gibi istisnaların faydasını en üst düzeye çıkarmaya yardımcı olabilecek diğer noktaları tartıştık.
Bu temel yönergelerle, kodumuzdaki hata durumlarıyla başa çıkmak ve istisnalarımızı gerçekten istisnai kılmak için çok daha rahat ve kendinden emin hissedebiliriz!
Avdi Grimm'e ve bu makalenin hazırlanmasında çok yardımcı olan harika konuşması Exceptional Ruby'ye özel teşekkürler.