Franco Battaglia

A case against Java or bad OOP

In this note I'll cover the most frustrating things I've encountered while developing with Java, and to a very lesser extent, OOP and DDD in general. Keep in mind this article is heavily subjective, born from experience, and not fact-based.

Besides, I'll try my best to include a potential solution to each problem I describe: maybe using OOP, or maybe ditching OOP in favor of some other construct.

We all know Java isn't a cutting-edge language, and I like to think its backwards compatibility is (somewhat) related to its verbosity.

So, without any further ado, let's begin!

Domain ≠ data

One (mostly OOP) approach to software development them is called DDD, or better known as domain-driven design. DDD classifies objects as

  • Entities (state + behavior)
  • Value Objects (state)
  • Service Objects (behavior)

and separates an application in the following layers:

  • Presentation
  • Application
  • Model
  • Persistency or DAO (see impedance mismatch below)

When using DDD, encapsulation and isolation are first-class citizens. One of DDD's advantages is that modeling difficult interactions between business concerns (model layer) is made easy using a Domain Service.

This is fine in some cases, but is really isn't how most applications are later used. Applications are built around use cases (or whatever other jargon is being used at the time). While separating an application in layers is a sound idea, one must always keep track of the actual use cases.


Let's plan a mock application, with the following requirements:

  1. It has a domain model: Students --> Subjects --> Grades
  2. It must be publicly exposed through a JSON REST API
  3. A student should be able to see his/her grades on his/her respective subjects, upon making an API call

The developer truth here is that 1 cannot remain unaware of 2. This simple 3 class domain can't be modeled and left behind, because it will have to be later coupled and adapted to comply with the JSON standard. Convenient decoupling becomes tech debt. Decoupling JSON from a domain becomes increasingly difficult and painful as the domain grows. You'll need to write a special service for this task (probaby using a custom framework like Gson), add DI, and custom JSON converter classes also have to be mantained by the developers. What about validation?

Non-domain requirements are as important as the domain itself, and the domain should be modeled according to all the requirements.

A solution using a JSON-friendly language

If we modeled the domain using, for instance, Javascript, the problem of adapting the domain into JSON would disappear completely. Why?

Because JSON is (mostly) a subset of Javascript objects, conversion is trivial. The domain now responds to the actual use cases, and many problems are avoided. Validation can be handled from the outside using a JSON validator.

For an interesting rant about Java and JSON, see this.

Impedance mismatch

As if the above problem wasn't enough, imagine a new requirement:

Persist the domain in a relational database of your choice

We'll have to add an ORM to our project, and use DAO or Repository classes to access the database. Because we have decoupled everything, this is actually going to be a problem. The term 'impedance mismatch' refers to this exact problem:

  • Objects behave like graphs and their relations are almost always one-way. Tables are algebraic constructs and their relations are almost always two-way
  • Objects are mostly embeddable, while tables seek normalization
  • Creating a DAO class for each class is actually duplicating the domain into a 'domain-class-but-you-can-use-it-for-a-database' class
  • In some cases you'll have to modify the domain for an ORM to even work correctly (interfaces and enums come to mind)
  • Inheritance strategies often result in persisting all subclasses as a single table, which kind of defeats the original purpose (SQL dislikes hierarchy because of unnecesary JOINs)
  • Concerns like transactions are managed on an application level rather than inside the RDBMS

Read more

Solution 1: active record pattern

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data. - Martin Fowler

Unsurprisingly, one of the proposed solutions is to couple the domain directly with the database logic. Ruby on Rails provides this much needed interface.

Solution 2: use noSQL

Let's ignore the requirement and try something else: avoid SQL altogether. Now (excluding the novel newSQL) we are left to choose between an array of database engines, each having a different approach or use case.

The most used noSQL engine, and appropiate for this use case, is MongoDB. MongoDB stores data in JSON-like documents, and features embedding as a first-class citizen.

This leaves us to ponder the domain ≠ data problem once again.

Object-oriented, or 'class' oriented?

Java is a general-purpose language, and that's alright. Its general-purposeness makes it okay for 'enterprise' solutions and teams which don't have a specific knack for a construct or way of coding.

But really now, saying Java is object-oriented is a stretch. You can't define well-known objects using obj!

  • Take any Service or Repository class and you'll find them quicker to implement using functions. And what about making them static?
    • If an object has to be created (expensive operation!) only to have it called once and immediately have the GC dispose of it, that object creation could have been a function call.
    • If you have a static class call with no internal state (input params at most), why is it a class?
  • If I had a penny each time I saw a class made up of a constructor and a call method, I'd be drowned in pennies.
  • I think Spring, wherein some of those concerns are largely automated through annotations, is telling us this exact thing.

