Course content
Liskov Substitution: Refactor the Account Hierarchy
Medium·Tagssolidliskov-substitutioninterface-segregationrefactoring
Problem Statement
The starter has three classes:
- `Account` is the base class with `deposit`, `withdraw`, and `getBalance`.
- `SavingsAccount extends Account` and inherits everything.
- `FixedDepositAccount extends Account` and overrides `withdraw` to throw `UnsupportedOperationException`, because a fixed deposit cannot be withdrawn from before maturity.
This violates the Liskov Substitution Principle. Any function that holds an `Account` reference and calls `withdraw` works for `SavingsAccount` but breaks for `FixedDepositAccount`. The base type promised a capability that one of its subtypes cannot honour.
Refactor the design so that any `Account` reference is safe to use polymorphically. The fix is to move the withdraw capability out of the base class and into a separate interface. After your refactor, the design must expose this exact public API:
```java
class Account {
public Account(double balance);
public double getBalance();
public void deposit(double amount);
// No withdraw on the base class.
}
interface Withdrawable {
void withdraw(double amount);
}
class SavingsAccount extends Account implements Withdrawable { ... }
class FixedDepositAccount extends Account { ... } // does NOT implement Withdrawable
```
The `Withdrawable` interface is already declared in the starter. Your job is to:
1. Remove the `withdraw` method from `Account`.
2. Remove the broken `withdraw` override from `FixedDepositAccount`.
3. Make `SavingsAccount` implement `Withdrawable` and define `withdraw` correctly (subtract the amount, but reject withdrawals that exceed the balance with `IllegalStateException`).
The validator runs five checks:
1. `SavingsAccount` supports the full lifecycle: deposit and withdraw work correctly, and overdrawing throws `IllegalStateException`.
2. `FixedDepositAccount` does not expose a `withdraw` method at all. The validator uses reflection to confirm that calling `getMethod("withdraw", double.class)` on `FixedDepositAccount` throws `NoSuchMethodException`. This forces both the removal of `withdraw` from `Account` and the removal of the override from `FixedDepositAccount`.
3. `SavingsAccount` implements `Withdrawable`. The validator uses reflection to confirm `Withdrawable.class.isAssignableFrom(SavingsAccount.class)` returns true.
4. `Account` does not declare a `withdraw` method. Reflection check, mirrors the requirement that withdraw belongs on the capability interface, not on the base class.
5. A `SavingsAccount` cast to `Withdrawable` behaves correctly when `withdraw` is invoked through the interface reference. This is the LSP test in its purest form: substituting a subtype for the abstraction does not change the contract.
The smelly starter passes only check 1 out of the box. The other four require the actual refactor.
Examples
Example 1
Input
Withdrawable w = new SavingsAccount(1000); w.withdraw(300); ((SavingsAccount) w).getBalance()Output
"700"Why
A SavingsAccount can be substituted for any Withdrawable reference. The withdraw call subtracts 300 from the balance.
Example 2
Input
FixedDepositAccount fd = new FixedDepositAccount(1000); fd.deposit(500); fd.getBalance()Output
"1500"Why
Fixed deposits support deposit and getBalance through the Account base. They do not expose withdraw at all.
Example 3
Input
FixedDepositAccount fd = new FixedDepositAccount(1000); fd.withdraw(100)Output
"compile error"Why
After the refactor, FixedDepositAccount does not have a withdraw method. The compiler rejects the call, which is exactly the protection LSP is meant to provide.
Constraints
- •Refactor must keep the four declarations named: Account, SavingsAccount, FixedDepositAccount, Withdrawable.
- •Account must not declare a withdraw method.
- •FixedDepositAccount must not declare or inherit a withdraw method.
- •SavingsAccount must implement Withdrawable.
- •SavingsAccount.withdraw must throw IllegalStateException when the requested amount exceeds the current balance.
Hints
Stuck? Reveal a nudge toward the right pattern, one step at a time.
Hint 1
Start by deleting the withdraw method from Account. The point of the refactor is that Account should not promise something one of its subtypes cannot deliver.
Hint 2
Delete the broken withdraw override from FixedDepositAccount as well. Fixed deposits do not have a withdraw operation, so the class should not have a withdraw method at all.
Hint 3
Make SavingsAccount implement Withdrawable. Move the working withdraw logic (the same overdraft check the starter Account had) into SavingsAccount.
Hint 4
The validator uses reflection on FixedDepositAccount.class.getMethod("withdraw", double.class). getMethod walks the inheritance chain, so this throws NoSuchMethodException only when neither FixedDepositAccount nor Account declares a withdraw method. That is the architectural property you are trying to establish.
Hint 5
Resist the temptation to make FixedDepositAccount.withdraw a no-op or to weaken its exception. The fix is structural: withdraw should not be on FixedDepositAccount's API surface at all.