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
{
//
Name of the Account Owner, Its common for all derived classes
public string AccountOwnerName { get; set; }
//
Account Number field is a common field for all the account types
public string AccountNumber { get; set; }
//
A field to hold the Account Balance
public decimal AccountBalance { get; protected set; }
//
A field to hold the MinimumAccount Balance
protected decimal MinAccountBalance { get; set; }
//
A field to hold the Max Deposit Amount Balance
protected decimal MaxDepositAmount { get; set; }
protected decimal InteresetRate { get; set; }
//
this variable will hold the summary of all the transaction that took place
protected string TransactionSummary { get; set; }
protected BankAccount(string accountOwnerName, string accountNumber)
{
AccountOwnerName = accountOwnerName;
AccountNumber = accountNumber;
TransactionSummary = string.Empty;
}
// Deposit
is an abstract method so that Saving/Current Account must override
//
it to give their specific implementation.
public abstract void Deposit(decimal amount);
//
Withdraw is an abstract method so that Saving/Current Account must override
//
it to give their specific implementation.
public abstract void Withdraw(decimal amount);
public decimal CalculateInterest()
{
return (this.AccountBalance * this.InteresetRate) / 100;
}
// This
method adds a Reporting functionality
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)
{
//
some hard coded logic that withdraw count should not be greater than 3
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);
}
//
This method adds details to the base class Reporting functionality
public override void GenerateAccountReport()
{
Console.WriteLine("Saving Account Report");
base.GenerateAccountReport();
//
Send an email to user if Savings account balance is less
//
than user specified balance this is different than MinAccountBalance
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);
}
//
This method adds details to the base class Reporting functionality
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.
Hide Copy Code
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);
//
Generate Report
savingAccount.GenerateAccountReport();
Console.WriteLine();
currentAccount.Deposit(190000);
currentAccount.Withdraw(1000);
currentAccount.GenerateAccountReport();
Console.ReadLine();
}
Here is the output
Hide Copy Code
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:
1.
Class expresses an idea which is too generic and whose independent
(alone) existence is not
required in your application, e.g., BankAccount.
2.
There is a family of types forming a hierarchy. An “IS-A” relation exists between a base class and other derived
classes. E.g.:
o
Saving Account IS-A Bank Account
o
Current Account IS-A Bank Account
3.
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.
4.
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:
Hide Copy Code
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.
No comments:
Post a Comment