Pull to refresh

«Bloated code» в коммерческой разработке ПО

Reading time3 min
Views1K
В продуктовой разработке используются согласованные внутри команды паттерны. Это не только известные паттерны проектирования, но и, например, паттерны обработки ошибок внутри системы, форматы запросов и ответов в межсистемном взаимодействии и прочее. Так же при индивидуальной разработке не все повторяющиеся по логике и структуре куски можно завернуть в методы, что тоже не способствует читабельности и простоте кода.

С ростом количества систем и их размеров при малейшем изменении паттернов приходится рефакторить кучу мест для приведения кода к заданному шаблону.

После исследования альтернативных языков на 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 [(+ 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>
Tags:
Hubs:
Total votes 21: ↑17 and ↓4+13
Comments14

Articles