Are you still stuck in the habit of using getters and setters in your code? Well, you've landed on the right page to
grasp one of the most fundamental concepts of Object-Oriented Programming!
Rethinking Object-Oriented Development
Often, developers mistakenly believe they are practicing Object-Oriented Programming (OOP) simply by employing
service-like classes. The example below is a common scenario encountered across various projects:
class IncreaseBankAccountBalance {
constructor(private readonly _bankAccountRepository: BankAccountRepository) {
}
execute({id, increase}: { id: string, increase: number }) {
const account = this._bankAccountRepository.findById(id)
const currentBalance = account.getBalance()
const newBalance = currentBalance + increase
account.setBalance(newBalance)
this._bankAccountRepository.save(account)
}
}
class BankAccount {
constructor(
private readonly _id: number,
private _balance: number
) {
}
getBalance() {
return this._balance
}
setBalance(value: number) {
this._balance = value;
}
}
In this scenario, we fetch an account from a repository and then proceed to call getters and setters to update it. It's
akin to treating a private property like a public one, exposing its value and providing a method to modify it at the
user's discretion.
Unveiling the Object-Oriented Approach
The core principle of OOP is to tell instances to perform work rather than manipulating their properties directly.
This encapsulation of logic within domain entities, rather than imperative services, defines true OOP.
Refining our example, we can shift the increase logic from the IncreaseBankAccountBalance
service to the BankAccount
class through a well-named instance method:
class IncreaseBankAccountBalance {
constructor(private readonly _bankAccountRepository: BankAccountRepository) {
}
execute({id, increase}: { id: string, increase: number }) {
const account = this._bankAccountRepository.findById(id)
account.increaseBalance(increase)
this._bankAccountRepository.save(account)
}
}
class BankAccount {
constructor(
private readonly _id: number,
private _balance: number
) {
}
increaseBalance(increase: number) {
this._balance += increase;
}
}
Now, notice that we no longer expose the private balance property. Any user of the BankAccount
class must go through
the
increaseBalance
instance method, effectively encapsulating the logic of balance increase.
Elevating Immutability for Code Integrity
As a bonus, we can take it a step further by introducing immutability to our code. This minimizes side effects by
returning a new instance of BankAccount
with the updated balance, instead of modifying its private property directly:
class IncreaseBankAccountBalance {
constructor(private readonly _bankAccountRepository: BankAccountRepository) {
}
execute({id, increase}: { id: string, increase: number }) {
const account = this._bankAccountRepository.findById(id)
this._bankAccountRepository.save(account.withIncreasedBalance(increase))
}
}
class BankAccount {
constructor(
private readonly _id: number,
private readonly _balance: number
) {
}
withIncreasedBalance(increase: number) {
return new BankAccount(this._id, this._balance + increase)
}
}
In summary, here are some key takeaways for your future coding sessions:
- Be cautious when exposing getters and especially when exposing a setter.
- Objects should be the sole entities allowed to modify their properties.
- Every method must be well-named, clearly expressing its intent.
Stay tuned for more insights! Free to follow me on this platform
and LinkedIn. I share insights every week about software
design, OOP practices, and some personal project discoveries! 💻🏄
Top comments (0)