And I'm liking it. A lot. Possibly because of the way we're using it (microservices), but probably just intrinsically, it is a language that seems to fit in the head very nicely. Not encumbered by special cases, exceptions, implicit magic and overloads. (Don't worry, I still enjoy Scala, but it's a very different Kettle[Either[Throwable, Map[String, Fish]]]).
The succinctness and elegance of Clojure is also thrown into sharp relief by the other thing I seem to be spending a lot of time on at work - grinding through a multiple-hundred-thousand-line instant-legacy untested Java codebase. This thing might have been considered state-of-the-art ten years ago when it was all about 3-tiered systems putting messages on busses - iff it had been implemented nicely, but it wasn't. As a result, it's a monolithic proliferation of POJO-manipulation, with control flow by exceptions, mutable state throughout, and impossible to test in isolation.
It can take hours to find code that actually "does something", but you have to follow the path(s) all the way down from "the top" just in case there's a bug or "hidden feature" somewhere on the way through the myriad layers with methods that look like this (anonymised somewhat):
public ListThis is everywhere. The lines that get me most annoyed are things like this:getAllFoo(Integer primaryId, Short secondaryId, String detail, Locale locale, String timeZone, String category) { if (category != null) { Map foosMap = ParameterConstants.foosMap; if (foosMap != null) { category = (foosMap.get(category.toUpperCase()) != null) ? foosMap.get(category.toUpperCase()) : category; } } List values = new ArrayList (); FooValue searchValue = new FooValue(); List fooValues = null; searchValue.setPrimaryID(primaryId); searchValue.setSecondaryId(secondaryId); searchValue.setCategory(category); try { LOGGER.info(CommonAPILoggingConstants.INF_JOBTYPE_GETALL_VALIDATION_COMPLETED); fooValues = fooDAO.getFoos(searchValue, detail); } catch (FooValidationException e) { handleException(e.getErrorId(), e); } catch (Exception e) { throw new InternalAPIException(UNKNOWN_CODE, e); } if (FULL.equalsIgnoreCase(detail)) { for (FooValue fooValue : fooValues) { Bar bar = null; try { if (StringUtils.isNotBlank(fooValue.getBarID())) { bar = barDAO.getBarByBarId(fooValue.getBarID()); fooValue.setBarName(bar.getBarName()); fooValue.setBarShortName(bar.getShortName()); LOGGER.debug(CommonAPILoggingConstants.DBG_JOBTYPE_GETALL_FETCH_BAR_BY_ID, bar.getBarName(),fooValue.getBarID()); } } catch (Exception e) { throw new InternalAPIException(UNKNOWN_CODE, e); } try { if (null != bar) { if (StringUtils.isNotBlank(bar.getBrandID())) { fooValue.setBazID(bar.getBazID()); Baz baz = bazDAO.getBazByBazId(fooValue.getBazID()); LOGGER.debug(CommonAPILoggingConstants.DBG_JOBTYPE_GETALL_FETCH_BAZ, baz.getName(),fooValue.getBazID()); fooValue.setBazName(baz.getName()); } } } catch (Exception e) { throw new InternalAPIException(UNKNOWN_CODE, e); } FooValue value = filterFooDetails(fooValue); values.add(value); } } else if (BASIC.equalsIgnoreCase(detail)) { for (FooValue fooValue : fooValues) { FooValue value = new FooValue(); value.setFooID(fooValue.getFooID()); value.setJobName(fooValue.getJobName()); value.setContentTypeName(fooValue.getContentTypeName()); value.setCategory(fooValue.getCategory()); value.setIsOneToMany(fooValue.getIsOneToMany()); values.add(value); } } else { throw new CommonAPIException(INVALID_DETAIL_PARAM,"Detail parameter value invalid"); } return values; }
fooValue.setBarName(bar.getBarName()); fooValue.setBarShortName(bar.getShortName());These x.setFoo(y.getFoo()) stanzas can go on for tens of lines. I haven't come across a name for them, so I'll call them POJO Shuffles. They suck the will-to-live out of anyone who has to navigate them as they frequently contain misalignments, micro-adjustments and hard-coding e.g.:
fooValue.setBarName(bar.getBazName()); fooValue.setBarShortName("Shortname: " + bar.getShortName()); fooValue.setBarLongName(bar.getShortName().toUpperCase());Did you notice:
- We're actually getting bazName from bar - almost certainly an autocomplete fail, but perhaps not?
- The "short name" of fooValue will actually be longer than in the source object. Is that important to something?
- There's a potential NullPointerException when we innocently try and set the "long name" of the fooValue
Then I read this gem of a paragraph from Rich Hickey, which is merely an introduction to the usage of defrecord in the official Clojure documentation, and yet reads like poetry when you've just come from code like the above:
It ends up that classes in most OO programs fall into two distinct categories: those classes that are artifacts of the implementation/programming domain, e.g. String or collection classes, or Clojure's reference types; and classes that represent application domain information, e.g. Employee, PurchaseOrder etc. It has always been an unfortunate characteristic of using classes for application domain information that it resulted in information being hidden behind class-specific micro-languages, e.g. even the seemingly harmless employee.getName() is a custom interface to data. Putting information in such classes is a problem, much like having every book being written in a different language would be a problem. You can no longer take a generic approach to information processing. This results in an explosion of needless specificity, and a dearth of reuse.