Изключенията са толкова стари, колкото самото програмиране. В дните, когато програмирането се извършваше в хардуер или чрез езици за програмиране на ниско ниво, бяха използвани изключения за промяна на потока на програмата и за избягване на хардуерни грешки. Днес Уикипедия дефинира изключения като:
аномални или изключителни условия, изискващи специална обработка - често променящи нормалния поток на изпълнение на програмата ...
И това, че боравенето с тях изисква:
специализирани конструкции на езика за програмиране или компютърни хардуерни механизми.
Така че изключенията изискват специално третиране, а необработеното изключение може да причини неочаквано поведение. Резултатите често са грандиозни. През 1996 г. известният Неуспех при изстрелването на ракета Ariane 5 бе приписано на необработено изключение за преливане. Най-лошите софтуерни грешки в историята съдържа някои други грешки, които биха могли да бъдат приписани на необработени или неправилно обработени изключения.
С течение на времето тези грешки и безброй други (които може би бяха не толкова драматични, но все пак катастрофални за участниците) допринесоха за впечатлението, че изключенията са лоши .
Но изключенията са основен елемент на съвременното програмиране; те съществуват, за да направят нашия софтуер по-добър. Вместо да се страхуваме от изключения, трябва да ги прегърнем и да се научим как да се възползваме от тях. В тази статия ще обсъдим как да управляваме елегантно изключенията и да ги използваме, за да напишем чист код, който е по-поддържаем.
С възхода на обектно-ориентирано програмиране (OOP), подкрепата за изключения се превърна в ключов елемент на съвременните езици за програмиране. Днес в повечето езици е вградена стабилна система за обработка на изключения. Например, Ruby предоставя следния типичен модел:
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
Няма нищо лошо в предишния код. Но прекаленото използване на тези модели ще предизвика миризми на кода и не е задължително да е от полза. По същия начин, злоупотребата с тях всъщност може да навреди много на вашата кодова основа, да я направи крехка или да замъгли причината за грешките.
Стигмата около изключенията често кара програмистите да се чувстват загубени. Факт от живота е, че изключенията не могат да бъдат избегнати, но често ни учат, че трябва да се справят бързо и решително. Както ще видим, това не е непременно вярно. По-скоро трябва да се научим на изкуството да боравим изящно с изключенията, като ги правим хармонични с останалата част от нашия код.
Следват някои препоръчани практики, които ще ви помогнат да приемете изключенията и да се възползвате от тях и техните способности, за да запазите кода си поддържаема , разтегателен , и четлив :
Тези елементи са основните фактори на това, което бихме могли да наречем чистота или качество , което не е пряка мярка, а вместо това е комбинираният ефект от предишните точки, както е показано в този комикс:
С това казано, нека да се потопим в тези практики и да видим как всяка от тях влияе на тези три мерки.
Забележка: Ще представим примери от Ruby, но всички демонстрирани тук конструкции имат еквиваленти в най-често срещаните OOP езици.
ApplicationError
йерархияПовечето езици идват с различни класове изключения, организирани в йерархия на наследяване, както всеки друг клас на OOP. За да запазим четливостта, поддръжката и разширяемостта на нашия код, е добра идея да създадем наше собствено поддърво от специфични за приложението изключения, които разширяват базовия клас изключения. Инвестирането на известно време в логическо структуриране на тази йерархия може да бъде изключително полезно. Например:
class ApplicationError 
Наличието на разширяващ се, изчерпателен пакет от изключения за нашето приложение прави справянето с тези специфични за приложението ситуации много по-лесно. Например можем да решим кои изключения да обработим по по-естествен начин. Това не само повишава четливостта на нашия код, но също така увеличава поддръжката на нашите приложения и библиотеки (скъпоценни камъни).
От гледна точка на четливостта е много по-лесно да се чете:
rescue ValidationError => e
След това да прочетете:
rescue RequiredFieldError, UniqueFieldError, ... => e
От гледна точка на поддръжката, да речем, например, ние прилагаме JSON API и сме дефинирали нашите собствени ClientError
с няколко подтипа, които да се използват, когато клиент изпрати лоша заявка. Ако някой от тях е повдигнат, приложението трябва да представи JSON представяне на грешката в своя отговор. Ще бъде по-лесно да коригирате или добавите логика към един блок, който обработва ClientError
s, вместо да прелиствате всяка възможна грешка на клиента и да прилагате един и същ код на манипулатора за всеки. По отношение на разширяемостта, ако по-късно се наложи да внедрим друг тип грешка на клиента, можем да се доверим, че тук вече ще се работи правилно.
Освен това, това не ни пречи да внедрим допълнителна специална обработка за конкретни клиентски грешки по-рано в стека на повикванията или да променим същия обект на изключение по пътя:
# 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
Както можете да видите, повишаването на това конкретно изключение не ни попречи да можем да се справяме с него на различни нива, да го променяме, да го повдигаме отново и да позволяваме на манипулатора на родителски клас да го разрешава.
Тук трябва да обърнете внимание на две неща:
- Не всички езици поддържат повишаване на изключения от манипулатора на изключения.
- В повечето езици повдигането на a ново изключение от манипулатор ще доведе до загуба на оригиналното изключение завинаги, така че е по-добре да повдигнете отново същия обект на изключение (както в горния пример), за да избегнете загуба на следа от първоначалната причина за грешката. (Освен ако не правите това преднамерено ).
Никога rescue Exception
Тоест, никога не се опитвайте да приложите манипулатор за всички прихващания за базовия тип изключение. Спасяването или улавянето на всички изключения на едро е никога добра идея на всеки език, независимо дали е в световен мащаб на базово ниво на приложение, или в малък погребан метод, използван само веднъж. Не искаме да спасяваме Exception
защото ще замъгли всичко, което наистина се е случило, увреждайки както поддръжката, така и разтегливостта. Можем да загубим огромно количество време за отстраняване на грешки какъв е действителният проблем, когато това може да бъде толкова просто, колкото синтаксисната грешка:
# 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
Може да сте забелязали грешката в предишния пример; return
е въведен погрешно. Въпреки че съвременните редактори осигуряват известна защита срещу този специфичен тип синтаксична грешка, този пример илюстрира как rescue Exception
вреди на нашия код. В нито един момент не е адресиран действителният тип изключение (в този случай NoMethodError
), нито някога е изложено на разработчика, което може да ни накара да губим много време в кръгове.
Никога rescue
повече изключения, отколкото трябва
Предишната точка е конкретен случай на това правило: Винаги трябва да внимаваме да не прекалено обобщаваме нашите манипулатори на изключения. Причините са едни и същи; винаги, когато спасяваме повече изключения, отколкото трябва, в крайна сметка скриваме части от логиката на приложението от по-високите нива на приложението, да не говорим за потискане на способността на разработчика да се справи сам с изключението. Това сериозно засяга разширяемостта и поддръжката на кода.
Ако се опитаме да обработим различни подтипове изключения в един и същ манипулатор, ние въвеждаме блокове с дебел код, които имат твърде много отговорности. Например, ако изграждаме библиотека, която консумира отдалечен API, обработвайки MethodNotAllowedError
(HTTP 405), обикновено се различава от обработката на UnauthorizedError
(HTTP 401), въпреки че и двамата са ResponseError
s.
Както ще видим, често съществува различна част от приложението, която би била по-подходяща за обработка на конкретни изключения в повече СУХА начин.
Така че, дефинирайте една отговорност на вашия клас или метод и да се справят с минималния брой изключения, които отговарят на това изискване за отговорност . Например, ако методът е отговорен за получаване на информация за запасите от отдалечен API, той трябва да обработва изключения, възникващи от получаването само на тази информация, и да остави обработката на останалите грешки на различен метод, създаден специално за тези отговорности:
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
Тук дефинирахме договора за този метод, за да получим само информацията за запасите. Справя се специфични за крайната точка грешки , като непълен или деформиран JSON отговор. Не се справя със случая, когато удостоверяването е неуспешно или изтича, или ако наличността не съществува. Това е отговорност на някой друг и изрично се предава в стека на повикванията, където трябва да има по-добро място за обработка на тези грешки по СУХ начин.
Устояйте на желанието да се справите незабавно с изключенията
Това е допълнението към последната точка. Изключение може да бъде обработено във всяка точка в стека на повикванията и всяка точка в йерархията на класовете, така че знанието къде точно да се обработи може да бъде загадъчно. За да разрешат тази загадка, много разработчици избират да се справят с всяко изключение веднага щом възникне, но инвестирането на време в това обмисляне обикновено води до намиране на по-подходящо място за справяне с конкретни изключения.
Един често срещан модел, който виждаме в приложенията Rails ( особено тези, които излагат само JSON API ) е следният метод на контролера:
# 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
(Обърнете внимание, че въпреки че това технически не е манипулатор на изключения, функционално той служи на същата цел, тъй като @client.save
връща false само когато срещне изключение.)
В този случай обаче повтарянето на един и същ манипулатор на грешки във всяко действие на контролера е противоположно на DRY и уврежда поддръжката и разширяемостта. Вместо това можем да се възползваме от специалния характер на разпространението на изключения и да се справим с тях само веднъж, в клас родителски контролер , ApplicationController
:
# 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
По този начин можем да гарантираме, че всички ActiveRecord::RecordInvalid
грешките се обработват правилно и СУХО на едно място, на основата ApplicationController
ниво. Това ни дава свободата да се занимаваме с тях, ако искаме да разглеждаме конкретни случаи на по-ниско ниво, или просто да ги оставим да се разпространяват грациозно.
Не всички изключения се нуждаят от обработка
Когато разработват скъпоценен камък или библиотека, много разработчици ще се опитат да капсулират функционалността и да не позволят на никакви изключения да се разпространяват извън библиотеката. Но понякога не е очевидно как да се справим с изключение, докато конкретното приложение не бъде внедрено.
Да вземем ActiveRecord
като пример за идеалното решение. Библиотеката предоставя на разработчиците два подхода за пълнота. The save
метод обработва изключения, без да ги разпространява, просто връщайки false
, докато save!
създава изключение, когато не успее. Това дава възможност на разработчиците да се справят по различен начин с конкретни случаи на грешки или просто да се справят с някаква грешка по общ начин.
Но какво, ако нямате време или ресурси да осигурите такова цялостно изпълнение? В такъв случай, ако има някаква несигурност, най-добре е изложи изключението , и го пуснете в дивата природа.
Ето защо: Работим с движещи се изисквания почти през цялото време и вземането на решение, че дадено изключение винаги ще се обработва по специфичен начин, може действително да навреди на изпълнението ни, да навреди на разширяемостта и поддръжката и потенциално да добави огромни технически дълг , особено при разработване на библиотеки.
Вземете по-ранния пример за потребител на API за акции, който извлича цени на акциите. Избрахме да се справим с непълния и деформиран отговор на място и избрахме да опитаме отново същата заявка, докато получим валиден отговор. Но по-късно изискванията могат да се променят, така че трябва да се върнем към запазените исторически данни за запасите, вместо да опитваме отново заявката.
На този етап ще бъдем принудени да променим самата библиотека, като актуализираме как се обработва това изключение, тъй като зависимите проекти няма да се справят с това изключение. (Как биха могли? Никога преди това не е било изложено на тях.) Също така ще трябва да информираме собствениците на проекти, които разчитат на нашата библиотека. Това може да се превърне в кошмар, ако има много такива проекти, тъй като те вероятно са били изградени въз основа на предположението, че тази грешка ще бъде обработена по специфичен начин.
Сега можем да видим накъде се насочваме с управление на зависимости. Перспективите не са добри. Тази ситуация се случва доста често и по-често влошава полезността, разширяемостта и гъвкавостта на библиотеката.
И така, тук е долният ред: ако е неясно как трябва да се обработва едно изключение, нека то се разпространява грациозно . Има много случаи, при които съществува ясно място за вътрешно обработване на изключението, но има много други случаи, при които излагането на изключението е по-добро. Така че, преди да изберете да се справите с изключението, просто го помислете. Едно добро правило е само настояват относно обработката на изключения, когато взаимодействате директно с крайния потребител.
Следвайте конвенцията
Прилагането на Ruby и, още повече, Rails, следва някои конвенции за именуване, като например разграничаване между method_names
и method_names!
с „взрив“. В Ruby взривът показва, че методът ще промени обекта, който го е извикал, а в Rails това означава, че методът ще създаде изключение, ако не успее да изпълни очакваното поведение. Опитайте се да спазвате същата конвенция, особено ако ще отворите вашата библиотека.
Ако трябваше да напишем нов method!
с гръм и трясък в приложението Rails, трябва да вземем предвид тези конвенции. Няма нищо, което да ни принуждава да правим изключение, когато този метод се провали, но като се отклони от конвенцията, този метод може да заблуди програмистите да повярват, че ще им бъде дадена възможност да се справят сами с изключенията, когато всъщност няма.
Друга конвенция на Руби, приписвана на Джим Уейрих, е да използвайте fail
за да се посочи неуспех на метода , и само за използване raise
ако повдигате повторно изключението.
Встрани, тъй като използвам изключения, за да посоча грешки, почти винаги използвам fail
ключова дума, а не raise
ключова дума в Ruby. Неуспех и повишаване са синоними, така че няма разлика, освен че неуспехът по-ясно съобщава, че методът е неуспешен. Единственият път, когато използвам рейз, е когато хващам изключение и го повдигам повторно, защото тук не успявам, а изрично и целенасочено повдигам изключение. Това е стилистичен въпрос, който следвам, но се съмнявам, че много други хора го правят.
Много други езикови общности са приели конвенции като тези около това как се третират изключенията и игнорирането на тези конвенции ще навреди на четливостта и поддръжката на нашия код.
Logger.log (всичко)
Тази практика не се отнася само за изключения, разбира се, но ако има нещо, което трябва винаги бъдете регистрирани, това е изключение.
Регистрацията е изключително важна (достатъчно важна за Ruby да изпрати a трупи със стандартната си версия). Това е дневникът на нашите приложения и дори по-важно от записването на това как нашите приложения успяват, е регистрирането как и кога те се провалят.
Не липсва регистрационни библиотеки или лог-базирани услуги и дизайнерски модели. Изключително важно е да следим нашите изключения, за да можем да прегледаме случилото се и да проучим дали нещо не изглежда както трябва. Правилните регистрационни съобщения могат да насочат разработчиците директно към причината за проблема, спестявайки им неизмеримо време.
Това доверие на чистия код
Чистата обработка на изключения ще изпрати качеството на кода ви на Луната! Tweet Изключенията са основна част от всеки език за програмиране. Те са специални и изключително мощни и ние трябва да използваме тяхната сила, за да повишим качеството на нашия код, вместо да се изтощаваме да се борим с тях.
В тази статия се потопихме в някои добри практики за структуриране на нашите дървета с изключения и как може да бъде от полза за четливостта и качеството при тяхното логическо структуриране. Разгледахме различни подходи за обработка на изключения, на едно място или на няколко нива.
Видяхме, че е лошо да ги „хванем всички“ и че е добре да ги оставим да се носят и да се издуват.
Разгледахме къде да се справим с изключенията по СУХ начин и научихме, че не сме длъжни да ги обработваме, когато или къде възникват за първи път.
Обсъдихме кога точно е добра идея да се справите с тях, когато това е лоша идея и защо, когато се съмнявате, е добра идея да ги оставите да се разпространяват.
И накрая, обсъдихме други точки, които могат да помогнат за максимизиране на полезността на изключенията, като следване на конвенции и регистриране на всичко.
С тези основни насоки можем да се чувстваме много по-комфортно и уверено да се справяме с случаи на грешки в нашия код и да правим нашите изключения наистина изключителни!
Специални благодарности на Авди грим и страхотната му реч Изключителна Ruby , което много помогна при създаването на тази статия.
Свързани: Съвети и най-добри практики за разработчиците на Ruby