In software systems, encapsulation refers to the bundling of data with the mechanisms or methods that operate on the data. It may also refer to the limiting of direct access to some of that data, such as an object’s components. Essentially, encapsulation prevents external code from being concerned with the internal workings of an object.

wikipedia

Imagine a banking application where customers can:

Deposit money: Customers can add money to their accounts.

Withdraw money: Customers can take money out of their bank accounts.

Transfer money to another customer’s account: Customers can send money from their bank account to another person’s account.

With some rules for example:

  1. The maximum daily withdrawal amount is $50,000
  2. For deposit or withdrawal amount should be greater than 0
  3. To withdraw or deposit funds, the user’s account must be active
  4. Users can make a maximum of 10 withdrawal transactions per day (within a 24-hour period).
  5. Deposits are unlimited
  6. Customers must maintain a minimum balance of $1000 in their account

Look at customer class code :

final class Customer
{
    private string $name;
    private string $family;
    private bool $status; // true means active and false means deactive
    private array $transactions;


    public function __construct(
        string $name,
        string $family,
        bool $status,
        array $transactions
    ) {
        $this->name = $name;
        $this->family = $family;
        $this->status = $status;
        $this->transactions = $transactions;
    }


    public function setTransaction(array $transaction): void
    {

        array_push($this->transactions, [
            'type' => $transaction['type'],
            'amount' => $transaction['amount'],
            'date' => $transaction['date']
        ]);
    }

    public function getStatus(): bool
    {
        return $this->status;
    }

    public function getTransactions(): array
    {
        return $this->transactions;
    }
}

create an instance from customer class.

$customer = new Customer('John', 'Doe', true, []);

after instantiation customer can deposit,look at deposit action :

$amount = 5000;

if ($customer->getStatus() === true) {

    if ($amount >= 0) {
        throw new RuntimeException('Amount cannot less or equal to 0');
    }

    $customer->setTransaction([
        'amount' => $amount,
        'type' => 'deposit',
        'date' => date('Y-m-d', time())
    ]);
} else {
    throw new RuntimeException("User isn't active");
}

In line 3 we check customer status is active or not. if customer is active, at line 5 we check amount should be greater than 0 else throw new runtime exception. if everything’s true now we can inserted new transaction as deposit to list of transactions.

The code works correctly, but there is a flaw in the OOP design—specifically, encapsulation.

The operation of the deposit, which you saw earlier, is implemented by the client. This implies that the client can change the logic regarding the deposit. Additionally, this code design violates the ‘tell, don’t ask’ rule, as it first retrieves the status of the customer and then performs the deposit action. This violation is a defect in the encapsulation rule. For instance, consider the following code:”

$amount = 5000;

if ($customer->getStatus() === true) {

    if ($amount >= 2000) { // change condition
        throw new RuntimeException('Amount cannot less or equal to 0');
    }

    $customer->setTransaction([
        'amount' => $amount,
        'type' => 'deposit',
        'date' => date('Y-m-d', time())
    ]);
} else {
    throw new RuntimeException("User isn't active");
}

In line 5, someone changed the condition, but the business rule specifies that the amount must be greater than 0.

To solve this problem, transfer the deposit logic to the customer class, and let the client only call it, look at following code :

final class Customer
{
    private string $name;
    private string $family;
    private bool $status; // true means active and false means deactive
    private array $transactions;


    public function __construct(
        string $name,
        string $family,
        bool $status,
        array $transactions
    ) {
        $this->name = $name;
        $this->family = $family;
        $this->status = $status;
        $this->transactions = $transactions;
    }

    public function deposit(float $amount, string $date): void
    {

        if ($this->status === false) {
            throw new RuntimeException("User isn't active");
        }

        if ($amount >= 0) {
            throw new RuntimeException('Amount cannot less or equal to 0');
        }

        array_push($this->transactions, [
            'amount' => $amount,
            'type' => 'deposit',
            'date' => $date
        ]);
    }
}

Now compare the customer class with the customer class at the beginning of the article, we removed the getStatus() and getTransaction() methods because the deposit management logic is implemented by the class itself, and there is no need to expose internal data such as transactions to the outside of the class.

After instantiating the customer class, customers can make a deposit just by calling a method from the object and don’t need to check the business rules. And they also can’t do it without calling the deposit method, because transactions are private and the client doesn’t have access to it. look at following code :

$customer = new Customer('John', 'Doe', true, []);

$customer->deposit(2500, date('Y-m-d', time()));

we encapsulated logic and internal data about customer, each client need to perform deposit should be call deposit method from object and cannot implement different logic for that.

let’s take look at another example, this time withdrawal action, look at the following code :

