/ OBJECT-ORIENTED PROGRAMMING, OOP, SPRING

Is Object-Oriented Programming compatible with an enteprise context?

This week, during a workshop related to a Java course I give at a higher education school, I noticed the code produced by the students was mostly - ok, entirely, procedural. In fact, though the Java language touts itself as an Object-Oriented language, it’s not uncommon to find such code developed by professional developers in enterprises. For example, the JavaBean specification is in direct contradiction of one of OOP’s main principle, encapsulation.

Another example is the widespread controller, service and DAO architecture found equally in Java EE and Spring applications. In that context, entities are in general anemic, while all business logic is located in the service layer. While this is not bad per se, this design separates between state and behaviour, and sits at the opposite to true OOP.

Both Java EE and the Spring framework enforce this layered design. For example, in Spring, there’s one annotation for every such layer: @Controller, @Service and @Repository. In the Java EE world, only @EJB instances - the service layer, can be made transactional.

This post aims to try to reconcile both the OOP paradigm and the layered architecture. I’ll be using the Spring framework to highlight my point because I’m more familiar with it, but I believe the same approach could be used for pure Java EE apps.

A simple use-case

Let’s have a simple use-case: from an IBAN number, find the associated account with the relevant balance. Within a standard design, this could look like that:

@RestController
class ClassicAccountController(private val service: AccountService) {

    @GetMapping("/classicaccount/{iban}")
    fun getAccount(@PathVariable("iban") iban: String) = service.findAccount(iban)
}

@Service
class AccountService(private val repository: ClassicAccountRepository) {
    fun findAccount(iban: String) = repository.findOne(iban)
}

interface ClassicAccountRepository : CrudRepository<ClassicAccount, String>

@Entity
@Table(name = "ACCOUNT")
class ClassicAccount(@Id var iban: String = "", var balance: BigDecimal = BigDecimal.ZERO)

There are a couple of issues there:

  1. The JPA specifications mandates for a no-arg constructor. Hence, it’s possible to create ClassicalAccount instances with an empty IBAN.
  2. There’s no validation of the IBAN. The full round-trip to the database is required to check if an IBAN is valid.
Yes, there’s no currency. It’s a simple example, remember?

Being compliant

In order to comply with the no-args constructor JPA constraint - and because we use Kotlin, it’s possible to generate a synthetic constructor. That means the constructor is accessible through reflection, but not by calling the constructor directly.

<plugin>
    <artifactId>kotlin-maven-plugin</artifactId>
    <groupId>org.jetbrains.kotlin</groupId>
    <version>${kotlin.version}</version>
    <configuration>
        <compilerPlugins>
            <plugin>jpa</plugin>
        </compilerPlugins>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
    </dependencies>
</plugin>
If you use Java, tough luck, I don’t know about any option to fix that.

Adding validation

In a layer architecture, the service layer is the obvious place to put the business logic, including validation:

@Service
class AccountService(private val repository: ClassicAccountRepository) {
    fun findAccount(iban: String): Account? {
        checkIban(iban)
        return repository.findOne(iban)
    }

    fun checkIban(iban: String) {
        if (iban.isBlank()) throw IllegalArgumentException("IBAN cannot be blank")
    }
}

In order to be more OOP-compliant, we must decide whether we should allow invalid IBAN numbers or not. It’s easier to forbid it altogether.

@Entity
@Table(name = "ACCOUNT")
class OopAccount(@Id var iban: String, var balance: BigDecimal = BigDecimal.ZERO) {
    init {
        if (iban.isBlank()) throw IllegalArgumentException("IBAN cannot be blank")
    }
}

However, this means that we must first create the OopAccount instance to validate the IBAN - with a balance of 0, even if the balance is actually not 0. Again, as per the empty IBAN, the code does not match the model. Even worse, to use the repository we must access the OopAccount inner state:

repository.findOne(OopAccount(iban).iban)

A more OOP-friendly design

Improving the state of the code requires a major rework on the class model, separating between the IBAN and the account, so that the former can be validated, and can access the latter. The IBAN class serves both as the entry point, and the PK of the account.

