В продуктовой разработке используются согласованные внутри команды паттерны. Это не только известные паттерны проектирования, но и, например, паттерны обработки ошибок внутри системы, форматы запросов и ответов в межсистемном взаимодействии и прочее. Так же при индивидуальной разработке не все повторяющиеся по логике и структуре куски можно завернуть в методы, что тоже не способствует читабельности и простоте кода.
С ростом количества систем и их размеров при малейшем изменении паттернов приходится рефакторить кучу мест для приведения кода к заданному шаблону.
После исследования альтернативных языков на jvm мы остановились на clojure. Вот небольшой пример реализации паттерна обработки ошибки на нем.
Допустим, мы оформляем обработку ошибок в джаве следующим образом:
На clojure был реализован макрос handler-ie в соответсвующей библиотеке, который имплементирует логику данного паттерна. Например, в функции middle обрабатываем вызов функции bottom c помощью данного макроса:
Посмотрим, во что разворачивается код тела функции во время компиляции:
Результат (суффиксы у автопеременных и полные неймспейсы убраны для читабельности):
Видно, что получившийся код является параметризированным аналогом джава версии. Также преимущество clojure версии заключается в том, что при изменении реализации логики обработки ошибки мне не придется изменять исходный код проектов, где данная обработка используется. Достаточно будет просто пересобрать соотвествующий проект.
На данный момент мы используем макросы для обработки ошибок, формирования межсистемных ответов и внутри нашего eDSL по работе с БД. Т.о. все повторяющиеся паттерны в наших проектах стали просто расширением языка.
После переходе на лисп размер исходного кода у нас уменшьшился на порядки. Это не только благодаря макросам, но и из-за возможности писать в функциональном стиле с использованием удобных универсальных структур данных clojure, функций высокого порядка и замыканий. При этом не потерялась возможность работы clojure с библиотеками, фреймворком java и j2ee. И самое главное, теперь единственной причиной быдлокода может быть только сам программист.
Исходый код:
Результат без exception-а:
С exception-ом:
С ростом количества систем и их размеров при малейшем изменении паттернов приходится рефакторить кучу мест для приведения кода к заданному шаблону.
После исследования альтернативных языков на jvm мы остановились на clojure. Вот небольшой пример реализации паттерна обработки ошибки на нем.
Допустим, мы оформляем обработку ошибок в джаве следующим образом:
try {
... work …
} catch (InternalException e) {
exceptionHelper.addGroup(e, TestTest.class.getName(), "... error ...",
new Pair("param1", param1);
new Pair("param2", param2));
throw e;
} catch (Exception e) {
throw exceptionHelper.generate(ErrorRef.SYSTEM_ERROR, "... error ...",
new Pair("param1", param1);
new Pair("param2", param2);
new Pair("exception", MySerialization.exceptionToString(e)));
}
}
На clojure был реализован макрос handler-ie в соответсвующей библиотеке, который имплементирует логику данного паттерна. Например, в функции middle обрабатываем вызов функции bottom c помощью данного макроса:
(defn middle [a b]
(ie/handler-ie ie/system-test "middle level processing" nil [a b]
(let [result (bottom a b)]
result)))
Посмотрим, во что разворачивается код тела функции во время компиляции:
(macroexpand-1 '(ie/handler-ie ie/system-test "middle level processing" nil [a b]
(let [result (bottom a b)]
result)))
Результат (суффиксы у автопеременных и полные неймспейсы убраны для читабельности):
(let [params (reverse (zipmap (map str '[a b])
[a b]))
error-place (current-function-name)]
(try (let [result (bottom a b)]
result)
(catch errors.entities.internalexception e
(add-group-to-ie e error-place "middle level processing" params)
(throw e))
(catch java.lang.exception e
(let [error-ref (error-map (.getname (.getclass e)))
error-ref (if (nil? error-ref)
(make-system-error-ref system-test errorref-system-error)
error-ref)
ie (make-ie error-ref error-place "middle level processing" e params)]
(throw ie)))))
Видно, что получившийся код является параметризированным аналогом джава версии. Также преимущество clojure версии заключается в том, что при изменении реализации логики обработки ошибки мне не придется изменять исходный код проектов, где данная обработка используется. Достаточно будет просто пересобрать соотвествующий проект.
На данный момент мы используем макросы для обработки ошибок, формирования межсистемных ответов и внутри нашего eDSL по работе с БД. Т.о. все повторяющиеся паттерны в наших проектах стали просто расширением языка.
После переходе на лисп размер исходного кода у нас уменшьшился на порядки. Это не только благодаря макросам, но и из-за возможности писать в функциональном стиле с использованием удобных универсальных структур данных clojure, функций высокого порядка и замыканий. При этом не потерялась возможность работы clojure с библиотеками, фреймворком java и j2ee. И самое главное, теперь единственной причиной быдлокода может быть только сам программист.
Пример работы библиотеки
Исходый код:
(defn bottom [a b]
(ie/handler-ie ie/system-test "low level operation"
{"java.lang.ArithmeticException" TestSystemEM$ErrorRef/BAD_ARGS}
[a b]
(let [result (/ a b)]
result)))
(defn middle [a b]
(ie/handler-ie ie/system-test "middle level processing" nil [a b]
(let [result (bottom a b)]
result)))
(defn tester [a b]
(let [c (+ a b)]
(ie/handler-ie ie/system-test "public interface action" nil [a b c]
(let [result (middle a b)]
result))))
Результат без exception-а:
user> (try (tester 1 2)
(catch InternalException e
(println (ie/human-readable-ie e))))
1/2
С exception-ом:
user> (try (tester 1 0)
(catch InternalException e
(println (ie/human-readable-ie e))))
ErrorRef: BAD_ARGS
Group parameters:
className: libs.error.example$tester
message: public interface action
parameters:
name: a, value: <int>1</int>
name: b, value: <int>0</int>
name: c, value: <int>1</int>
Group parameters:
className: libs.error.example$middle
message: middle level processing
parameters:
name: a, value: <int>1</int>
name: b, value: <int>0</int>
Group parameters:
className: libs.error.example$bottom
message: low level operation
parameters:
name: b, value: <int>0</int>
name: a, value: <int>1</int>
name: Exception, value: <java.lang.String>Error message: Divide by zero
java.lang.ArithmeticException: Divide by zero
at clojure.lang.Numbers.divide(Numbers.java:138)
at libs.error.example$bottom.invoke(example.clj:12)
at libs.error.example$middle.invoke(example.clj:17)
at libs.error.example$tester.invoke(example.clj:23)
…
</java.lang.String>