Introduction
There is a lot of confusion among beginners about the abstract class. Knowing its syntax is easy, but when and why should we use an abstract class is something which has troubled most start up developers. I will try to explain the concept with an easy to understand example. Hope this proves to be useful!
Background
There are a lot of definitions for an abstract class, more often I find that instead of explaining the concept, articles deal with the actual syntax of the abstract class. For example, an abstract class cannot have a private virtual member, blah blah.
This article will try to explain the What and the When.
Understanding the Abstract Class
An abstract class is a class which cannot be instantiated, which means you cannot create an object of an abstract class, it can only act as a base class and other classes can derive from it. So what is the rationale behind the existence of a class if we cannot create an object of it?
Let’s take an example of a bank to explain this. Later, we will go through the code to implement it.
Suppose you go to a bank for opening an account. Let’s see below what transpires between an applicant and a bank staff.
Bank Staff: Welcome sir.
Applicant: I want to open an account.
Bank Staff: Sure sir, what kind of account do you want to open?
Applicant: I just want to open an account.
Bank Staff: Do you want to open a Savings Account or a Current Account?
Applicant: I do not want to open any specific account, just open an account.
Bank Staff: Sir, this is not possible, we need to create a specific type of account. Unless you tell me the type of the account, I cannot help you.
In the light of the above discussion, we come up with the below inference for our banking application software.
- Banks have a facility to open an account
- Bank account should be of a specific type (Saving/Current)
- Bank cannot open a generic account
To explain this concept, let's create a banking application. This application is just for demonstration purposes and should not be used as a design guide.
Using the Code
We have a BankAccount
class which is an abstract class for the reason explained above.
Irrespective of the type of account, there
are certain members/behaviors which are common to all types of accounts and thus should be part of the base class, in our case the BankAccount
class.
Bank Account Common Members
- Account Owner property will have the name of the account holder
- Account Number uniquely identifies a bank account
- Minimum Balance will have the minimum threshold for the account
- Maximum Deposit Amount will have the maximum amount which can be deposited in one go
- Interest Rate is required for all bank accounts
- Transaction Summary records all the transactions taking place in an account
Bank Account Common Behavior
- You can deposit money in it.
- You can withdraw money from it.
- There should be facility to calculate interest.
- User can generate Report/ Summary to see the transactions.
Deposit
and Withdraw
: These are two abstract methods. It looks like Deposit/Withdraw functionality should be the same for a
SavingAccount
and CurrentAccount
but might
not always be as in our case. These methods are abstract because we want that the child class should give its own implementation. (These two methods however can also be created as virtual methods but for the sake of our demonstration, let them be abstract methods.)
The CalculateInterest()
method is implemented in the abstract class and the child class will reuse this functionality,
clearly an advantage of an abstract class over an interface regarding code reuse:
public abstract class BankAccount
{
public string AccountOwnerName { get; set; }
public string AccountNumber { get; set; }
public decimal AccountBalance { get; protected set; }
protected decimal MinAccountBalance { get; set; }
protected decimal MaxDepositAmount { get; set; }
protected decimal InteresetRate { get; set; }
protected string TransactionSummary { get; set; }
protected BankAccount(string accountOwnerName, string accountNumber)
{
AccountOwnerName = accountOwnerName;
AccountNumber = accountNumber;
TransactionSummary = string.Empty;
}
public abstract void Deposit(decimal amount);
public abstract void Withdraw(decimal amount);
public decimal CalculateInterest()
{
return (this.AccountBalance * this.InteresetRate) / 100;
}
public virtual void GenerateAccountReport()
{
Console.WriteLine("Account Owner:{0}, Account Number:{1}, AccountBalance:{2}",
this.AccountOwnerName, this.AccountNumber, this.AccountBalance);
Console.WriteLine("Interest Amount:{0}", CalculateInterest());
Console.WriteLine("{0}", this.TransactionSummary);
}
}
Constructor: Though the BankAccount
class is an abstract class,
it still has a constructor. What is the use of a constructor if an instance of
the abstract class
cannot be created? The constructor is used when we create an instance of SavingAccount
or
CurrentAccount
, so variables defined in the abstract class could be initialized using
the abstract class constructor. Remember that whenever a child class is instantiated,
the constructor of its base class is called first and then the derived class constructor is called.
Some of the fields are
protected
and some are public
, I have kept it like
that for no serious reason. TransactionSummary
is kept
protected
so that only a child class should be able to see and change it.
The
GenerateAccountReport()
method will show the details of
the account including the transaction summary. It is a virtual method. By setting it
as virtual, we are declaring that any subclass can override it to give its own implementation; however,
the default implementation is provided by the base class.
Let's now move to our child classes, i.e.,
SavingAccount
and CurrentAccount
:
public class SavingBankAccount : BankAccount
{
protected int withdrawCount = 0;
public SavingBankAccount(string accountOwnerName, string accountNumber)
:base(accountOwnerName,accountNumber)
{
this.MinAccountBalance = 20000m;
this.MaxDepositAmount = 50000m;
InteresetRate = 3.5m;
}
public override void Deposit(decimal amount)
{
if (amount >= MaxDepositAmount)
{
throw new Exception(string.Format("You can not deposit amount
greater than {0}", MaxDepositAmount.ToString()));
}
AccountBalance = AccountBalance + amount;
TransactionSummary = string.Format("{0}\n Deposit:{1}",
TransactionSummary, amount);
}
public override void Withdraw(decimal amount)
{
if (withdrawCount > 3)
{
throw new Exception("You can not withdraw amount more than thrice");
}
if (AccountBalance - amount <= MinAccountBalance)
{
throw new Exception("You can not withdraw amount from your
Savings Account as Minimum Balance limit is reached");
}
AccountBalance = AccountBalance - amount;
withdrawCount++;
TransactionSummary = string.Format("{0}\n Withdraw:{1}",
TransactionSummary, amount);
}
public override void GenerateAccountReport()
{
Console.WriteLine("Saving Account Report");
base.GenerateAccountReport();
if(AccountBalance > 15000)
{
Console.WriteLine("Sending Email for Account {0}", AccountNumber);
}
}
}
Let's see our CurrentAccount
class which also derives from the abstract base class.
public class CurrentBankAccount : BankAccount
{
public CurrentBankAccount(string accountOwnerName, string accountNumber)
:base(accountOwnerName,accountNumber)
{
this.MinAccountBalance = 0m;
this.MaxDepositAmount = 100000000m;
InteresetRate = .25m;
}
public override void Deposit(decimal amount)
{
AccountBalance = AccountBalance + amount;
TransactionSummary = string.Format("{0}\n Deposit:{1}",
TransactionSummary, amount);
}
public override void Withdraw(decimal amount)
{
if (AccountBalance - amount <= MinAccountBalance)
{
throw new Exception("You can not withdraw amount from
your Current Account as Minimum Balance limit is reached");
}
AccountBalance = AccountBalance - amount;
TransactionSummary = string.Format("{0}\n Withdraw:{1}",
TransactionSummary, amount);
}
public override void GenerateAccountReport()
{
Console.WriteLine("Current Account Report");
base.GenerateAccountReport();
}
}
Let's dig inside our child classes. The constructors of the SavingAccount
as well as
CurrentAccount
are initializing some variable as per their requirements,
however certain common variables are set by the abstract class, which explains the rationale behind the need of a constructor in
the abstract class.
The Withdraw
and Deposit
methods are pretty simple and need no detailed explanation. Both classes have overridden them to provide their own implementation.
The Deposit
method of SavingAccount
throws an exception if
the amount deposited is greater than a specified limit.
The Withdraw
method of
SavingAccount
checks the number
of withdrawals before throwing an exception.
The GenerateAccountReport
method of
SavingAccount
adds a report header, calls the base class method for the generic implementation,
and then sends the account report email.
To use the above code, here is a our
Main
method which creates an instance each of a Saving and Current
account. The variable taken to store
these instance is of type BankAccount
, thus allowing us to have polymorphic behavior.
public static void Main(string[] args)
{
BankAccount savingAccount = new SavingBankAccount("Sarvesh", "S12345");
BankAccount currentAccount = new CurrentBankAccount("Mark", "C12345");
savingAccount.Deposit(40000);
savingAccount.Withdraw(1000);
savingAccount.Withdraw(1000);
savingAccount.Withdraw(1000);
savingAccount.GenerateAccountReport();
Console.WriteLine();
currentAccount.Deposit(190000);
currentAccount.Withdraw(1000);
currentAccount.GenerateAccountReport();
Console.ReadLine();
}
Here is the output
Saving Account Report
Account Owner:Sarvesh, Account Number:S12345, AccountBalance:37000
Interest Amount:1295.0
Deposit:40000
Withdraw:1000
Withdraw:1000
Withdraw:1000
Sending Email for Account S12345
Current Account Report
Account Owner:Mark, Account Number:C12345, AccountBalance:189000
Interest Amount:472.50
Deposit:190000
Withdraw:1000
When to Use Abstract Class
Let's go back to the example of our banking application.
Though we cannot open a generic account, all accounts will have
a certain member and behavior as discussed above.
Also, we want that all types of accounts should conform to these attributes and behaviors.
To summarize,
create an abstract class if:
-
Class expresses an idea which is too generic and whose
independent (alone) existence is not required in your application,
e.g.,
BankAccount
.
- There is a family of types forming a hierarchy. An “IS-A” relation exists between a base class and other derived classes. E.g.:
- Saving Account IS-A Bank Account
- Current Account IS-A Bank Account
- If there are certain members/ behaviors which are common
to all classes, these should be placed inside the abstract class, e.g.,
AccountNumber
, Deposit()
, etc.
- If there are behaviors/attributes which should be implemented or
must be present in all classes, declare them as abstract methods in the
abstract class, e.g.,
CalculateInterest()
.
Why Not
an Interface in Place of the BankAccount Class?
In our example, you can argue that we can use
a Interface instead of the BankAccount
abstract class, something like shown below:
public class SavingBankAccount : IBankAccount
{
void Deposit(decimal amount);
void Withdraw(decimal amount);
decimal CalculateInterest();
}
First of all, there is a hierarchy between BankAccount
and SavingAccount
and a close relation exists. Also, we are able to figure
out certain common features present in all the child classes,
thus an abstract class will help in code reuse. An interface is more of a contract between classes. There are
a lot of syntactical differences between an abstract class and an interface,
a little Googling may be of great help so I haven't touched it in this article.
Conclusion
I have explained what an abstract class is, how to use it, and when to use it with the help of an example.