# 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 November 3, 2026

## 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 `abstract` class can't be created with `new`. Derive a concrete class, such as `CheckingAccount`, 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 `virtual` method, but must override an `abstract` one.
- An abstract class can still have properties, a constructor and finished methods such as `Deposit`, and a `List<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`:

```csharp
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;
    }
}
```

```text
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:

```csharp
CheckingAccount checking = new CheckingAccount("1001", 500m);
checking.Deposit(250m);

Console.WriteLine($"{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)
    {
    }
}
```

```text
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.

```csharp
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:

```csharp
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($"{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;
    }
}
```

```text
1001: withdrew $80.00, balance -30.00
2001: refused, balance 50.00
```

It's polymorphism, as in the last chapter: one loop over `BankAccount`s, 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:

```csharp
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)
    {
    }
}
```

```text
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:

```csharp
List<BankAccount> accounts = new List<BankAccount>
{
    new CheckingAccount("1001", 50m),
    new SavingsAccount("2001", 1200m)
};

foreach (BankAccount account in accounts)
{
    Console.WriteLine($"{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";
}
```

```text
Checking 1001: $50.00
Savings 2001: $1,200.00
```

## The rules

- An `abstract` class **can't be created** with `new`. Its concrete derived classes can.
- An `abstract` method 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 `virtual` ones, 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.

## Key terms

- [Concrete Class](https://www.ask-codey.com/glossary/concrete-class)

## Cheat sheets

- [Adding Abstraction](https://www.ask-codey.com/cheat-sheets/abstraction-in-csharp-adding-abstraction-cheat-sheet)

Source: https://www.ask-codey.com/lessons/abstraction-in-csharp-adding-abstraction