Don't take my word for it, but I bet not everything in our project is a stateful, method-rich entity. Sometimes classes are overkill and add little to no value.

Let's see a quick example of an URLBuilder class, which formats an URL based on some parameters:

Implementation in Java
class URLBuilder {
    private StringBuilder folder, param;
    private String connType, host;

    void setConnectionType(String conn) {
        connType = conn;
    }

    URLBuilder(){
        folder = new StringBuilder();
        param = new StringBuilder();
    }

    URLBuilder(String host) {
        this();
        this.host = host;
    }

    void addSubfolder(String tfolder) {
        folder.append("/");
        folder.append(tfolder);
    }

    void addParameter(String parameter, String value) {
        param.append(parameter);
        param.append("=");
        param.append(value);
    }

    String getURL() throws URISyntaxException, MalformedURLException {
        URI uri = new URI(connType, host, folder.toString(),
                param.toString(), null);
        return uri.toURL().toString();
    }
}
...
URLBuilder urlb = new URLBuilder("www.example.com");
urlb.setConnectionType("http");
urlb.addSubfolder("somesub");
urlb.addParameter("param", "unknown");
String url = urlb.getURL(); // finally gets URL

Example simplified from this stackoverflow post.

Now, let's see Scala in action
def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = {
  val schema = if (ssl) "https://" else "http://"
  (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query"
}
...
def getURL = urlBuilder(ssl=true, domainName)
val url = getURL('myEndpoint', 'myVal=3') // gets URL

Taken from Scala tour. For a real world (albeit more complex) Java implementation, refer to Apache's URIBuilder.

NullPointerException

Also known as the billion dollar mistake. Did you know in 2020, 97% of all program halts were traced back to NullPointerException? Yeah, don't believe everything you read on the internet I know, but sometimes it really feels like that.

null checking is a tedious task, and Java is no stranger to it. Java likes null so much it even has its own design pattern!

This pattern consists of creating classes with empty logic (yay, more classes!) which encompass the possible null state of its parent class. All of this is done to prevent ad-hoc obj != null checks, which are even worse.

I don't think we even need an example of why modeling a no-op is cumbersome. Instead, let's look at some modern solutions:

Borrowing from FP, Java 8+ introduced an Optional API
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(() -> "john"); // john
...
Optional<String> opt = Optional.of("a string");
opt.ifPresent(str -> System.out.println(str.length()));

Even though it is not a 'purist-worthy' implementation, it's better than plain null checking!

Kotlin chooses to outright ignore null
var a: String = "abc" // regular initialization -> non-null by default
a = null // compilation error
...
val a = "Kotlin"
val b: String? = null
println(b?.length) // actually returns null
println(a?.length) // unnecessary safe call

Patterns are actual antipatterns

I tried to solve a problem with Java. I now have a ProblemFactory

Patterns

The Visitor (anti?) pattern

The Visitor's intent is to circumvent the lack of multimethod support in static type-checked languages. Let's see an example:

interface Graphic
    void draw()

class Shape implements Graphic
    field id
    void draw()
    // ...

class Dot extends Shape
    field x, y
    void draw()
    // ...

class Circle extends Dot
    field radius
    void draw()
    // ...

We have a bunch of shapes, which know how to draw() themselves. Now let's imagine we add an Exporter which exports based on the shape they're 'passed': let's see how it behaves.

class Exporter
    void export(s: Shape)
        print("Exporting shape")
    void export(d: Dot)
        print("Exporting dot")
    void export(c: Circle)
        print("Exporting circle")
...
class App
    void export(shape: Shape)
        Exporter exporter = new Exporter()
        exporter.export(shape);

app.export(new Circle());
"Exporting shape"

Wait, what? We passed a Circle but a Shape was exported instead. Here's the thing: the compiler doesn't know which implementation of draw() to call because of static binding, and can't evaluate the type passed at runtime.

The proposed OOP solution is the Visitor pattern, which effectively tells each type to accept a visitor, and once accepted tell it to visit this type. The Exporter would act as a visitor, visiting each type and being called therein.

class Circle extends Dot
    method accept(v: Visitor)
        v.visit(this)

What you're actually doing is a glorified, messy switch. First of all, I'd try to change my design to eliminate the need for double dispatch. If you still have to use it, the Java solution I suggest using is to have pattern matching on the instance type thanks to Java 14:

if (shape instanceof Circle c) {
    exporter.export(c)
} else if(shape instanceof Dot d) {
    exporter.export(d)
...
}

An alternative solution I've coded in Javascript using an object and "matching" through keys as types:

class Shape {
    draw(){
        return 'drawing shape'
    };
}
class Dot extends Shape {
    draw(){
        return 'drawing dot'
    }
}
class Rectangle extends Shape {
    draw(){
        return 'drawing rectangle'
    }
  }
...
class Exporter {
    constructor() {
        this.types = {
            [Shape.name]: 'like a shape!',
            [Rectangle.name]: 'like a rectangle!',
            [Dot.name]: 'like a dot!'
        }
      }
    export(shape){
        console.log(shape.draw() + ' and exporting ' + this.types[shape.constructor.name])
    }
}
...
r = new Rectangle()
e = new Exporter()
e.export(r)
'drawing rectangle and exporting like a rectangle!'

Reinventing the Decorator pattern

The decorator pattern isn't actually bad in and of itself. It prevents combinatorial explosion from happening due to a high number of 'combinable' requirements. I actually think it is quite ingenuous, but its structure isn't trivial: sometimes it can be kind of verbose. Decorator pattern structure Is there a simpler way of implementing a Decorator? Let's see below with examples!


A pizza menu

Here, Derek Banas posts his +100 Java SLOC for a pizza shop that sells pizzas with the following options:

  • Each pizza has a base, which he calls Planpizza
  • You may add Mozzarella for $0.5
  • You may add TomatoSauce for $0.35

There are 4 possible pizzas here, but it's easy to see this number growing fast for each new topping added.

Using currying, I was able to implement this Decorator as higher-order functions:

const seed = (pizza) => pizza // identity
const dough = ['dough', 4.0] // base pizza

const mozCheese = otherIngredient => pizza => otherIngredient([pizza[0] + ', moz cheese', pizza[1] + 0.5])
const tomatoSauce = otherIngredient => pizza => otherIngredient([pizza[0] + ', tomato sauce', pizza[1] + 0.35])
// one could refactor the ingredients and make an ingredient generator
let pizzaNoToppings = dough
let tomatoPizza = tomatoSauce(seed)(dough)
let cheesePizza = mozCheese(seed)(dough)
let pizzaWithTomatoAndCheese = mozCheese(tomatoSauce(seed))(dough)

console.log(pizzaNoToppings) // [ 'dough', 4 ]
console.log(tomatoPizza) // [ 'dough, tomato sauce', 4.35 ]
console.log(cheesePizza) // [ 'dough, moz cheese', 4.5 ]
console.log(pizzaWithTomatoAndCheese) // [ 'dough, moz cheese, tomato sauce', 4.85 ]

Each ingredient is a higher-order function which takes another ingredient and applies it later, that's why we need to define a seed operator. Surely, a more creative programmer may choose to fold with an operator, or implement a monad perhaps?


Generalized 'decorator' to verify lists

I've quickly coded a 'custom list instantiator' which allows to verify a list based on a set of rules.

// conditions :: [[String, Boolean]]
const listChecker = conditions => list => {
    const [errorMessage, _fn] = conditions.find(([_error, fn]) => fn(list)) || []
    if(errorMessage)
        throw new Error(errorMessage)
    return list
}

const cappedToFiveNumbers = listChecker([
  ["List has more than five elements", (l) => l.length > 5],
  ["List has non-number members", (l) => l.some(isNaN)]
])

let myList = cappedToFiveNumbers([1, 2, 3, 4, 5]) // [1, 2, 3, 4, 5]
let anotherList = cappedToFiveNumbers([1, 2, 3, 4, 5, 6]) // Error: List has more than five elements
let yetAnotherList = cappedToFiveNumbers(['a', 2]) // List has non-number members

To make this resistant to array mutability, I'd suggest a Proxy (listening to changes), or creating a customArray ES6 class.


Bottom line

Remember there is no silver bullet! Plus, the JVM is still a widely used and stable platform!

I, personally, think the best way to look at programming concepts is to see them as tools. Having a diverse toolbox prevents me from doing everything the same and probably inefficient way.

I would love to see better or different solutions. Don't hesitate to contact me!

My LinkedIn


almost 4 years ago

Franco Battaglia