Object Oriented Programming is dead.
Object Oriented Programming is a mature programming paradigm. There are plenty of books describing what is all about. Every computer science student learned about it in a college. Yet, it’s 2021, and Object Oriented Programming in Java world is dead. You may wonder how is that possible since almost everything in Java language starts with the keyword class?
The purpose of this post is to bring awareness to developers of what is OOP and how it differs from procedural programming… During the years of my career, I’ve seen too many programmers who don’t understand how and when choosing the correct paradigm.
Let’s quickly remind ourselves the first rule of OOP which is:
Encapsulation
Personally, I really like one of the definitions I’ve heard a time ago:
The object that has the knowledge should do the work
Before we jump into the practical example I would like you to keep in mind that I’m not saying that you should always use Object Oriented Programming. What I’m saying is that it suppose to be used in some part of our system but unfortunately it’s not.
Let’s imagine for a while you join the team which writing the software for the bank industry. You looked into the code and you found something like this (Of course this is a simplified version):
@Entity
public class BankAccountEntity {
@Id
private String bankAccountId;
private BigDecimal amount;
private String currency;
public String getBankAccountId() {
return bankAccountId;
}
public void setBankAccountId(String bankAccountId) {
this.bankAccountId = bankAccountId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
// many others fields, getters and setters
}
public class BankAccountService {
// constructor, repository, etc.
public void deposit(String accountId, BigDecimal amount, String currency){
BankAccountEntity bankAccountEntity = // getting entity from repository...
if(bankAccountEntity.getCurrency().equals(currency)){
BigDecimal currentAmount = bankAccountEntity.getAmount();
bankAccountEntity.setAmount(currentAmount.add(amount));
}
else {
throw SomeException("....");
}
}
}
You may ask, what is wrong with this code? Well, in fact, everything! We completely broke the Encapsulation rule. The keyword private
next to our properties means nothing in that case because we allow access to it through getters and setters.
It’s extremely dangerous because now, every other object in our system can easily change values like amount
. It’s easy to control the situation with 5 people working on the code but after two years and tens of people in the project, it’s not possible.
Furthermore, In this situation, we give all control to a class called BankAccountService
. For some unknown reasons, this convention is extremely popular in the Java world. You have 99% sure that if you see a class with a name like XxxxService
your deal with procedural code. It means that line by line we have to tell the program how to do the work instead of what to do.
Also, some questions always bother me when I see code like this in the domain model. How do you unit test it? Do you use TDD, if yes how do you do it with procedural code? What’s are the benefits of procedural programming in this context compared to Object Oriented version?
Now, what is the alternative and OOP version of the same code?
class BankAccount {
private final BankAccountId bankAccountId;
private final Money balance;
public BankAccount(BankAccountId bankAccountId, Money balance) {
this.bankAccountId = bankAccountId;
this.balance = balance;
}
public BankAccount deposit(Money toDeposit){
Money newBalance = this.balance.add(toDeposit);
return new BankAccount(bankAccountId,newBalance);
}
}
There are few key things to notice here.
- First, we don’t use raw type anymore. Instead of
BigDecimal amount
andString currency
we encapsulated them into theMoney
object.BankAccount
itself as an object “which has the knowledge” performs deposit operation. - Furthermore, all variables are immutable. With this approach, we can easily write our unit tests. We don’t have any dependencies like in the previous
BankAccountService
class, so we don’t have to mock anything. We can easily go with TDD and before we write the body of theMoney
class create proper test scenarios. - Also, we get rid of
@Entity
annotation but details of it are covered in this post.
Now, let’s take a closer look into the second rule of OOP which is:
Polymorphism
There are plenty of definitions on the web but let’s focus on the example.
Imagine that this time we get the new feature to implement. Account-holders have to pay a fee for it based on the account type. Currently, we support two types: Regular and Premium
. For the Regular holders, we want to take 1% from the current balance. For the Premium clients, we want to have a constant number of 1$.
The procedural version, of course, implements the behavior inside the service class:
public class BankAccountService {
// constructor, repository, etc.
public BigDecimal calculateAccountFee(){
BankAccount bankAccountEntity = // getting entity from repository...
if(bankAccountEntity.getType() == REGULAR {
BigDecimal currentAmount = bankAccountEntity.getAmount();
BigDecimal fee = currentAmount.multiply(BigDecimal.valueOf(0.01));
return fee;
}
else {
return BigDecimal.valueOf(1);
}
}
}
Some people might say, it’s not so bad… Well, again it is. In terms of polymorphism, we have to ask a question what will happen if we have to add new account types? Like Basic, Silver, Gold, Vip
, and many more. Also, each of them will have more sophisticated algorithms to calculate the fee. Eventually, we will end up with multiple if-else statements, unmaintainable and untested code.
We can avoid that by introducing polymorphism. Let’s start with simple java interface which express a proper behavior.
public interface Chargeable {
Money chargeFee();
}
Now we are able to create multiple bank account types each of them with unique implementation of Chargeable
interface.
class RegularBankAccount implements Chargeable {
// fields, constructor
@Override
public Money chargeFee() {
return balance.multiplyBy(0.01);
}
}
class PremiumBankAccount implements Chargeable {
// fields, constructor
@Override
public Money chargeFee() {
return new Money(1, Currency.USD);
}
}
With this approach it’s easy to add another Bank account types with their own implementation.
One general advice: always try to express behavior with your interfaces. Good examples are: Payable, Flyable, Comparable, etc.
There is also a third rule – Inheritance
. The key to remember about it is:
- Do your best to avoid it
- Favor composition over inheritance 🙂
The final thoughts
I hope you don’t get me wrong. I’m not try to say that in every little piece of our system we should apply Object Oriented Programming techniques. What I try to say is that mostly we don’t apply it at all. I consider it as a huge mistake since procedural code in rich domain model always turns into a nightmare.
1 Comment