Modeling a Banking system in OOP
Tags: oop, programming, beginners, tutorial
Published: 2023-01-11 20:24:26

The beauty of Object-oriented programming is that any real-world system can easily be modeled with it. Using [abstraction](https://en.wikipedia.org/wiki/Abstraction_(computer_science)) a complex system can be made simple and represented in code.

In this post, I will be modeling a Banking system and along the way, I will be introducing key concepts from OOP.

## Abstraction

First of all, let's create an abstraction of the whole system. Banks have accounts that they manage for their clients. A client can create an account. These accounts have balances from which they can make withdrawals and deposits.

Following the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle) let's create two classes one called **Account** that manages the balances. Then another class called **Bank** that manages and creates Accounts.

## Implementation

![UML diagram for BankAccounts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9dkyztaqnlg06h5o62s.png)

Here's the UML diagram of the system. First, we have to implement an [abstract base class](https://dev.to/dollardhingra/understanding-the-abstract-base-class-in-python-k7h) from which all account class [inherit from](https://dev.to/flippedcoding/what-is-inheritance-3fk6).

```python
from abc import ABC, abstractmethod
from datetime import datetime


class AccountBase(ABC):
    """
    Abstract base class for account.
    """

    def __init__(self, account_name: str, bank_name: str, balance: float = 0 ) -> None:
        """
        Initialization method for AccountBase subclasses.
        """

        self._account_name = account_name
        self._bank_name = bank_name
        self._created = datetime.now()
        self._updated = None
        self._balance = balance

    def withdraw(self, amount: float) -> None:
        """
        method to make withdrawals.
        """

        if self._balance >= amount:
            self._balance -= amount
            self._created = datetime.now()
            print(f"Detail: ₦{amount} succesfully withdrawn.")
        else:
            print("Withdrawal failed")

    @abstractmethod
    def deposit(self, amount: float):
        """
        Abstract method to make deposit. To be implemented by subclasses.
        """

        pass

    def balance(self):
        """
        Method to return blance of account
        """

        return self._balance

    def info(self):
        print(f"Bank name: {self._bank_name}")
        print(f"Account name: {self._account_name}")
        print(f"Account balance: ₦{self._balance}")
        print(f"Account created at: {self._created}")
        print(f"Account updated at: {self._updated}")
```

The `AccountBase` class is abstract which means no object can be created from it. It contains five instance variables and five methods. Four of which have been implemented. The `deposit` method is an abstract method that should be implemented by the `AccountBase` subclasses.

Let's create the first subclass of the AccountBase:

```python
class AdultAccount(AccountBase):
    """
    Adult account class
    """

    def deposit(self, amount: float):
        """
        AdultAccount implementation of deposit.
        """

        self._balance += amount
        self._created = datetime.now()
        print("Deposit succesful")

```

The `AdultAccount` class inherits from the `AccountBase`. It is a concrete class meaning objects can be created from it. It implemented the `deposit` method and allows an unlimited amount of deposits.

Let's take a look at another subclass:

```python
class StudentAccount(AccountBase):
    """
    Student account class
    """

    ACCOUNT_LIMIT = 4E5

    def deposit(self, amount: float):
        """
        StudentAccount implementation of deposit.
        """

        if (self._balance + amount) > self.ACCOUNT_LIMIT:
            print("Account limit exceeded.")
        else:
            self._balance += amount
            self._updated = datetime.now()
            print("Deposit succesful")
```

This subclass has a limit to the amount that can be deposited. The `ACCOUNT_LIMIT` is a [class variable](https://dev.to/ogwurujohnson/distinguishing-instance-variables-from-class-variables-in-python-81)

> The fact that the two subclasses have different implementations is an example of [polymorphism](https://dev.to/flippedcoding/what-is-polymorphism-2j6e)(many forms).

Now let's create the Bank class:

```python
class Bank:
    """
    Factory class for Account class.
    A Bank can create Accounts.
    """

    def __init__(self, name: str):
        """
        Initialization method for bank.
        """

        self._name = name
        self._accounts: list[AccountBase] = []

    def create_adult_account(self, account_name: str, initial_deposit: float) -> AccountBase:
        """
        Creation method to create AdultAccount
        """

        acct = AdultAccount(account_name, self._name, initial_deposit)
        self._accounts.append(acct)
        return acct

    def create_student_account(self, account_name: str, initial_deposit: float) -> AccountBase:
        """
        Creation method to create StudentAccount
        """

        acct = StudentAccount(account_name, self._name, initial_deposit)
        self._accounts.append(acct)
        return acct

    def list_accounts(self) -> list[AccountBase]:
        """
        Return list of accounts in Bank.
        """

        return self._accounts

    def total_amount(self) -> float:
        """
        Return total amount in bank.
        """

        total = sum([acct.balance() for acct in self._accounts])
        return total
```
The bank class creates accounts. It has two creation methods that can create `AdultAccount` and `StudentAccount` respectively. This class is a [factory](https://refactoring.guru/design-patterns/factory-comparison) that produces accounts. It contains two instance variables `self._name` and `self._accounts`. The `self._accounts` stores every account created by the object.

## Client code

Here's the client code that will use the Bank system:

```python
b = Bank(name="devbank")

acct1 = b.create_adult_account(account_name="EteimZ", initial_deposit=40000) # create an adult account with initial deposits of 40000
acct2 = b.create_student_account(account_name="John", initial_deposit=4000) # create a student account with initial deposits of 4000

for acct in b.list_accounts():
    acct.info() # displays all account's info from bank

b.total_amount() # 44000 
acct1.deposit(8000) # make deposit in acct1
b.total_amount() # 52000
```

First, we created an instance of the `Bank` class called `b`. Using that instance we created an Adult and Student Account. These results are instances of the `AccountBase` subclasses. Thanks to the creation methods we don't have to worry about creating the objects ourselves. 

From each account, we can make deposits and withdrawals. The `_accounts` instance variable maintains a list of all accounts created from that instance. It can be accessed from the `list_accounts()` method of the Bank class. The total amount in the bank can also be retrieved from the `total_amount()` method.