JAX London Blog

What I learned about Java from Clojure

Insights from the other side

16 May 2022

It's 2012: Curiosity lands on Mars, Windows 8 is released, the first part of "The Hobbit" hits theaters, the Beastie Boys break up, and Germany once again fails to win the European Championship. Excluding the Mars landing, this is (subjectively) a year full of disappointments. Apparently…

That same year, I learned a lot about software development with the not-so-old Java 7 and Java EE 6 at my job. So, while I was busy with application servers, EJB, JPA, and JSF, and adding the “Gang of Four” patterns to my bag of tricks, I happened to stumble across Clojure on a forum – a JVM-compatible language that seemed to think very little of my painstakingly learned OOP and Java skills: no static typing, no classes, immutable data structures, or functions at the heart of the language. After digesting the initial culture shock, I became increasingly involved with the language concepts in my private work. I noticed that some problems, but not all, could be solved better in Clojure. This was not always due to just the language itself, but also its idiomatic approach. Over the last few years, some of these paradigms seeped into my Java code and greatly shaped my image of what is good, maintainable code. In this article, I would like to highlight some of these concepts. Of course, the goal isn’t writing Java code that only Clojure developers can understand, or deviating completely from Java’s paradigms. No Clojure knowledge is needed to follow this article. 

Avoiding mutable state

When getting started with Clojure, you will quickly notice that state is handled much differently in the application than in Java. This is caused by Clojure’s immutable data types. If a function is applied to them, it returns a new data structure and leaves the originally passed one unchanged. The example in Listing 1 shows that the data structure behind the symbol my-vector cannot be manipulated afterward. The increment is applied to each element of the vector in the second statement, resulting in a new data structure. Therefore, developers can pass a data structure to further functions at any time, without hesitation. You can be sure that no operation will manipulate it. Changing this passed data has several disadvantages: Parallelized execution of code is more difficult as the risk of Race conditions cannot be ruled out. After all, no one can say which called code modifies the object and when. A customized code point could suddenly cause errors in a completely different part of the code without developers expecting it. Rich Hickey, the inventor of Clojure, addressed this problem in detail at Java One in his talk “Clojure Made Simple” [1]. Your first step on the way to a more robust data model is abandoning an anemic data model like Java Beans, where each field has getters and setters. The Square class in Listing 2 is an example of an immutable data structure in Java. Leaving Reflection aside, once an object of this class is created, it cannot be changed.

Listing 1

; Assign a vector to the symbol my-vector

(def my-vector [1 2 3 4 5])

 

; Apply the inc function to each element

(map inc my-vector)

-> [2 3 4 5 6]

 

; Output of the original vectors

my-vector

-> [1 2 3 4 5]

Listing 2

class Square {

 

  private final int x;

  private final int y;

 

  Square(int x, int y) {

    this.x = x;

    this.y = y;

  }

 

  public int getX() {

    return x;

  }

 

  public int getY() {

    return y;

  }

 

}

In Java, code can be written defensively, but it’s not always possible to prevent an object from being manipulated later in your work. Nested object structures and collections offer a lot of room for (unintentional) state manipulation of an object. Ultimately, it only helps determine the team’s discipline and the use of libraries and frameworks that do not manipulate transferred objects. There are still libraries and frameworks that expect the Java Bean structure, in terms of mapping, persistence, or data serialization.

STAY TUNED!

Learn more about JAX London

Concatenate calls, don’t nest them

In enterprise Java projects, the concept of layer architecture is widespread. The idea behind this is to structure classes in the project according to their technical affiliation. Classes only get access to the next layer to be used in order to keep dependencies at a minimum. Usually, the reference to the responsible object is passed to the next layer with the dependency injection, and is then used in the program’s code. A simple example of this procedure can be found in Listing 3. There is no comparable concept in Clojure. But in my early days, I tried to reproduce this layered model first (Listing 4) and fell into the same trap that layered architecture does in Java. Neither the Java variant shown here or the associated Clojure version can be tested as a standalone unit without a mocking library. In a unit test, only the smallest possible functionality should be tested, and never the storage mechanism, under any circumstances – especially not if a database is required. However, accessing another namespace cannot really be mocked in Clojure, it is similar to calling a static method in Java. One solution could be including the memory function as a parameter in the call. This way, you could swap/mock the memory function in the test and test the logic. This sounds a lot like the Single Responsibility Principle from object orientation and should not actually be a big discovery. But in my opinion, it is largely lost in the standard layer architectures. Reducing the size of units in Java code to have more methods free of side effects makes testing much easier, reduces the use of mocks, and decouples the code.

Listing 3

public class PersonService {

 

  private PersonRepository personRepository;

 

  public PersonService(PersonRepository personRepository) {

    this.personRepository = personRepository;

  }

  

  public void saveWithUpdateDate(Person person) {

    person.setUpdateDate(LocalDateTime.now());

    personRepository.save(person);

  }

 

  …

}

Listing 4

;; Definition of a namespace, referencing of another namespace Namespaces ;; with the alias ‘db’

(ns myapp.service.person

  (:require [myapp.db.person :as db]))

 

;; Example 1: Not a good idea

;; Definition of a function with the name ‘save-with-update-date’ and ;; the parameter ‘person’

(defn save-with-update-date! [person]

  (db/save-person! 

    (assoc person :update-date (java.time.LocalDateTime/now)))) 

;assoc associates the date value with the key :update-date to ‘person’

 

;; Example 2: Better, but not great

(defn save-with-update-date! [person save-function]

  (save-function

    (assoc person :update-date (java.time.LocalDateTime/now))))

 

 

;; Example 3: Separate side effect and action

(ns myapp.othernamespace.person

  (:require [myapp.service.person :as service]

    [myapp.db.person :as db]))

 

