⬆️ ⬇️

"Bloated code" in commercial software development

In product development, consistent patterns are used within the team. These are not only well-known design patterns, but also, for example, error-handling patterns within the system, request and response formats in intersystem interaction, and so on. Also, in case of individual development, not all chunks that repeat in logic and structure can be wrapped in methods, which also does not contribute to readability and simplicity of the code.



With the increase in the number of systems and their size with the slightest change in patterns, it is necessary to refactor a bunch of places to bring the code to a given pattern.



After exploring alternative languages ​​on jvm, we stopped at clojure. Here is a small example of implementing an error handling pattern on it.

')

Suppose we arrange error handling in Java as follows:





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 ) ) ) ;

}

}




On clojure , the macro handler-ie was implemented in the corresponding library, which implements the logic of this pattern. For example, in the middle function, we process the call to the bottom function using this macro:



( defn middle [ a b ]

( ie / handler-ie ie / system-test "middle level processing" nil [ a b ]

( let [ result ( bottom a b ) ]

result ) ) )





Let's see what the code of the function body unfolds at compile time:



( macroexpand- 1 ' ( ie / handler-ie ie / system-test "middle level processing" nil [ a b ]

( let [ result ( bottom a b ) ]

result ) ) )




Result (auto-variable suffixes and full namespaces are removed for readability):



( 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 ) ) ) ) )




It can be seen that the resulting code is a parameterized version of the Java version. Also, the advantage of the clojure version is that if you change the implementation of the error handling logic, I will not have to change the source code of the projects where this processing is used. It will be enough just to reassemble the corresponding project.



At the moment, we use macros to handle errors, generate intersystem responses and within our eDSL for working with the database. So All the repeated patterns in our projects have become just an extension of the language.



After switching to Lisp, the size of the source code has decreased by orders of magnitude. This is not only due to macros, but also because of the ability to write in a functional style using convenient universal clojure data structures, high-order functions and closures. At the same time, the possibility of working with clojure libraries, java and j2ee frameworks was not lost. And most importantly, now only the programmer can be the only reason for bydlokod.



Library example





Source Code:



( 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 ) ) ) )





Result without exception:



user > ( try ( tester 1 2 )

( catch InternalException e

( println ( ie / human-readable-ie e ) ) ) )



1/2





With 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>

Source: https://habr.com/ru/post/130323/



All Articles