Adding Abstraction
Mark a base class abstract when it's too general to create on its own. An abstract class can't be created with new, and its abstract methods have no body, so every concrete derived class must override them.
Journeyman · C# · OOP · Updated
After this lesson you can
- Declare an abstract class, and explain why it can't be created with new
- Declare an abstract method or property, and override it in every concrete derived class
Key takeaways
- An
abstractclass can't be created withnew. Derive a concrete class, such asCheckingAccount, and create that. - An abstract method or property has no body, and every concrete derived class must override it or the program doesn't compile.
- A derived class may override a
virtualmethod, but must override anabstractone. - An abstract class can still have properties, a constructor and finished methods such as
Deposit, and aList<BankAccount>can hold any kind of account.
Go to a bank to open an account, and the first question is "What kind?" A checking account or a savings account. Nobody opens "an account" of no kind at all.
Whenever you have to say what kind before you can have one, you're dealing with an abstract idea: an account, a shape (a circle? a square?), an insurance policy (car? home?). In C#, that idea becomes an abstract class.
An abstract class
Add the keyword abstract to a class, and C# won't let anyone create one with new:
BankAccount account = new BankAccount("1001", 500m);
public abstract class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; protected set; }
public BankAccount(string accountNumber, decimal balance)
{
AccountNumber = accountNumber;
Balance = balance;
}
public void Deposit(decimal amount)
{
Balance += amount;
}
}
error CS0144: Cannot create an instance of the abstract type or interface 'BankAccount'
BankAccount still has everything a base class can have: properties, a constructor, and finished methods such as Deposit. It just can't be created on its own.
A concrete class
To get an account, derive a specific kind from it. A class that isn't abstract is called concrete, and a concrete class can be created:
CheckingAccount checking = new CheckingAccount("1001", 500m);
checking.Deposit(250m);
Console.WriteLine( quot;{checking.AccountNumber}: {checking.Balance:C}");
public abstract class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; protected set; }
public BankAccount(string accountNumber, decimal balance)
{
AccountNumber = accountNumber;
Balance = balance;
}
public void Deposit(decimal amount)
{
Balance += amount;
}
}
public class CheckingAccount : BankAccount
{
public CheckingAccount(string accountNumber, decimal balance) : base(accountNumber, balance)
{
}
}
1001: $750.00
The abstract class's constructor still runs: CheckingAccount passes the account number and balance up with : base(...), as any derived class does.
Abstract methods
Every account can take a withdrawal, but each kind has its own rules. A checking account allows an overdraft of up to $100. A savings account never goes below zero. There's no sensible version of Withdraw to write in BankAccount itself.
So declare it as an abstract method: a method with no body, just its signature and a semicolon.
public abstract bool Withdraw(decimal amount);
An abstract method says: every account has this, and every concrete kind must write it. A concrete derived class must override it:
List<BankAccount> accounts = new List<BankAccount>
{
new CheckingAccount("1001", 50m),
new SavingsAccount("2001", 50m)
};
foreach (BankAccount account in accounts)
{
bool done = account.Withdraw(80m);
Console.WriteLine( quot;{account.AccountNumber}: {(done ? "withdrew $80.00" : "refused")}, balance {account.Balance:0.00}");
}
public abstract class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; protected set; }
public BankAccount(string accountNumber, decimal balance)
{
AccountNumber = accountNumber;
Balance = balance;
}
public void Deposit(decimal amount)
{
Balance += amount;
}
public abstract bool Withdraw(decimal amount);
}
public class CheckingAccount : BankAccount
{
private const decimal Overdraft = 100m;
public CheckingAccount(string accountNumber, decimal balance) : base(accountNumber, balance)
{
}
public override bool Withdraw(decimal amount)
{
if (Balance - amount < -Overdraft)
{
return false;
}
Balance -= amount;
return true;
}
}
public class SavingsAccount : BankAccount
{
public SavingsAccount(string accountNumber, decimal balance) : base(accountNumber, balance)
{
}
public override bool Withdraw(decimal amount)
{
if (amount > Balance)
{
return false;
}
Balance -= amount;
return true;
}
}
1001: withdrew $80.00, balance -30.00
2001: refused, balance 50.00
It's polymorphism, as in the last chapter: one loop over BankAccounts, and each account withdraws by its own rules. The difference is the guarantee. With a virtual method, a derived class may override. With an abstract method, it must.
Forgetting the override
Leave Withdraw out of a concrete class, and the program doesn't compile:
SavingsAccount savings = new SavingsAccount("2001", 50m);
public abstract class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; protected set; }
public BankAccount(string accountNumber, decimal balance)
{
AccountNumber = accountNumber;
Balance = balance;
}
public abstract bool Withdraw(decimal amount);
}
public class SavingsAccount : BankAccount
{
public SavingsAccount(string accountNumber, decimal balance) : base(accountNumber, balance)
{
}
}
error CS0534: 'SavingsAccount' does not implement inherited abstract member 'BankAccount.Withdraw(decimal)'
That's the point: no kind of account can be forgotten. (In Visual Studio, put the cursor on the red line and press Ctrl+., then Implement abstract class, and it writes the empty overrides for you.)
Abstract properties
Properties can be abstract too. Each kind of account has a name for its kind, so BankAccount can ask for one:
List<BankAccount> accounts = new List<BankAccount>
{
new CheckingAccount("1001", 50m),
new SavingsAccount("2001", 1200m)
};
foreach (BankAccount account in accounts)
{
Console.WriteLine( quot;{account.Kind} {account.AccountNumber}: {account.Balance:C}");
}
public abstract class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; protected set; }
public abstract string Kind { get; }
public BankAccount(string accountNumber, decimal balance)
{
AccountNumber = accountNumber;
Balance = balance;
}
}
public class CheckingAccount : BankAccount
{
public CheckingAccount(string accountNumber, decimal balance) : base(accountNumber, balance)
{
}
public override string Kind => "Checking";
}
public class SavingsAccount : BankAccount
{
public SavingsAccount(string accountNumber, decimal balance) : base(accountNumber, balance)
{
}
public override string Kind => "Savings";
}
Checking 1001: $50.00
Savings 2001: $1,200.00
The rules
- An
abstractclass can't be created withnew. Its concrete derived classes can. - An
abstractmethod or property has no body, and every concrete derived class must override it. - Only an abstract class can have abstract members. (A concrete class with one would be a class you could create but couldn't use.)
- An abstract class can also have ordinary members: properties, constructors, finished methods and
virtualones, all inherited as usual. - A variable or list can still be of the abstract type:
List<BankAccount>holds any kind of account.
Check yourself
2 questions to try on the lesson page.
So what
An abstract class says "be specific": you can't create a plain BankAccount, and every kind of account must write its own Withdraw. The compiler checks both rules, so a new kind of account can never be added half-finished.