$amount = 4000;
if ($customer->getStatus() === true) {

    $transactions = $customer->getTransactions();

    $withDrawals = array_filter($transactions, function ($item) {
        return $item['type'] === 'withdrawal' && $item['date'] === date('Y-m-d', time());
    });

    if (count($withDrawals) === 10) {
        throw new RuntimeException('Maximum withdrawal transactions is 10 times');
    }

    $sumOfTodayWithDrawals = array_sum(array_column($withDrawals, 'amount'));

    if ($sumOfTodayWithDrawals >= 50000) {
        throw new RuntimeException('Maximum daily withdrawal is 50000');
    }

    $sumOfAllWithDrawal =  array_sum(array_column(array_filter($transactions, function ($item) {
        return $item['type'] === 'withdrawal';
    }), 'amount'));

    $sumOfAllDeposits =  array_sum(array_column(array_filter($transactions, function ($item) {
        return $item['type'] === 'deposit';
    }), 'amount'));

    $currentAmount = $sumOfAllDeposits - $sumOfAllWithDrawal;

    if ($currentAmount <= $amount) {

        throw new RuntimeException('You cannot withdraw more than your account balance');
    }

    if (($currentAmount - $amount) < 1000) {
        throw new RuntimeException('You must keep at least $1,000 in your account');
    }

    $customer->setTransaction([
        'type' => 'withdrawal',
        'amount' => $amount,
        'date' => date('Y-m-d', time())
    ]);
} else {
    throw new RuntimeException("User isn't active");
}

As you can see, withdrawing is more complicated than depositing. in line 10 we check rule 4, in line 16 we check rule 1, in line 35 we check rule 6 and in line 30 we check that an amount that the customer wants to withdraw should not be more than or equal to the balance of the customer’s account.

What happens if the client changes these rules? Data consistency is lost, we also spread domain logic across the application, and all of them means violate encapsulation.

So let’s fix the code :

final class Customer
{
    private string $name;
    private string $family;
    private bool $status; // true means active and false means deactive
    private array $transactions;


    public function __construct(
        string $name,
        string $family,
        bool $status,
        array $transactions
    ) {
        $this->name = $name;
        $this->family = $family;
        $this->status = $status;
        $this->transactions = $transactions;
    }

    public function deposit(float $amount, string $date): void
    {

        if ($this->status === false) {
            throw new RuntimeException("User isn't active");
        }

        if ($amount >= 0) {
            throw new RuntimeException('Amount cannot less or equal to 0');
        }

        array_push($this->transactions, [
            'amount' => $amount,
            'type' => 'deposit',
            'date' => $date
        ]);
    }

    public function withDrawal(float $amount, string $date): void
    {

        if ($this->status === false) {
            throw new RuntimeException("User isn't active");
        }

        if ($amount >= 0) {
            throw new RuntimeException('Amount cannot less or equal to 0');
        }

        $withDrawals = array_filter($this->transactions, function ($item) {
            return $item['type'] === 'withdrawal' && $item['date'] === date('Y-m-d', time());
        });

        if (count($withDrawals) === 10) {
            throw new RuntimeException('Maximum withdrawal transactions is 10 times');
        }

        $sumOfTodayWithDrawals = array_sum(array_column($withDrawals, 'amount'));

        if ($sumOfTodayWithDrawals >= 50000) {
            throw new RuntimeException('Maximum daily withdrawal is 50000');
        }

        $sumOfAllWithDrawal =  array_sum(array_column(array_filter($this->transactions, function ($item) {
            return $item['type'] === 'withdrawal';
        }), 'amount'));

        $sumOfAllDeposits =  array_sum(array_column(array_filter($this->transactions, function ($item) {
            return $item['type'] === 'deposit';
        }), 'amount'));


        $currentAmount = $sumOfAllDeposits - $sumOfAllWithDrawal;

        if ($currentAmount <= $amount) {

            throw new RuntimeException('You cannot withdraw more than your account balance');
        }

        if (($currentAmount - $amount) < 1000) {
            throw new RuntimeException('You must keep at least $1,000 in your account');
        }

        array_push($this->transactions, [
            'amount' => $amount,
            'type' => 'withdrawal',
            'date' => $date
        ]);
    }
}

now client just call withdrawal method, and don’t need figure out about withdrawal logic :

$customer = new Customer('John', 'Doe', true, []);

$customer->withDrawal(2500, date('Y-m-d', time()));

Summary

encapsulation keep domain concepts and knowledge to class, and prevent to spread it in across application.

also prevent to expose internal data from class.

Leave a Reply

Your email address will not be published. Required fields are marked *