@Entity
@Table(name = "ACCOUNT")
class OopAccount(@EmbeddedId var iban: Iban, var balance: BigDecimal)

class Iban(@Column(name = "iban") val number: String,
           @Transient private val repository: OopAccountRepository) : Serializable {

    init {
        if (number.isBlank()) throw IllegalArgumentException("IBAN cannot be blank")
    }

    val account
        @JsonIgnore
        get() = repository.findOne(this)
}
Notice the returned JSON structure will be different from the one returned above. If that’s an issue, it’s quite easy to customize Jackson to obtain the desired result.

With this new design, the controller requires a bit of change:

@RestController
class OopAccountController(private val repository: OopAccountRepository) {

    @GetMapping("/oopaccount/{iban}")
    fun getAccount(@PathVariable("iban") number: String): OopAccount {
        val iban = Iban(number, repository)
        return iban.account
    }
}

The only disadvantage of this approach is that the repository needs to be injected into the controller, then be explicitly passed to the entity’s constructor.

The final touch

It would be great if the repository could automatically be injected into the entity when it’s created. Well, Spring makes it possible - though this is not a very well-known feature, through Aspect-Oriented Programming. It requires the following steps:

Add AOP capabilities to the application

To effectively add the AOP dependency is quite straightforward and requires just adding the relevant starter dependency to the POM:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Then, the application must be configured to make use of it:

@SpringBootApplication
@EnableSpringConfigured
class OopspringApplication
Update the entity
  1. The entity must first be set as a target for injection. Dependency injection will be done through autowiring.
  2. Then, the repository be moved from a constructor argument to a field.
  3. Finally, the database fetching logic can be moved into the entity:
    @Configurable(autowire = Autowire.BY_TYPE)
    class Iban(@Column(name = "iban") val number: String) : Serializable {
    
        @Transient
        @Autowired
        private lateinit var repository: OopAccountRepository
    
        init {
            if (number.isBlank()) throw IllegalArgumentException("IBAN cannot be blank")
        }
    
        val account
            @JsonIgnore
            get() = repository.findOne(this)
    }
Remember that field-injection is evil.
Aspect weaving

There are two ways to weave the aspect into the , either compile-time weaving, or load-time weaving. I choose the the later is much easier to configure. It’s achieved through a standard Java agent.

  1. First, it needs to be added as a runtime dependency in the POM:
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-agent</artifactId>
        <version>2.5.6</version>
        <scope>runtime</scope>
    </dependency>
  2. Then, the Spring Boot plugin must be configured with the agent:
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <agent>${settings.localRepository}/org/springframework/spring-agent/2.5.6/spring-agent-2.5.6.jar</agent>
        </configuration>
    </plugin>
  3. Finally, the application must be configured accordingly:
    @EnableLoadTimeWeaving
    class OopspringApplication

And then?

Of course, this sample leaves out an important piece of the design: how to update the balance of an account? The layer approach would have a setter for that, but that’s not OOP. Thinking about it, the balance of an account changes because there’s a transfer from another account. This could be modeled as:

fun OopAccount.transfer(source: OopAccount, amount: BigDecimal) { ... }

Experienced developers should see some transaction management requirements sneaking in. I leave the implementation to motivated readers. The next step would be to cache the values, because accessing the database for each read and write would be killing performance.

Conclusion

There are a couple of points I’d like to make.

First, the answer to the title question is a resounding 'yes'. The results is a true OOP code while still using a so-called enterprise-grade framework - namely Spring.

However, migrating to this OOP-compatible design came with a bit of overhead. Not only did we rely on field injection, we had to bring in AOP with load-time weaving. The first is a hindrance during unit testing, the second is a technology you definitely don’t want in every team, as they make apps more complex. And that’s only for a trivial example.

Finally, this approach has a huge drawback: most developers are not familiar with it. Whatever its advantages, they first must be "conditioned" to have this mindset. And that might be a reason to continue using the traditional layered architecture.

I’ve searched Google for scientific studies proving OOP is better in terms of readability and maintainability: I found none. I would be very grateful for pointers.
The complete source code for this post can be found on Github in Maven format.
Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Is Object-Oriented Programming compatible with an enteprise context?
Share this