(db/save-person! (service/update-date person))

Objects do not always have to be passed

A Java method that expects a certain object type is inevitably coupled to this type. So far, so logical. If you want to be able to pass different types to this method, you can imagine that they implement a common interface (Listing 5). Although, this interface couples all the types it implements together to some degree. So, Developers make the decision that these classes have something in common and must go along with future changes to that interface. A “person” can appear in different class contexts – and these contexts can be decoupled from one another. Would it really make sense to put a common interface on the classes after decoupling? 

Meet our Java Speakers

We can discuss one potential answer to this problem by looking at it from the perspective of Clojure. Subsets of data structures – in Clojure, these are often maps – can be highlighted in a function signature with destructuring (Listing 6). The render-address function uses associative destructuring in its parameter list, indicated by :keys. If a map is passed to the function as a parameter, the value after the keyword :name is bound to the symbol name, :street to street, and so on. This means that the function works with any data structure that contains these keys – even in different contexts of the application. No coupling or higher-level data structure is introduced for these data structures to suggest that they belong together. As you can see in the bottom method signature in Listing 5, this cannot be conveniently implemented in Java. Decomposing the object that is passed in before the method call can quickly get out of hand and make code confusing. Limiting the parameter list can be a viable way to make a method usable in different contexts without coupling. This would be much easier if Java offered destructuring. TypeScript and Kotlin show that it’s possible in statically typed languages. However, taking a look at Java’s roadmap, it doesn’t seem so unlikely that the proposals for pattern matching could be extended to general destructuring in the near future. 

Listing 5

// Only works with class Person

public String renderAddress(Person person) {

  return String.format(“%s\n %s \n %s %s”, person.getName(), person.getStreet(), person.getZipCode(), person.getCity());

}

 

// Definition of a common Interface

public class Person implements Address {

 … 

}

 

// Any type that implements the interface can be passed in

public String renderAddress(Address address) {

 …

}

 

// is more awkward, but more versatile without making assumptions about the type //

public String renderAddress(String name, String street, String zipCode, String city) {

 …

}

Listing 6

;; Example of associative destructuring

(defn render-address [{:keys [name street zipCode city]]

  (format “%s \n %s \n %s %s” name street zipCode city))

 

;; The excess values are ignored

(render-address {:name “Harry Hirsch”

                 :street “Hirsch Street 4”

                 :zipCode “35781”

                 :city “Hirschhausen”

                 :age 41

                 :height 181}

 

YOU LOVE JAVA?

Explore the Java Core Track

Libraries and Templates instead of Frameworks

After familiarizing myself with the basics of Clojure, I set about creating my first web application. I quickly realized that a worry-free framework in the style of Spring, Quarkus, the Play Framework, or even JEE did not exist. In fact, it seems common to combine different libraries into one application. What at first sounds tedious and a lot of work has a big advantage: A library does not interfere with the structure of the code, but a framework sometimes does. There is no configuration by convention, no annotations are expected at certain points in the code, and developers are not forced into an anemic data model just because they use a framework that expects a class to have complete getters and setters. So, they always have a free hand when structuring their program. In general, it is common to create larger systems from several small subsystems in Clojure. So after a while, it seems natural not to span any overarching concepts over the code. So that you don’t spend your first two weeks of programming just compiling and wiring these libraries, there are different template mechanisms similar to the Spring Boot Starter. If you want to create an application with the Luminus Stack [2] (which is called a framework, but is actually a collection of libraries), you can specify which libraries you want to use when you create a new project. They will be generated as code in your new project. You can choose from different HTTP servers (Jetty, http-kit, Undertow), persistence services (H2, MySQL, PostgreSQL, MongoDB, Datomic), front-end technologies, and test tools. Boundaries between the libraries are not blurred, so it’s easy to replace individual components, even  in strongly grown code bases without damaging other parts that aren’t directly affected. Applications built this way also lose a large part of their “magic”. The logic is entirely in the code and not in configurations, aspects, and automatically generated proxy classes.

In the Java world, there are technologies that can be used in a similar way. For example, Eclipse vert.x [3] sees itself as a Toolkit – instead of a framework – and offers developers similar degrees of freedom in structuring their program. On their website, vert.x also provides a templating mechanism that allows you to select components in the stack. At the same time, it’s clear that the common, opinionated Java frameworks are justified. You can save a lot of development time with the proven interlocking of components. The predefined structures fit well for many applications and architectures.

Summary

Clojure’s greatest strengths can’t be transferred to Java without any restrictions. After all, it is a different language with different paradigms. Your enthusiasm for Clojure should not spill over into your own Java code so much that Java developers are surprised by the code and concepts. For example, no one would seriously suggest that Java developers only use maps instead of objects and that they should start putting all logic into static methods.  Nevertheless, Clojure’s methodologies have proven themselves and can help Java developers look at their own code from a slightly different angle and break long-used, well-worn patterns. The suggestions I presented here – avoidance of anemic data models, single responsibility principle, decoupling of data in different domains – are by no means unique selling points of Clojure. They are also seen as good practices in the pure Java world. Still, you can see that in many larger Java projects, they often aren’t used because of predefined frameworks. You can sharpen your own view by looking outside of the box at a language that specifies these principles through its design.

 

Links & Literature

[1] https://www.youtube.com/watch?v=VSdnJDO-xdg

 

[2] https://github.com/luminus-framework

 

[3] https://vertx.io

Behind the Tracks

Software Architecture & Design
Software innovation & more
Microservices
Architecture structure & more
Agile & Communication
Methodologies & more
Emerging Technologies
Everything about the latest technologies
DevOps & Continuous Delivery
Delivery Pipelines, Testing & more
Cloud & Modern Infrastructure
Everything about new tools and platforms
Big Data & Machine Learning
Saving, processing & more