Pull to refresh

Коды возврата vs исключения: взгляд с колокольни

Reading time4 min
Views2.5K
Просмотрев пост Коды возврата vs исключения и комментарии к нему, я заметил, что в обсуждении упущена одна нить, краткий тезис которой следующий: в некоторых языках такая проблема даже не стоИт, т.к. вопрос «что выбрать, коды возврата или исключения» в таком языке является низкоуровневым. Как, например, не стоит вопрос, каким образом реализовать конструкцию «foreach». Т.к. для программиста, использующего тот же «foreach», нет никакой разницы, использовали ли создатели языка while или for или что-то еще в имплементации данного оператора. Главное это паттерн, который представляет собой этот самый оператор.

Хватит рассуждать про foreach. Покажу непосредственно на примере два очень похожих друг на друга оператора, один из которых использует в качестве реализации «исключения», другой — «коды возврата».

process-exception [mapping & body]
, где mapping — map вида {исключение 1 <-> метод обработки 1, исключение 2 <-> метод обработки 2, ...}, body — тело оператора, в котором может возникнуть исключение.

и второй оператор:
process-retcode-answer [mapping & body]
, где mapping — map вида {код возврата 1 <-> метод обработки 1, код возврата 2 <-> метод обработки 2, ...}, body — тело оператора, которое заканчивается ответом подпрограммы или какой-либо вызываемой системы, который нужно обработать на основе кода возврата.

Посмотрим, они работают.

process-retcode-answer



Допустим у нас есть функции обработки кодов возврата 0, -1, -2 и логика обработки остальных кодов:

(defn ok-processor [result]
  (println (str "ok. result: " result)))
 
(defn error-processor [result]
  (println (str "error. result: " result)))
 
(defn another-error-processor [result]
  (println (str "another error. result: " result)))
 
(defn unknown-error-processor [result]
  (println (str "unknown error. result: " result)))


Определяем map кодов возврата на наименования обрабатывающих их функций:

(def result-mapping {0 'ok-processor
                     -1 'error-processor
                     -2 'another-error-processor
                     :other 'unknown-error-processor})
 


Теперь создаем тестовые подпрограммы, возвращающие разные коды возврата и соответствующий результат:

(defn test-call-ok []
  [0 "test result"])
 
(defn test-call-error []
  [-1 "test result"])
 
(defn test-call-another-error []
  [-2 "test result"])
 
(defn test-call-unknown-error []
  [-1000 "test result"])


Работа нашего оператора в данном случае выглядит так:

(process-retcode-answer result-mapping (test-call-ok))
ok. result: test result
 
(process-retcode-answer result-mapping (test-call-error))
error. result: test result
 
(process-retcode-answer result-mapping (test-call-another-error))
another error. result: test result
 
(process-retcode-answer result-mapping (test-call-unknown-error))
unknown error. result: test result


Здесь в каждое тело состоит только из одного метода. В реальности можно вместо него вставить любую последовательность функций.

Преимущество данного оператора состоит в том, что обработка новых кодов или изменение обработчиков существующих, осуществляется прозрачно путем реализации обработчиков и внесением соответсвующих изменений в mapping, который может находиться в отдельном файле.

Никаких if-ов. Данный подход реализует достаточно гибкий паттерн обработки кодов возврата.

process-exception



По аналогии с предыдущим примером. Есть функции обработки некоторых исключений:

(defn arithmetic-exception-processor [e]
  (println (str "Arithmetic exception.")))
 
(defn nullpointer-exception-processor [e]
  (println (str "Nullpointer exception.")))
 
(defn another-exception-processor [e]
  (println (str "Other exception.")))


Map исключений на наименования обрабатывающих их функций:

(def exception-mapping {java.lang.ArithmeticException 'arithmetic-exception-processor
                        java.lang.NullPointerException 'nullpointer-exception-processor
                        java.lang.Exception 'another-exception-processor})


Создаем тестовые подпрограммы, генерирующие разные исключения:

(defn test-call-ok []
  "test result")
 
(defn test-throw-arithmetic-exception []
  (throw (new java.lang.ArithmeticException))
  "test resutl")
 
(defn test-throw-nullpointer-exception []
  (throw (new java.lang.NullPointerException))
  "test resutl")
 
(defn test-throw-other-exception []
  (throw (new java.lang.ClassNotFoundException))
  "test resutl")


Работа нашего оператора:

(process-exception exception-mapping
                   (test-call-ok))
"test result"
 
(process-exception exception-mapping
                   (test-throw-arithmetic-exception))
Arithmetic exception.
 
(process-exception exception-mapping
                   (test-throw-nullpointer-exception))
Nullpointer exception.
 
(process-exception exception-mapping
                   (test-throw-other-exception))
Other exception.


Замечания в конце описания предыдущего оператора применимы и здесь.

Выводы



Данные операторы очень похожи и в принципе реализуют один и тот же паттерн обработки ответа, но с разной реализацией. Здесь я предпочтение отдаю process-retcode-answer. Хотя в других языках варианты с кодами возврата не всегда выгодны в сравнении с вариантами, использующих исключения (конечно, зависит от условий задачи и самого языка — это уже обсудили).

Вот вариант реализации вышеописанных операторов:

(defmacro process-exception [mapping & body]
  (let [catch-items (map (fn [m]
                           `(catch ~(first m) e#
                              (~(eval (second m)) e#)))
                         (eval mapping))]
    `(try ~@body
          ~@catch-items)))


(defmacro process-retcode-answer [mapping & body]
  `(let [answer# (do ~@body)
         retcode# (first answer#)
         result# (second answer#)
         processor# (get ~mapping retcode#)
         processor# (if (nil? processor#) (:other ~mapping) processor#)]
     ((eval processor#) result#)))


Этот довольно утрированный пример показывает, что базовые элементы языка не сильно важны, как возможность расширять язык новыми конструкциями и также иметь в нем те же функции высокого порядка, лямбды, замыкания. Такой язык позволяет программисту стать художником. Он не пишет паттерны снова и снова. Он просто «создает» язык, в котором может наиболее естественно, наглядно и кратко сформулировать решение поставленной перед ним задачи. Профессия становится не ремеслом, а искусством.
Tags:
Hubs:
Total votes 40: ↑29 and ↓11+18
Comments23

Articles