OOP concepts are not easy to grasp as because sometimes we can't relate it's features with real life applications. But with a practical example we can clearly understand the usage of OOP in real world. Let's create a simple banking application today. So, the first thing first let's figure out what we will need to create a simple banking system:
- Customer: account and balance info
- Ledger: a money transaction history of customers.
We have defined some entities above. These entities have their own characteristics and functionalities. For example, customer entity has some properties like a name, an account number, contact info and their current balance. It has also some functionalities like a customer can send money to another customer or add money to his account or checking balance. Ledger is a transaction history where we keep track of money flow in terms of debit or credit transaction on a particular date for a certain customer. It has properties like customer account, debit/credit amount, date. An entity can be any thing. Anything like a person, an animal, a book or a pen which has its own properties and functionalities. In OOP we can define an entity as a class.
Class
It's a blueprint for an entity. A class defines what characteristics an entity owns and what functionalities it can perform. In OOP, characteristics are referred as properties and functionalities are methods. For example, if a dog is an entity then a dog has properties like 4 legs, 2 eyes, 2 ears, 1 tail and a dog has functionalities like it can run, bark, sniff etc.
So, if a customer of our banking application is an entity then our customer class would be:
class Customer { // Properties Id: number; Name: string; AccountNumber: string; PhoneNumber: string; Balance: number; // Methods CheckBalance(): number { return this.Balance; } }
Like a design of a building is not the actual building similarly a class of an entity is not the actual entity. It's just an abstraction or a concept of that entity. We need to instantiate it by creating objects. Here's how:
var customerOne = new Customer(); // We have instantiated a class by creating an object customerOne customerOne.Id = 1; customerOne.Name = "John"; customerOne.AccountNumber = "44432113"; customerOne.PhoneNumber = "+880988754"; customerOne.Balance = 1450; console.log(customerOne.CheckBalance()); var customerTwo = new Customer(); // We can instantiate multiple objects of a class customerTwo.Id = 2; customerTwo.Name = "Sofia"; customerTwo.AccountNumber = "8987222"; customerTwo.PhoneNumber = "+881133"; customerTwo.Balance = 500; console.log(customerTwo.CheckBalance());
Apart from our defined functionalities a class has a special method called constructor that is invoked automatically at the time of object creation. We can use to pass any parameters while creating a new object of a class:
class Customer { // Properties Id: number; Name: string; AccountNumber: string; PhoneNumber: string; Balance: number; constructor(id: number) { this.Id = id; } // Methods CheckBalance(): number { return this.Balance; } } // Instantiation var customerThree = new Customer(1);
We have just learned two of the core features of OOP which are classes and objects. Now let's move to our next entity ledger:
class Ledger{ RecordId: number; CustomerAccount: Customer; // A class can also be used as a type Amount: number; TransactionType: number; Date: Date; }
We need another class for the money transfer history in ledger:
class MoneyTransferLedger { RecordId: number; CustomerAccount: Customer; ToAccount: string; Amount: number; TransactionType: number; Date: Date; }
Inheritance
But hold on! Did you notice that few properties of Ledger and MoneyTransferLedger are quite common? That's because of MoneyTransferLedger is a Ledger too just with an extra property called ToAccount. But there's a problem. If I need to change the name of a property in the ledger class money transfer ledger will still hold the older one. That will create confusion in the future but OOP has a solution to this kind of problem. Entities can have parent-child relationships. For example, a dog and a cat are both animals. They have common characteristics like 4 eyes, 2 eyes etc. Like this, we can think Ledger entity is the parent and MoneyTransferLedger is the child of Ledger. So the MoneyTransferLedger class will have all the properties and functionalities of Ledger class and also properties and functionalities of it's own. So, we can define our classes like:
class Ledger { RecordId: number; CustomerAccount: Customer; Amount: number; TransactionType: number; Date: Date; GetCustomer(): Customer { return this.CustomerAccount; } } class MoneyTransferLedger extends Ledger { // All the properties and functionalities of Ledger ReceiverAccount: Customer; // Properties of it's own // Functionalities of it's own GetCustomer(): Customer { return this.CustomerAccount; } }
If we create a money transfer ledger record we will still have all the properties of the ledger as well.
var moneyTransferRecord = new MoneyTransferLedger(); moneyTransferRecord.CustomerAccount = new Customer(); moneyTransferRecord.ReceiverAccount = new Customer(); moneyTransferRecord.CustomerAccount.AccountNumber = "44432113"; moneyTransferRecord.ReceiverAccount.AccountNumber = "44432113"; moneyTransferRecord.Amount = 3000; moneyTransferRecord.TransactionType = 0; console.log("Amount: " + moneyTransferRecord.Amount + "has been sent to: " + moneyTransferRecord.CustomerAccount.AccountNumber + " from: moneyTransferRecord.ReceiverAccount.AccountNumber");
In OOP, we can use inheritance where we need to reuse the properties and functionality of an existing class.
Okay now we can register new customers, add or subtract balance, transfer money to another account and keep records of the money flow. These classes would be used from anywhere to create new features. But what if a developer of your team accidentally updates the account number of a customer? The whole record would be lost. What we need now is to restrict the developer either to set the property value directly. In OOP, we can set access levels for a particular property or a method. Here how:
Access Modifiers
We are just going to talk about three of them: Public, Protected and Private. Public properties can be accessed from anywhere. Protected properties can only be accessed from child class and Private properties can only be accessed within a class. In our case, our customer class will be:
class Customer { private Id: number; public Name: string; private AccountNumber: string; public PhoneNumber: string; private Balance: number; constructor(id: number) { this.Id = id; } public CheckBalance(): number { return this.Balance; } }
But we need to set it anyway right? We can not register new customers without an account number. We have one way out of this problem is to create a public method to set the property value with a validation and another method to get the value. In OOP, this is called:
Encapsulation
Which is a way for restricting unauthorized access and mutation of properties of an object. We can access it only by getter and setter methods. So, our customer class will set account number for new customers only. Here's how:
class Customer { private Id: number; public Name: string; private AccountNumber: string; public PhoneNumber: string; private Balance: number; constructor(id: number) { this.Id = id; } // We are allowing to set account number for new accounts only public setAccountNumber(accNumber: string) { if (typeof this.AccountNumber == "undefined" || this.AccountNumber) { this.AccountNumber = accNumber; } else { console.log("Account number " + accNumber + " can not be updated"); } } public getAccountNumber() { return this.AccountNumber; } public setBalance(balance: number) { this.Balance = balance; } public getBalanceInfo() { return this.Balance; } public CheckBalance(): number { return this.Balance; } }; var customerOne = new Customer(); customerOne.Name = "John"; customerOne.setAccountNumber("44432113"); customerOne.PhoneNumber = "+880988754"; customerOne.setBalance(1450); // Try to update account number customerOne.setAccountNumber("010101"); // console output: Account number 010101 can not be updated
Polymorphism
Let's assume few days later we have decided to onboard business customers to our bank. We can simply follow the OOP inheritance rule to introduce this new type of customer:
class BusinessCustomer extends Customer { public CompanyAddress: string; }
Suppose we are setting rule for minimum deposit balance. Personal accounts should have 500 and business accounts should have 2000 minimum in their account. But the problem is setBalance() is a method from Customer class which is intended for personal accounts by default. Should we create a new method in BusinessCustomer class to set the balance? For example setBusinessCustomerBalance() method to set the balance for business accounts. But OOP provides a better solution to this. We can actually override a method of a parent class in child class which means we can re-define a parent class method in child class and that would be only child class specific. For example,
class Customer { private Id: number; public Name: string; private AccountNumber: string; public PhoneNumber: string; private Balance: number; constructor(id: number) { this.Id = id; } public setBalance(balance: number) { if (balance >= 500) { this.Balance = balance; } else { console.log("Personal accounts must have an initial deposit of more than 499") } } public getBalanceInfo() { return this.Balance; } }; class BusinessCustomer extends Customer { public CompanyAddress: string; public setBalance(balance: number) { if (balance >= 2000) { this.Balance = balance; } else { console.log("Business accounts must have an initial deposit of more than 1999") } } } var personalAccount = new Customer(1); personalAccount.setBalance(300); // console output: Personal accounts must have an initial deposit of more than 499 var businessAccount = new BusinessCustomer(2); businessAccount.setBalance(1500); // console output: Business accounts must have an initial deposit of more than 1999
A very cleaner solution right? This is called polymorphism in OOP. It is a way to define a type that can be extended and reused by other classes, while still preserving the type information of the current class.
Abstraction
So far what we have built above are entity classes. Now let's create service classes which will provide methods for handling adding and retrieving new customer information, transferring money from one account to another, get current balance by calculating ledger records etc. Other developers can also use these service classes to implement new features. For this purpose other developers don't need to know how a customer is getting created or how balance calculation or money transfer works internally. They just need a parameterized method where they are going to set value and invoke it without thinking about its internal complexities. In OOP, this is called abstraction which is very useful to reduce developer overheads. For example, to make coffee with a coffee machine you just need to add water, coffee and sugar. You don't need to understand how that machine is making coffee. Someone else had thought about it for you. This coffee machine is an abstraction here. Here we are going to use interfaces. An interface defines the specifications of an entity. It lays out the contract that states what needs to be done but doesn’t specify how it will be done. Interfaces needs to be implemented and can't not be instantiated directly. Let's create our ICustomerService interface and it's implementation:
interface ICustomerService { createCustomerAccount(newCustomerInfo: Customer): boolean; transferMoney(fromAccount: Customer, toAccount: Customer, amount: number): boolean; } class CustomerService implements ICustomerService { private _customers: Customer[]; private _ledger: Ledger[]; constructor() { this._customers = []; this._ledger = []; } createCustomerAccount(newCustomerInfo: Customer): boolean { return this.saveCustomer(newCustomerInfo); } transferMoney(fromAccount: Customer, toAccount: Customer, amount: number): boolean { var ledgerEntry = this.createLedgerEntry(fromAccount, toAccount, amount); return this.saveLedger(ledgerEntry); } saveCustomer(customerInfo: Customer): boolean { try { this._customers.push(customerInfo); return true; } catch(error) { return false; } } saveLedger(ledgerEntry: Ledger): boolean { try { this._ledger.push(ledgerEntry); return true; } catch(error) { return false; } } createLedgerEntry(fromAccount: Customer, toAccount: Customer, amount: number): Ledger { if (toAccount) { var moneyTranfer = new MoneyTransferLedger(); moneyTranfer.RecordId = 1; moneyTranfer.CustomerAccount = fromAccount; moneyTranfer.ReceiverAccount = toAccount; moneyTranfer.Amount = amount; moneyTranfer.Date = new Date(); return moneyTranfer; } else { var transaction = new Ledger(); transaction.RecordId = 1; transaction.CustomerAccount = fromAccount; transaction.Amount = amount; transaction.Date = new Date(); return transaction; } } } let customerService: ICustomerService = new CustomerService(); // here you'll see just transferMoney() and createCustomerAccount() methods are only accessible
We have few other internal methods like createLedgerEntry(), saveCustomer(), saveLedger() but user can only access transferMoney() and createCustomerAccount() methods. This is abstraction! See how it helped us to reduce developer overheads? Pretty cool. That's all everyone. Please let me know about your feedback on this. Thanks!